Compare commits

..

10 commits

28 changed files with 782 additions and 32 deletions

View file

@ -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",

View file

@ -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",

View file

@ -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} />;
}; };

View 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);
}
}
}

View 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;
});

View 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>
);
});

View file

@ -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} />
</>
);
}; };

View 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);
}

View 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%;
}

View 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>
);
});

View 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>
);
});

View 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} />;
});

View 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} />;
});

View 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>
);
});

View file

@ -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>;

View file

@ -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} />;
}; };

View file

@ -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 (
<> <>

View file

@ -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>

View file

@ -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) => {

View file

@ -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;
}, },

View file

@ -0,0 +1,5 @@
.debugInfo {
margin: 3px;
padding: 3px;
border: 1px solid black;
}

View 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>;
};

View file

@ -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(':');

View file

@ -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(

View file

@ -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,

View file

@ -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> {

View file

@ -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,
});
} }
} }

View file

@ -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;
}; };