Подключен первый виджет kanban_by_tree к дашбордам
This commit is contained in:
parent
05b6364ae5
commit
ae14189cd3
10 changed files with 327 additions and 13 deletions
|
|
@ -11,7 +11,9 @@ export const DashboardPage = (): JSX.Element => {
|
|||
id: id,
|
||||
loaded: false,
|
||||
});
|
||||
Store.DashboardLoadData(store);
|
||||
Store.DashboardLoadData(store).then(() => {
|
||||
return Store.LoadDataForWidgets(store);
|
||||
});
|
||||
|
||||
return <Dashboard store={store} />;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,14 +9,58 @@ 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),
|
||||
widgetParams: types.maybe(WidgetParams),
|
||||
dataLoaderParams: types.maybe(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>()),
|
||||
})
|
||||
.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;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export type IWidget = Instance<typeof Widget>;
|
||||
|
||||
export function createWidgetStore(
|
||||
id: string,
|
||||
type: string,
|
||||
title: string,
|
||||
collapsed = false,
|
||||
widgetParams?: _WidgetParams,
|
||||
dataLoaderParams?: _DataLoaderParams,
|
||||
): IWidget {
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
console.debug('init new Widget store, params:', arguments);
|
||||
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),
|
||||
|
|
@ -32,17 +76,68 @@ export const Dashboard = types
|
|||
.actions((self) => {
|
||||
return {
|
||||
setData: (data: any) => {
|
||||
console.debug('Dashboard store new data:', data); // DEBUG
|
||||
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]?.widget?.id;
|
||||
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> {
|
||||
console.debug('DashboardLoadData store:', store); // DEBUG
|
||||
const url = `${process.env.REACT_APP_BACKEND}api/dashboard/${store.id}`;
|
||||
const resp = await axios.get(url);
|
||||
console.debug('DashboardLoadData resp:', resp); // DEBUG
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,36 @@
|
|||
import { observer } from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
import * as Store from './dashboard-store';
|
||||
import * as DashboardStoreNs from './dashboard-store';
|
||||
import * as TopRightMenuNs from '../misc-components/top-right-menu';
|
||||
import * as WidgetNs from './widget';
|
||||
import { DebugInfo } from '../misc-components/debug-info';
|
||||
|
||||
export type Props = { store: Store.IDashboard };
|
||||
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>;
|
||||
}
|
||||
|
||||
return <pre>{JSON.stringify(props.store, null, ' ')}</pre>;
|
||||
const debugInfo = JSON.stringify(props.store, null, ' ');
|
||||
|
||||
const topRightMenuStore = TopRightMenuNs.Store.create({ visible: false });
|
||||
|
||||
const widgets = props.store.data?.widgets.map((widget) => {
|
||||
return <WidgetNs.Widget key={widget.id} store={widget}></WidgetNs.Widget>;
|
||||
});
|
||||
|
||||
const res = (
|
||||
<div>
|
||||
<TopRightMenuNs.TopRightMenu store={topRightMenuStore}>
|
||||
<a href="/dashboards">Назад</a>
|
||||
<span>Дашборд - {props.store.data?.title || props.store.id}</span>
|
||||
</TopRightMenuNs.TopRightMenu>
|
||||
{widgets}
|
||||
<DebugInfo value={debugInfo} />
|
||||
</div>
|
||||
);
|
||||
|
||||
return res;
|
||||
});
|
||||
|
|
|
|||
96
frontend/src/dashboard/widget.tsx
Normal file
96
frontend/src/dashboard/widget.tsx
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
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"
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
|
||||
export const Widget = observer((props: Props): JSX.Element => {
|
||||
const display = props.store.visible ? 'block' : 'none';
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<button onClick={() => props.store.toggle()}>Show/hide</button>
|
||||
<span>Title - {props.store.title}</span>
|
||||
</div>
|
||||
<div style={{ display: display }}>
|
||||
<WidgetFactoryNs.WidgetFactory store={props.store} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
17
frontend/src/dashboard/widgets/kanban-by-tree.tsx
Normal file
17
frontend/src/dashboard/widgets/kanban-by-tree.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 KanbanByTree = observer((props: Props): JSX.Element => {
|
||||
const store = KanbanBoardsStoreNs.PageStore.create({ loaded: false });
|
||||
onSnapshot(props.store, (state) => {
|
||||
store.setData(state.data.data);
|
||||
});
|
||||
return <KanbanBoardsNs.KanbanBoards store={store} />;
|
||||
});
|
||||
26
frontend/src/dashboard/widgets/widget-factory.tsx
Normal file
26
frontend/src/dashboard/widgets/widget-factory.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import React from 'react';
|
||||
import * as DashboardStoreNs from '../dashboard-store';
|
||||
import { Instance } from 'mobx-state-tree';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import * as KanbanByTreeWidgetNs from './kanban-by-tree';
|
||||
import { DebugInfo } from '../../misc-components/debug-info';
|
||||
|
||||
export type Props = {
|
||||
store: Instance<typeof DashboardStoreNs.Widget>;
|
||||
};
|
||||
|
||||
export const WidgetFactory = observer((props: Props): JSX.Element => {
|
||||
const type = props.store.type;
|
||||
|
||||
switch (type) {
|
||||
case 'kanban_by_tree':
|
||||
return <KanbanByTreeWidgetNs.KanbanByTree store={props.store} />;
|
||||
default:
|
||||
return (
|
||||
<div>
|
||||
<div>Unknown widget</div>
|
||||
<DebugInfo value={JSON.stringify(props.store, null, ' ')} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
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>;
|
||||
};
|
||||
|
|
@ -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')
|
||||
async save(@Param('id') id: string, @Body() data: any): Promise<void> {
|
||||
const res = await getOrAppErrorOrThrow(
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export class DashboardsDataService {
|
|||
let isSuccess = false;
|
||||
for (let i = 0; i < cfg.widgets.length; i++) {
|
||||
const widget = cfg.widgets[i];
|
||||
if (widget.collapsed) continue;
|
||||
const loadRes = await this.loadWidgetData(
|
||||
widget.type,
|
||||
widget.widgetParams,
|
||||
|
|
@ -33,13 +34,32 @@ export class DashboardsDataService {
|
|||
if (loadRes.result) {
|
||||
isSuccess = true;
|
||||
loadRes.result.widgetId = widget.id;
|
||||
results.push(loadRes.result);
|
||||
results.push({ data: loadRes.result, widget: widget });
|
||||
}
|
||||
}
|
||||
if (!isSuccess) throw createAppError('CANNOT_LOAD_DATA');
|
||||
return results;
|
||||
}
|
||||
|
||||
async loadDataForWidget(
|
||||
id: string,
|
||||
widgetId: string,
|
||||
): Promise<WidgetWithData> {
|
||||
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 { widget: widget, data: loadRes.error };
|
||||
throw createAppError('CANNOT_LOAD_DATA');
|
||||
}
|
||||
|
||||
async loadWidgetData(
|
||||
type: string,
|
||||
widgetParams: DashboardModel.WidgetParams,
|
||||
|
|
|
|||
Loading…
Reference in a new issue