Compare commits
10 commits
2687062906
...
0cdb20be04
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0cdb20be04 | ||
|
|
4cd0cf607e | ||
|
|
c54baec4ed | ||
|
|
8f36e6cca2 | ||
|
|
6925fde0e9 | ||
|
|
f02d13e3dc | ||
|
|
a756677c89 | ||
|
|
ae14189cd3 | ||
|
|
05b6364ae5 | ||
|
|
6e2fc1de6c |
28 changed files with 782 additions and 32 deletions
63
frontend/package-lock.json
generated
63
frontend/package-lock.json
generated
|
|
@ -8,6 +8,7 @@
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@monaco-editor/react": "^4.6.0",
|
||||||
"@testing-library/jest-dom": "^5.16.5",
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
"@testing-library/react": "^13.4.0",
|
"@testing-library/react": "^13.4.0",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
|
|
@ -2977,6 +2978,30 @@
|
||||||
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz",
|
||||||
"integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A=="
|
"integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@monaco-editor/loader": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==",
|
||||||
|
"dependencies": {
|
||||||
|
"state-local": "^1.0.6"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"monaco-editor": ">= 0.21.0 < 1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@monaco-editor/react": {
|
||||||
|
"version": "4.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.6.0.tgz",
|
||||||
|
"integrity": "sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@monaco-editor/loader": "^1.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"monaco-editor": ">= 0.25.0 < 1",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||||
|
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@nicolo-ribaudo/eslint-scope-5-internals": {
|
"node_modules/@nicolo-ribaudo/eslint-scope-5-internals": {
|
||||||
"version": "5.1.1-v1",
|
"version": "5.1.1-v1",
|
||||||
"resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz",
|
"resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz",
|
||||||
|
|
@ -11945,6 +11970,12 @@
|
||||||
"mobx": "^6.3.0"
|
"mobx": "^6.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/monaco-editor": {
|
||||||
|
"version": "0.44.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.44.0.tgz",
|
||||||
|
"integrity": "sha512-5SmjNStN6bSuSE5WPT2ZV+iYn1/yI9sd4Igtk23ChvqB7kDk9lZbB9F5frsuvpB+2njdIeGGFf2G4gbE6rCC9Q==",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
|
|
@ -15205,6 +15236,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
|
||||||
"integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="
|
"integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/state-local": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz",
|
||||||
|
"integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w=="
|
||||||
|
},
|
||||||
"node_modules/statuses": {
|
"node_modules/statuses": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||||
|
|
@ -19072,6 +19108,22 @@
|
||||||
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz",
|
||||||
"integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A=="
|
"integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A=="
|
||||||
},
|
},
|
||||||
|
"@monaco-editor/loader": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==",
|
||||||
|
"requires": {
|
||||||
|
"state-local": "^1.0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@monaco-editor/react": {
|
||||||
|
"version": "4.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.6.0.tgz",
|
||||||
|
"integrity": "sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==",
|
||||||
|
"requires": {
|
||||||
|
"@monaco-editor/loader": "^1.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@nicolo-ribaudo/eslint-scope-5-internals": {
|
"@nicolo-ribaudo/eslint-scope-5-internals": {
|
||||||
"version": "5.1.1-v1",
|
"version": "5.1.1-v1",
|
||||||
"resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz",
|
"resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz",
|
||||||
|
|
@ -25596,6 +25648,12 @@
|
||||||
"integrity": "sha512-oe82BNgMr408e6DxMDNat8msXQTuyuqzJ97DPupbhchEfjjHyjsmPSwtXHl+nXiW3tybpb/cr5siUClBqKqv+Q==",
|
"integrity": "sha512-oe82BNgMr408e6DxMDNat8msXQTuyuqzJ97DPupbhchEfjjHyjsmPSwtXHl+nXiW3tybpb/cr5siUClBqKqv+Q==",
|
||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
|
"monaco-editor": {
|
||||||
|
"version": "0.44.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.44.0.tgz",
|
||||||
|
"integrity": "sha512-5SmjNStN6bSuSE5WPT2ZV+iYn1/yI9sd4Igtk23ChvqB7kDk9lZbB9F5frsuvpB+2njdIeGGFf2G4gbE6rCC9Q==",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"ms": {
|
"ms": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
|
|
@ -27772,6 +27830,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
|
||||||
"integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="
|
"integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="
|
||||||
},
|
},
|
||||||
|
"state-local": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz",
|
||||||
|
"integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w=="
|
||||||
|
},
|
||||||
"statuses": {
|
"statuses": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@monaco-editor/react": "^4.6.0",
|
||||||
"@testing-library/jest-dom": "^5.16.5",
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
"@testing-library/react": "^13.4.0",
|
"@testing-library/react": "^13.4.0",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,19 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
import * as Store from './dashboard-store';
|
||||||
|
import { Dashboard } from './dashboard';
|
||||||
|
|
||||||
export const DashboardPage = (): JSX.Element => {
|
export const DashboardPage = (): JSX.Element => {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const id = params.id as string;
|
const id = params.id as string;
|
||||||
return <p>Dashboard</p>;
|
|
||||||
|
const store = Store.Dashboard.create({
|
||||||
|
id: id,
|
||||||
|
loaded: false,
|
||||||
|
});
|
||||||
|
Store.DashboardLoadData(store).then(() => {
|
||||||
|
return Store.LoadDataForWidgets(store);
|
||||||
|
});
|
||||||
|
|
||||||
|
return <Dashboard store={store} />;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
154
frontend/src/dashboard/dashboard-store.tsx
Normal file
154
frontend/src/dashboard/dashboard-store.tsx
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
import { Instance, types } from 'mobx-state-tree';
|
||||||
|
|
||||||
|
type _WidgetParams = Record<string, any> | null;
|
||||||
|
|
||||||
|
export const WidgetParams = types.frozen<_WidgetParams>();
|
||||||
|
|
||||||
|
type _DataLoaderParams = Record<string, any> | null;
|
||||||
|
|
||||||
|
export const DataLoaderParams = types.frozen<_DataLoaderParams>();
|
||||||
|
|
||||||
|
export const Widget = types
|
||||||
|
.model({
|
||||||
|
type: types.string,
|
||||||
|
id: types.string,
|
||||||
|
title: types.string,
|
||||||
|
collapsed: types.maybe(types.boolean),
|
||||||
|
visible: types.boolean,
|
||||||
|
widgetParams: types.maybe(WidgetParams),
|
||||||
|
dataLoaderParams: types.maybe(DataLoaderParams),
|
||||||
|
loaded: false,
|
||||||
|
data: types.maybe(types.frozen<any>()),
|
||||||
|
dashboardId: types.maybe(types.string),
|
||||||
|
})
|
||||||
|
.actions((self) => {
|
||||||
|
return {
|
||||||
|
show: () => {
|
||||||
|
self.visible = false;
|
||||||
|
},
|
||||||
|
hide: () => {
|
||||||
|
self.visible = true;
|
||||||
|
},
|
||||||
|
toggle: () => {
|
||||||
|
self.visible = !self.visible;
|
||||||
|
},
|
||||||
|
setData: (data: any) => {
|
||||||
|
self.loaded = true;
|
||||||
|
self.data = data;
|
||||||
|
},
|
||||||
|
setDashboardId: (dashboardId: string) => {
|
||||||
|
self.dashboardId = dashboardId;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export type IWidget = Instance<typeof Widget>;
|
||||||
|
|
||||||
|
export function createWidgetStore(
|
||||||
|
id: string,
|
||||||
|
type: string,
|
||||||
|
title: string,
|
||||||
|
collapsed = false,
|
||||||
|
widgetParams?: _WidgetParams,
|
||||||
|
dataLoaderParams?: _DataLoaderParams,
|
||||||
|
): IWidget {
|
||||||
|
return Widget.create({
|
||||||
|
id: id,
|
||||||
|
type: type,
|
||||||
|
title: title,
|
||||||
|
collapsed: collapsed,
|
||||||
|
visible: !collapsed,
|
||||||
|
widgetParams: widgetParams,
|
||||||
|
dataLoaderParams: dataLoaderParams,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Data = types.model({
|
||||||
|
widgets: types.array(Widget),
|
||||||
|
title: types.maybe(types.string),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Dashboard = types
|
||||||
|
.model({
|
||||||
|
loaded: types.boolean,
|
||||||
|
id: types.string,
|
||||||
|
data: types.maybe(Data),
|
||||||
|
})
|
||||||
|
.actions((self) => {
|
||||||
|
return {
|
||||||
|
setData: (data: any) => {
|
||||||
|
if (data.widgets) {
|
||||||
|
for (let i = 0; i < data.widgets.length; i++) {
|
||||||
|
const widget = data.widgets[i];
|
||||||
|
widget.visible = !widget.collapsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.data = data;
|
||||||
|
self.loaded = true;
|
||||||
|
},
|
||||||
|
setWidgetsData: (data: any) => {
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
const widgetData = data[i]?.data;
|
||||||
|
const widgetId = data[i]?.widgetId;
|
||||||
|
if (!widgetId || !widgetData) continue;
|
||||||
|
|
||||||
|
const widgets = self.data?.widgets;
|
||||||
|
if (!widgets || widgets.length <= 0) return;
|
||||||
|
|
||||||
|
const widget = widgets.find((w) => w.id == widgetId);
|
||||||
|
if (!widget) continue;
|
||||||
|
|
||||||
|
widget.setData(widgetData);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setWidgetData: (widgetId: string, data: any) => {
|
||||||
|
const widget = self.data?.widgets.find((w) => w.id == widgetId);
|
||||||
|
if (!widget) return;
|
||||||
|
widget.setData(data);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export type IDashboard = Instance<typeof Dashboard>;
|
||||||
|
|
||||||
|
export async function DashboardLoadData(store: IDashboard): Promise<void> {
|
||||||
|
const url = `${process.env.REACT_APP_BACKEND}api/dashboard/${store.id}`;
|
||||||
|
const resp = await axios.get(url);
|
||||||
|
if (!resp.data) return;
|
||||||
|
store.setData(resp.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function LoadDataForWidgets(store: IDashboard): Promise<void> {
|
||||||
|
const url = `${process.env.REACT_APP_BACKEND}api/dashboard/${store.id}/load-data`;
|
||||||
|
const resp = await fetch(url);
|
||||||
|
if (resp && resp.ok) {
|
||||||
|
const data = await resp.json();
|
||||||
|
store.setWidgetsData(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function LoadDataForWidget(
|
||||||
|
store: IDashboard,
|
||||||
|
widgetId: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const url = `${process.env.REACT_APP_BACKEND}api/dashboard/${store.id}/load-data/${widgetId}`;
|
||||||
|
const resp = await fetch(url);
|
||||||
|
if (resp && resp.ok) {
|
||||||
|
const data = await resp.json();
|
||||||
|
store.setWidgetData(widgetId, data);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function WidgetLoadData(widget: IWidget): Promise<void> {
|
||||||
|
if (!widget.dashboardId) return;
|
||||||
|
const url = `${process.env.REACT_APP_BACKEND}api/dashboard/${widget.dashboardId}/load-data/${widget.id}`;
|
||||||
|
const resp = await fetch(url);
|
||||||
|
if (resp && resp.ok) {
|
||||||
|
const data = await resp.json();
|
||||||
|
if (data) {
|
||||||
|
widget.setData(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
frontend/src/dashboard/dashboard.tsx
Normal file
43
frontend/src/dashboard/dashboard.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
import React from 'react';
|
||||||
|
import * as DashboardStoreNs from './dashboard-store';
|
||||||
|
import * as TopRightMenuNs from '../misc-components/top-right-menu';
|
||||||
|
import * as WidgetNs from './widget';
|
||||||
|
import * as EditorNs from './editor';
|
||||||
|
|
||||||
|
export type Props = { store: DashboardStoreNs.IDashboard };
|
||||||
|
|
||||||
|
export const Dashboard = observer((props: Props): JSX.Element => {
|
||||||
|
if (!props.store.loaded) {
|
||||||
|
console.debug('Dashboard - store:', JSON.stringify(props.store)); // DEBUG
|
||||||
|
return <pre>Loading... {JSON.stringify(props.store)}</pre>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const topRightMenuStore = TopRightMenuNs.Store.create({ visible: false });
|
||||||
|
|
||||||
|
const widgets = props.store.data?.widgets.map((widget) => {
|
||||||
|
widget.setDashboardId(props.store.id);
|
||||||
|
return <WidgetNs.Widget key={widget.id} store={widget}></WidgetNs.Widget>;
|
||||||
|
});
|
||||||
|
|
||||||
|
const editorStore = EditorNs.Store.create({ dashboardId: props.store.id });
|
||||||
|
const onEditClick = (e: React.MouseEvent) => {
|
||||||
|
if (e.target !== e.currentTarget) return;
|
||||||
|
e.stopPropagation();
|
||||||
|
editorStore.show();
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = (
|
||||||
|
<div>
|
||||||
|
<EditorNs.Editor store={editorStore}></EditorNs.Editor>
|
||||||
|
<TopRightMenuNs.TopRightMenu store={topRightMenuStore}>
|
||||||
|
<a href="/dashboards">Назад</a>
|
||||||
|
<span>Дашборд - {props.store.data?.title || props.store.id}</span>
|
||||||
|
<button onClick={onEditClick}>edit</button>
|
||||||
|
</TopRightMenuNs.TopRightMenu>
|
||||||
|
{widgets}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
});
|
||||||
23
frontend/src/dashboard/dashboards-list.tsx
Normal file
23
frontend/src/dashboard/dashboards-list.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import React from 'react';
|
||||||
|
import * as Store from './dashboards-store';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
|
||||||
|
export type Props = { store: Store.IList };
|
||||||
|
|
||||||
|
export const DashboardsList = observer((props: Props): JSX.Element => {
|
||||||
|
if (!props.store.loaded) {
|
||||||
|
return <div>Loading...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul>
|
||||||
|
{props.store.list.map((item) => {
|
||||||
|
return (
|
||||||
|
<li key={item.id}>
|
||||||
|
<a href={`/dashboard/${item.id}`}>{item.title}</a>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
@ -1,6 +1,51 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { DashboardsList } from './dashboards-list';
|
||||||
|
import * as Store from './dashboards-store';
|
||||||
|
|
||||||
export const DashboardsPage = (): JSX.Element => {
|
export const DashboardsPage = (): JSX.Element => {
|
||||||
// TODO: code for DashboardsPage
|
const store = Store.List.create({ loaded: false, list: [] });
|
||||||
return <p>Dashboards</p>;
|
Store.ListStoreLoadData(store);
|
||||||
|
|
||||||
|
const onNewDashboardButtonClick = async (e: React.MouseEvent) => {
|
||||||
|
if (e.target !== e.currentTarget) return;
|
||||||
|
e.stopPropagation();
|
||||||
|
const createUrl = `${process.env.REACT_APP_BACKEND}api/dashboard`;
|
||||||
|
const createResp = await fetch(createUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
if (!createResp || !createResp.ok) {
|
||||||
|
alert(`Ошибка - Не удалось создать новый дашборд`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const dashboardId = await createResp.text();
|
||||||
|
const dashboardName = prompt(
|
||||||
|
`Dashboard name for dashboardId = ${dashboardId}`,
|
||||||
|
);
|
||||||
|
if (dashboardName) {
|
||||||
|
const modifyUrl = `${process.env.REACT_APP_BACKEND}api/dashboard/${dashboardId}`;
|
||||||
|
const modifyResp = await fetch(modifyUrl, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ widgets: [], title: dashboardName }),
|
||||||
|
});
|
||||||
|
if (!modifyResp || !modifyResp.ok) {
|
||||||
|
alert(`Не удалось выполнить создание дашборда`);
|
||||||
|
} else {
|
||||||
|
alert(`Создан дашборд ${dashboardName}`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
alert(`Создан анонимный дашборд ${dashboardId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button onClick={onNewDashboardButtonClick}>New dashboard</button>
|
||||||
|
<DashboardsList store={store} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
32
frontend/src/dashboard/dashboards-store.ts
Normal file
32
frontend/src/dashboard/dashboards-store.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
import { Instance, types } from 'mobx-state-tree';
|
||||||
|
|
||||||
|
export const Item = types.model({
|
||||||
|
id: types.string,
|
||||||
|
title: types.string,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type IItem = Instance<typeof Item>;
|
||||||
|
|
||||||
|
export const List = types
|
||||||
|
.model({
|
||||||
|
list: types.array(Item),
|
||||||
|
loaded: types.boolean,
|
||||||
|
})
|
||||||
|
.actions((self) => {
|
||||||
|
return {
|
||||||
|
setList: (data: any) => {
|
||||||
|
self.list = data;
|
||||||
|
self.loaded = true;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export type IList = Instance<typeof List>;
|
||||||
|
|
||||||
|
export async function ListStoreLoadData(store: IList): Promise<void> {
|
||||||
|
const url = `${process.env.REACT_APP_BACKEND}api/dashboards`;
|
||||||
|
const resp = await axios.get(url);
|
||||||
|
if (!resp?.data) return;
|
||||||
|
store.setList(resp.data);
|
||||||
|
}
|
||||||
27
frontend/src/dashboard/editor.module.css
Normal file
27
frontend/src/dashboard/editor.module.css
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
.reset {
|
||||||
|
all: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
z-index: 1000;
|
||||||
|
position: fixed;
|
||||||
|
display: none;
|
||||||
|
padding-top: 40px;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
background-color: rgb(0, 0, 0);
|
||||||
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalContent {
|
||||||
|
height: 80%;
|
||||||
|
overflow: auto;
|
||||||
|
background-color: #fefefe;
|
||||||
|
margin: auto;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #888;
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
122
frontend/src/dashboard/editor.tsx
Normal file
122
frontend/src/dashboard/editor.tsx
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Editor as MonacoEditor } from '@monaco-editor/react';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
import { Instance, onSnapshot, types } from 'mobx-state-tree';
|
||||||
|
import Css from './editor.module.css';
|
||||||
|
|
||||||
|
export const Store = types
|
||||||
|
.model({
|
||||||
|
loaded: false,
|
||||||
|
dashboardId: '',
|
||||||
|
visible: false,
|
||||||
|
data: '',
|
||||||
|
})
|
||||||
|
.actions((self) => {
|
||||||
|
return {
|
||||||
|
setData: (data: string) => {
|
||||||
|
self.loaded = true;
|
||||||
|
self.data = data;
|
||||||
|
},
|
||||||
|
show: () => {
|
||||||
|
if (!self.loaded) LoadDashboardToStore(self as any);
|
||||||
|
self.visible = true;
|
||||||
|
},
|
||||||
|
hide: () => {
|
||||||
|
self.visible = false;
|
||||||
|
},
|
||||||
|
toggleVisible: () => {
|
||||||
|
self.visible = !self.visible;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.views((self) => {
|
||||||
|
return {
|
||||||
|
get displayStyle(): React.CSSProperties {
|
||||||
|
return { display: self.visible ? 'block' : 'none' };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
type IStore = Instance<typeof Store>;
|
||||||
|
|
||||||
|
export async function LoadDashboard(dashboardId: string): Promise<string> {
|
||||||
|
const url = `${process.env.REACT_APP_BACKEND}api/dashboard/${dashboardId}`;
|
||||||
|
const resp = await fetch(url);
|
||||||
|
if (!resp || !resp.ok) return '';
|
||||||
|
const data = await resp.json();
|
||||||
|
const text = JSON.stringify(data, null, ' ');
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function LoadDashboardToStore(store: IStore): Promise<void> {
|
||||||
|
const data = await LoadDashboard(store.dashboardId);
|
||||||
|
if (data) store.setData(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function SaveDashboard(
|
||||||
|
dashboardId: string,
|
||||||
|
data: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const url = `${process.env.REACT_APP_BACKEND}api/dashboard/${dashboardId}`;
|
||||||
|
await fetch(url, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function SaveDashboardFromStore(store: IStore): Promise<void> {
|
||||||
|
await SaveDashboard(store.dashboardId, store.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
store: IStore;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Editor = observer((props: Props): JSX.Element => {
|
||||||
|
const onCloseClick = (e: React.MouseEvent) => {
|
||||||
|
if (e.target !== e.currentTarget) return;
|
||||||
|
e.stopPropagation();
|
||||||
|
props.store.hide();
|
||||||
|
};
|
||||||
|
const onSaveClick = async (e: React.MouseEvent) => {
|
||||||
|
if (e.target !== e.currentTarget) return;
|
||||||
|
e.stopPropagation();
|
||||||
|
props.store.setData(editorValue);
|
||||||
|
await SaveDashboardFromStore(props.store);
|
||||||
|
alert('Сохранено');
|
||||||
|
};
|
||||||
|
|
||||||
|
const [editorValue, setEditorValue] = useState(props.store.data);
|
||||||
|
onSnapshot(props.store, (state) => {
|
||||||
|
setEditorValue(state.data);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={Css.reset}>
|
||||||
|
<div
|
||||||
|
className={Css.modal}
|
||||||
|
style={props.store.displayStyle}
|
||||||
|
onClick={onCloseClick}
|
||||||
|
>
|
||||||
|
<div className={Css.modalContent}>
|
||||||
|
<h1>
|
||||||
|
<button onClick={onCloseClick}>close</button>
|
||||||
|
<button onClick={onSaveClick}>save</button>
|
||||||
|
Редактор дашборда
|
||||||
|
</h1>
|
||||||
|
<MonacoEditor
|
||||||
|
height="80%"
|
||||||
|
defaultLanguage="json"
|
||||||
|
defaultValue={editorValue}
|
||||||
|
value={editorValue}
|
||||||
|
onChange={(value) => setEditorValue(value || '')}
|
||||||
|
></MonacoEditor>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
Editor
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
105
frontend/src/dashboard/widget.tsx
Normal file
105
frontend/src/dashboard/widget.tsx
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
import React from 'react';
|
||||||
|
import * as DashboardStoreNs from './dashboard-store';
|
||||||
|
import * as WidgetFactoryNs from './widgets/widget-factory';
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
store: DashboardStoreNs.IWidget;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Пример данных передаваемых в виджет:
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* "type": "kanban_by_tree",
|
||||||
|
* "id": "first",
|
||||||
|
* "title": "Первый виджет",
|
||||||
|
* "dataLoaderParams": {
|
||||||
|
* "rootIssueId": 2,
|
||||||
|
* "groups": {
|
||||||
|
* "fromIssues": [
|
||||||
|
* {
|
||||||
|
* "issueId": 3,
|
||||||
|
* "name": "Тест"
|
||||||
|
* }
|
||||||
|
* ],
|
||||||
|
* "fromIssuesIncluded": false,
|
||||||
|
* "showOthers": true
|
||||||
|
* },
|
||||||
|
* "statuses": [
|
||||||
|
* "New",
|
||||||
|
* "Re-opened",
|
||||||
|
* "In Progress",
|
||||||
|
* "Code Review",
|
||||||
|
* "Resolved",
|
||||||
|
* "Testing",
|
||||||
|
* "Feedback",
|
||||||
|
* "Wait Release",
|
||||||
|
* "Pending",
|
||||||
|
* "Closed",
|
||||||
|
* "Rejected"
|
||||||
|
* ],
|
||||||
|
* "tags": {
|
||||||
|
* "tagsKeyName": "tags",
|
||||||
|
* "styledTagsKeyName": "styledTags",
|
||||||
|
* "styles": {
|
||||||
|
* "supertag": "background-color: rgb(128, 0, 0); color: rgb(255, 255, 255);"
|
||||||
|
* },
|
||||||
|
* "defaultStyle": "background-color: rgb(128, 128, 128);"
|
||||||
|
* },
|
||||||
|
* "priorities": [
|
||||||
|
* {
|
||||||
|
* "rules": [
|
||||||
|
* {
|
||||||
|
* "priorityName": "P1",
|
||||||
|
* "style": "background-color: #DB2228;"
|
||||||
|
* },
|
||||||
|
* {
|
||||||
|
* "priorityName": "P2",
|
||||||
|
* "style": "background-color: #FA5E26;"
|
||||||
|
* },
|
||||||
|
* {
|
||||||
|
* "priorityName": "P3",
|
||||||
|
* "style": "background-color: #FDAF19;"
|
||||||
|
* },
|
||||||
|
* {
|
||||||
|
* "priorityName": "P4",
|
||||||
|
* "style": "background-color: #31A8FF;",
|
||||||
|
* "default": true
|
||||||
|
* },
|
||||||
|
* {
|
||||||
|
* "priorityName": "P5",
|
||||||
|
* "style": "background-color: #FFFFFF; border: 0.5px solid #393838; color: #202020;"
|
||||||
|
* }
|
||||||
|
* ],
|
||||||
|
* "targetKey": "priorityStyle"
|
||||||
|
* }
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
function onWidgetVisibleToggleClick(store: DashboardStoreNs.IWidget): void {
|
||||||
|
if (!store.loaded && store.dashboardId) {
|
||||||
|
DashboardStoreNs.WidgetLoadData(store);
|
||||||
|
}
|
||||||
|
store.toggle();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Widget = observer((props: Props): JSX.Element => {
|
||||||
|
const display = props.store.visible ? 'block' : 'none';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<button onClick={() => onWidgetVisibleToggleClick(props.store)}>
|
||||||
|
Show/hide
|
||||||
|
</button>
|
||||||
|
<span>Title - {props.store.title}</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: display }}>
|
||||||
|
<WidgetFactoryNs.WidgetFactory store={props.store} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
18
frontend/src/dashboard/widgets/issues-list.tsx
Normal file
18
frontend/src/dashboard/widgets/issues-list.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
import React from 'react';
|
||||||
|
import * as DashboardStoreNs from '../dashboard-store';
|
||||||
|
import * as IssuesListBoardsNs from '../../issues-list-board/issues-list-boards';
|
||||||
|
import * as KanbanBoardStoreNs from '../../kanban-board/store';
|
||||||
|
import { onSnapshot } from 'mobx-state-tree';
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
store: DashboardStoreNs.IWidget;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IssuesList = observer((props: Props): JSX.Element => {
|
||||||
|
const store = KanbanBoardStoreNs.PageStore.create({ loaded: false });
|
||||||
|
onSnapshot(props.store, (state) => {
|
||||||
|
if (state?.data) store.setData(state?.data);
|
||||||
|
});
|
||||||
|
return <IssuesListBoardsNs.IssuesListBoards store={store} />;
|
||||||
|
});
|
||||||
17
frontend/src/dashboard/widgets/kanban.tsx
Normal file
17
frontend/src/dashboard/widgets/kanban.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { Instance, onSnapshot } from 'mobx-state-tree';
|
||||||
|
import * as DashboardStoreNs from '../dashboard-store';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
import * as KanbanBoardsNs from '../../kanban-board/kanban-boards';
|
||||||
|
import * as KanbanBoardsStoreNs from '../../kanban-board/store';
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
store: Instance<typeof DashboardStoreNs.Widget>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Kanban = observer((props: Props): JSX.Element => {
|
||||||
|
const store = KanbanBoardsStoreNs.PageStore.create({ loaded: false });
|
||||||
|
onSnapshot(props.store, (state) => {
|
||||||
|
if (state?.data) store.setData(state?.data);
|
||||||
|
});
|
||||||
|
return <KanbanBoardsNs.KanbanBoards store={store} />;
|
||||||
|
});
|
||||||
30
frontend/src/dashboard/widgets/widget-factory.tsx
Normal file
30
frontend/src/dashboard/widgets/widget-factory.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import React from 'react';
|
||||||
|
import * as DashboardStoreNs from '../dashboard-store';
|
||||||
|
import { Instance } from 'mobx-state-tree';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
import * as KanbanWidgetNs from './kanban';
|
||||||
|
import { DebugInfo } from '../../misc-components/debug-info';
|
||||||
|
import * as IssuesListNs from './issues-list';
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
store: Instance<typeof DashboardStoreNs.Widget>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WidgetFactory = observer((props: Props): JSX.Element => {
|
||||||
|
const type = props.store.type;
|
||||||
|
|
||||||
|
if (type.startsWith('kanban_by_')) {
|
||||||
|
return <KanbanWidgetNs.Kanban store={props.store} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type.startsWith('issues_list_')) {
|
||||||
|
return <IssuesListNs.IssuesList store={props.store} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div>Unknown widget</div>
|
||||||
|
<DebugInfo value={JSON.stringify(props.store, null, ' ')} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
@ -1,17 +1,26 @@
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { IBoardStore } from './store';
|
import * as KanbanBoardStoreNs from '../kanban-board/store';
|
||||||
import Css from './issues-list-board.module.css';
|
import Css from './issues-list-board.module.css';
|
||||||
import * as IssuesListCardNs from './issues-list-card';
|
import * as IssuesListCardNs from './issues-list-card';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
store: IBoardStore;
|
store: KanbanBoardStoreNs.IBoardStore;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssuesListBoard = observer((props: Props): JSX.Element => {
|
export const IssuesListBoard = observer((props: Props): JSX.Element => {
|
||||||
const list: JSX.Element[] = props.store.data.map((issue) => {
|
const list: JSX.Element[] = [];
|
||||||
return <IssuesListCardNs.IssuesListCard store={issue} key={issue.id} />;
|
const data = props.store.data;
|
||||||
});
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
const column = data[i];
|
||||||
|
const issues: any[] = column.issues;
|
||||||
|
for (let j = 0; j < issues.length; j++) {
|
||||||
|
const issue = issues[j];
|
||||||
|
list.push(
|
||||||
|
<IssuesListCardNs.IssuesListCard store={issue} key={issue.id} />,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
let title: JSX.Element;
|
let title: JSX.Element;
|
||||||
if (props.store.metainfo.url) {
|
if (props.store.metainfo.url) {
|
||||||
title = <a href={props.store.metainfo.url}>{props.store.metainfo.title}</a>;
|
title = <a href={props.store.metainfo.url}>{props.store.metainfo.title}</a>;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import * as IssuesListStoreNs from './store';
|
import * as KanbanBoardStoreNs from '../kanban-board/store';
|
||||||
import * as IssuesListBoardsNs from './issues-list-boards';
|
import * as IssuesListBoardsNs from './issues-list-boards';
|
||||||
|
|
||||||
export const IssuesListBoardPage = (): JSX.Element => {
|
export const IssuesListBoardPage = (): JSX.Element => {
|
||||||
|
|
@ -15,12 +15,12 @@ export const IssuesListBoardPage = (): JSX.Element => {
|
||||||
});
|
});
|
||||||
// DEBUG: end
|
// DEBUG: end
|
||||||
|
|
||||||
const store = IssuesListStoreNs.PageStore.create({
|
const store = KanbanBoardStoreNs.PageStore.create({
|
||||||
loaded: false,
|
loaded: false,
|
||||||
type: type,
|
type: type,
|
||||||
name: name,
|
name: name,
|
||||||
});
|
});
|
||||||
IssuesListStoreNs.PageStoreLoadData(store);
|
KanbanBoardStoreNs.PageStoreLoadData(store);
|
||||||
|
|
||||||
return <IssuesListBoardsNs.IssuesListBoards store={store} />;
|
return <IssuesListBoardsNs.IssuesListBoards store={store} />;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import * as IssuesListBoardStore from './store';
|
import * as KanbanBoardStoreNs from '../kanban-board/store';
|
||||||
import * as IssuesListBoardNs from './issues-list-board';
|
import * as IssuesListBoardNs from './issues-list-board';
|
||||||
import * as TopRightMenuNs from '../misc-components/top-right-menu';
|
import * as TopRightMenuNs from '../misc-components/top-right-menu';
|
||||||
import { SetIssuesReadingTimestamp } from '../utils/unreaded-provider';
|
import { SetIssuesReadingTimestamp } from '../utils/unreaded-provider';
|
||||||
import * as ServiceActionsButtons from '../utils/service-actions-buttons';
|
import * as ServiceActionsButtons from '../utils/service-actions-buttons';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
store: IssuesListBoardStore.IPageStore;
|
store: KanbanBoardStoreNs.IPageStore;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssuesListBoards = observer((props: Props): JSX.Element => {
|
export const IssuesListBoards = observer((props: Props): JSX.Element => {
|
||||||
|
|
@ -28,7 +28,7 @@ export const IssuesListBoards = observer((props: Props): JSX.Element => {
|
||||||
const onAllReadItemClick = (e: React.MouseEvent) => {
|
const onAllReadItemClick = (e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
SetIssuesReadingTimestamp(props.store.issueIds);
|
SetIssuesReadingTimestamp(props.store.issueIds);
|
||||||
IssuesListBoardStore.PageStoreLoadData(props.store);
|
KanbanBoardStoreNs.PageStoreLoadData(props.store);
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ export const IssuesListCard = observer((props: Props): JSX.Element => {
|
||||||
<span className={Css.timeBox}>{props.store.status.name}</span>
|
<span className={Css.timeBox}>{props.store.status.name}</span>
|
||||||
<span> </span>
|
<span> </span>
|
||||||
<span className={Css.priorityBox} style={priorityStyle}>
|
<span className={Css.priorityBox} style={priorityStyle}>
|
||||||
{props.store.priority.name}
|
{props.store?.priority?.name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -30,9 +30,9 @@ export type IBoardStore = Instance<typeof BoardStore>;
|
||||||
|
|
||||||
export const PageStore = types
|
export const PageStore = types
|
||||||
.model({
|
.model({
|
||||||
loaded: types.boolean,
|
loaded: false,
|
||||||
type: types.string,
|
type: '',
|
||||||
name: types.string,
|
name: '',
|
||||||
data: types.maybeNull(types.array(BoardStore)),
|
data: types.maybeNull(types.array(BoardStore)),
|
||||||
})
|
})
|
||||||
.actions((self) => {
|
.actions((self) => {
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ export const PageStore = types
|
||||||
.actions((self) => {
|
.actions((self) => {
|
||||||
return {
|
return {
|
||||||
setData: (data: any) => {
|
setData: (data: any) => {
|
||||||
|
console.debug('Kanban page store new data -', data); // DEBUG
|
||||||
self.data = data;
|
self.data = data;
|
||||||
self.loaded = true;
|
self.loaded = true;
|
||||||
},
|
},
|
||||||
|
|
|
||||||
5
frontend/src/misc-components/debug-info.module.css
Normal file
5
frontend/src/misc-components/debug-info.module.css
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
.debugInfo {
|
||||||
|
margin: 3px;
|
||||||
|
padding: 3px;
|
||||||
|
border: 1px solid black;
|
||||||
|
}
|
||||||
19
frontend/src/misc-components/debug-info.tsx
Normal file
19
frontend/src/misc-components/debug-info.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import React from 'react';
|
||||||
|
import Css from './debug-info.module.css';
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
value?: string;
|
||||||
|
children?: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DebugInfo = (props: Props): JSX.Element => {
|
||||||
|
let output: any;
|
||||||
|
if (props.value) {
|
||||||
|
output = <pre>{props.value}</pre>;
|
||||||
|
} else if (props.children) {
|
||||||
|
output = props.children;
|
||||||
|
} else {
|
||||||
|
output = <pre>(none)</pre>;
|
||||||
|
}
|
||||||
|
return <div className={Css.debugInfo}>{output}</div>;
|
||||||
|
};
|
||||||
|
|
@ -13,6 +13,7 @@ const formatStringToCamelCase = (str: string): string => {
|
||||||
export const getStyleObjectFromString = (
|
export const getStyleObjectFromString = (
|
||||||
str: string,
|
str: string,
|
||||||
): Record<string, string> => {
|
): Record<string, string> => {
|
||||||
|
if (!str) return {};
|
||||||
const style = {} as Record<string, string>;
|
const style = {} as Record<string, string>;
|
||||||
str.split(';').forEach((el) => {
|
str.split(';').forEach((el) => {
|
||||||
const [property, value] = el.split(':');
|
const [property, value] = el.split(':');
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,17 @@ export class DashboardController {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get(':id/load-data/:widgetId')
|
||||||
|
async loadDataForWidget(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Param('widgetId') widgetId: string,
|
||||||
|
): Promise<any> {
|
||||||
|
return await getOrAppErrorOrThrow(
|
||||||
|
() => this.dashboardsDataService.loadDataForWidget(id, widgetId),
|
||||||
|
BadRequestErrorHandler,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Put(':id')
|
@Put(':id')
|
||||||
async save(@Param('id') id: string, @Body() data: any): Promise<void> {
|
async save(@Param('id') id: string, @Body() data: any): Promise<void> {
|
||||||
const res = await getOrAppErrorOrThrow(
|
const res = await getOrAppErrorOrThrow(
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { AppError, Result, createAppError, fail } from '../utils/result';
|
||||||
import { WidgetsCollectionService } from './widgets-collection.service';
|
import { WidgetsCollectionService } from './widgets-collection.service';
|
||||||
|
|
||||||
export type WidgetWithData = {
|
export type WidgetWithData = {
|
||||||
widget: DashboardModel.Widget;
|
widgetId: string;
|
||||||
data: any;
|
data: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -22,8 +22,12 @@ export class DashboardsDataService {
|
||||||
const cfg = await this.dashboardsService.load(id);
|
const cfg = await this.dashboardsService.load(id);
|
||||||
const results: WidgetWithData[] = [];
|
const results: WidgetWithData[] = [];
|
||||||
let isSuccess = false;
|
let isSuccess = false;
|
||||||
|
if (!cfg?.widgets || cfg?.widgets?.length <= 0) {
|
||||||
|
return results;
|
||||||
|
}
|
||||||
for (let i = 0; i < cfg.widgets.length; i++) {
|
for (let i = 0; i < cfg.widgets.length; i++) {
|
||||||
const widget = cfg.widgets[i];
|
const widget = cfg.widgets[i];
|
||||||
|
if (widget.collapsed) continue;
|
||||||
const loadRes = await this.loadWidgetData(
|
const loadRes = await this.loadWidgetData(
|
||||||
widget.type,
|
widget.type,
|
||||||
widget.widgetParams,
|
widget.widgetParams,
|
||||||
|
|
@ -33,13 +37,29 @@ export class DashboardsDataService {
|
||||||
if (loadRes.result) {
|
if (loadRes.result) {
|
||||||
isSuccess = true;
|
isSuccess = true;
|
||||||
loadRes.result.widgetId = widget.id;
|
loadRes.result.widgetId = widget.id;
|
||||||
results.push(loadRes.result);
|
results.push({ data: loadRes.result, widgetId: widget.id });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!isSuccess) throw createAppError('CANNOT_LOAD_DATA');
|
if (!isSuccess) throw createAppError('CANNOT_LOAD_DATA');
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async loadDataForWidget(id: string, widgetId: string): Promise<any> {
|
||||||
|
const cfg = await this.dashboardsService.load(id);
|
||||||
|
const widget = cfg.widgets.find((widget) => {
|
||||||
|
return widget.id == widgetId;
|
||||||
|
});
|
||||||
|
if (!widget) throw createAppError('WIDGET_NOT_FOUND');
|
||||||
|
const loadRes = await this.loadWidgetData(
|
||||||
|
widget.type,
|
||||||
|
widget.widgetParams,
|
||||||
|
widget.dataLoaderParams,
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
if (loadRes.result) return loadRes.result;
|
||||||
|
throw createAppError('CANNOT_LOAD_DATA');
|
||||||
|
}
|
||||||
|
|
||||||
async loadWidgetData(
|
async loadWidgetData(
|
||||||
type: string,
|
type: string,
|
||||||
widgetParams: DashboardModel.WidgetParams,
|
widgetParams: DashboardModel.WidgetParams,
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ export class DashboardsService {
|
||||||
async load(id: string): Promise<DashboardModel.Data> {
|
async load(id: string): Promise<DashboardModel.Data> {
|
||||||
this.logger.debug(`Load dashboard id - ${id}`);
|
this.logger.debug(`Load dashboard id - ${id}`);
|
||||||
const rawData = await this.loadRawData(id);
|
const rawData = await this.loadRawData(id);
|
||||||
return rawData.data;
|
return rawData?.data || { widgets: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
async isExists(id: string): Promise<boolean> {
|
async isExists(id: string): Promise<boolean> {
|
||||||
|
|
|
||||||
|
|
@ -16,13 +16,7 @@ export class InteractiveWidget
|
||||||
dashboardParams: any,
|
dashboardParams: any,
|
||||||
): Promise<Result<any, AppError>> {
|
): Promise<Result<any, AppError>> {
|
||||||
const data = await this.dataLoader.load(dataLoaderParams, dashboardParams);
|
const data = await this.dataLoader.load(dataLoaderParams, dashboardParams);
|
||||||
return data.error
|
return data.error ? fail(data.error) : success(data.result);
|
||||||
? fail(data.error)
|
|
||||||
: success({
|
|
||||||
data: data.result,
|
|
||||||
widgetParams: widgetParams,
|
|
||||||
dashboardParams: dashboardParams,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
export type Data = {
|
export type Data = {
|
||||||
widgets: Widget[];
|
widgets: Widget[];
|
||||||
title: string;
|
title?: string;
|
||||||
} | null;
|
} | null;
|
||||||
|
|
||||||
export type Dashboard = {
|
export type Dashboard = {
|
||||||
|
|
@ -11,9 +11,7 @@ export type Dashboard = {
|
||||||
/**
|
/**
|
||||||
* Параметры для отрисовки данных
|
* Параметры для отрисовки данных
|
||||||
*/
|
*/
|
||||||
export type WidgetParams = {
|
export type WidgetParams = Record<string, any> | null;
|
||||||
collapsed?: boolean;
|
|
||||||
} & Record<string, any>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Параметры для загрузки данных
|
* Параметры для загрузки данных
|
||||||
|
|
@ -24,6 +22,7 @@ export type Widget = {
|
||||||
type: string;
|
type: string;
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
collapsed?: boolean;
|
||||||
widgetParams?: WidgetParams;
|
widgetParams?: WidgetParams;
|
||||||
dataLoaderParams?: DataLoaderParams;
|
dataLoaderParams?: DataLoaderParams;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue