From bdf2bbce3c3d0469de209b5d3f616f5665f3d489 Mon Sep 17 00:00:00 2001 From: balaji-jr Date: Tue, 18 Jun 2024 23:35:04 +0530 Subject: [PATCH 01/34] readme - updated the demo app url --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b48d8ef7..1aedc2b5 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ For complete Parseable API documentation, refer to [Parseable API workspace on P - + From 3949493b4ea4f50b7127eda59ab7042a521443b0 Mon Sep 17 00:00:00 2001 From: balaji-jr Date: Sun, 30 Jun 2024 18:33:24 +0530 Subject: [PATCH 02/34] basic nav setup for dashboards --- src/components/Navbar/index.tsx | 11 +++++++++-- src/constants/routes.ts | 4 +++- src/pages/Dashboards/index.tsx | 12 ++++++++++++ src/routes/elements.tsx | 11 ++++++++++- src/routes/index.tsx | 8 +++++--- 5 files changed, 39 insertions(+), 7 deletions(-) create mode 100644 src/pages/Dashboards/index.tsx diff --git a/src/components/Navbar/index.tsx b/src/components/Navbar/index.tsx index b9c65c39..ed237fa6 100644 --- a/src/components/Navbar/index.tsx +++ b/src/components/Navbar/index.tsx @@ -7,12 +7,13 @@ import { IconServerCog, IconHomeStats, IconListDetails, + IconChartBar } from '@tabler/icons-react'; import { FC, useCallback, useEffect } from 'react'; import { useLocation, useParams } from 'react-router-dom'; import { useNavigate } from 'react-router-dom'; import { useDisclosure } from '@mantine/hooks'; -import { HOME_ROUTE, CLUSTER_ROUTE, USERS_MANAGEMENT_ROUTE, STREAM_ROUTE } from '@/constants/routes'; +import { HOME_ROUTE, CLUSTER_ROUTE, USERS_MANAGEMENT_ROUTE, STREAM_ROUTE, DASHBOARDS_ROUTE } from '@/constants/routes'; import InfoModal from './infoModal'; import { getStreamsSepcificAccess, getUserSepcificStreams } from './rolesHandler'; import Cookies from 'js-cookie'; @@ -35,6 +36,12 @@ const navItems = [ path: '/', route: HOME_ROUTE, }, + { + icon: IconChartBar, + label: 'Dashboards', + path: '/dashboards', + route: DASHBOARDS_ROUTE, + }, { icon: IconListDetails, label: 'Stream', @@ -211,4 +218,4 @@ const Navbar: FC = () => { ); }; -export default Navbar; +export default Navbar; \ No newline at end of file diff --git a/src/constants/routes.ts b/src/constants/routes.ts index 105dd952..cbd2748b 100644 --- a/src/constants/routes.ts +++ b/src/constants/routes.ts @@ -10,6 +10,7 @@ export const USERS_MANAGEMENT_ROUTE = '/users'; export const OIDC_NOT_CONFIGURED_ROUTE = '/oidc-not-configured'; export const CLUSTER_ROUTE = '/cluster'; export const STREAM_ROUTE = '/:streamName/:view?' +export const DASHBOARDS_ROUTE = '/dashboards' export const STREAM_VIEWS = ['explore', 'manage', 'live-tail'] @@ -25,4 +26,5 @@ export const PATHS = { oidcNotConfigured: '/oidc-not-configured', cluster: '/cluster', manage: '/:streamName/:view?', -} as { [key: string]: string }; + dashboards: '/dashboards' +} as { [key: string]: string }; \ No newline at end of file diff --git a/src/pages/Dashboards/index.tsx b/src/pages/Dashboards/index.tsx new file mode 100644 index 00000000..c3c1188d --- /dev/null +++ b/src/pages/Dashboards/index.tsx @@ -0,0 +1,12 @@ +import { Box } from '@mantine/core'; + +const Dashboards = () => { + return ( + + {/*
+
*/} +
+ ); +}; + +export default Dashboards; \ No newline at end of file diff --git a/src/routes/elements.tsx b/src/routes/elements.tsx index 43c13fce..4b0d2164 100644 --- a/src/routes/elements.tsx +++ b/src/routes/elements.tsx @@ -8,6 +8,7 @@ import { LogsProvider } from '@/pages/Stream/providers/LogsProvider'; import { FilterProvider } from '@/pages/Stream/providers/FilterProvider'; import { StreamProvider } from '@/pages/Stream/providers/StreamProvider'; import { ClusterProvider } from '@/pages/Systems/providers/ClusterProvider'; +import Dashboards from '@/pages/Dashboards'; export const HomeElement: FC = () => { return ( @@ -17,6 +18,14 @@ export const HomeElement: FC = () => { ); }; +export const DashboardsElement: FC = () => { + return ( + + + + ); +}; + const Login = lazy(() => import('@/pages/Login')); export const LoginElement: FC = () => { @@ -71,4 +80,4 @@ export const SystemsElement: FC = () => { ); -}; +}; \ No newline at end of file diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 19c672eb..ad5611db 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -5,14 +5,15 @@ import { OIDC_NOT_CONFIGURED_ROUTE, USERS_MANAGEMENT_ROUTE, CLUSTER_ROUTE, - STREAM_ROUTE + STREAM_ROUTE, + DASHBOARDS_ROUTE } from '@/constants/routes'; import FullPageLayout from '@/layouts/FullPageLayout'; import NotFound from '@/pages/Errors/NotFound'; import type { FC } from 'react'; import { Route, Routes } from 'react-router-dom'; import PrivateRoute from './PrivateRoute'; -import { HomeElement, LoginElement, StreamElement, MainLayoutElement, SystemsElement, UsersElement } from './elements'; +import { HomeElement, LoginElement, StreamElement, MainLayoutElement, SystemsElement, UsersElement, DashboardsElement } from './elements'; import AccessSpecificRoute from './AccessSpecificRoute'; import OIDCNotConFigured from '@/pages/Errors/OIDC'; @@ -23,6 +24,7 @@ const AppRouter: FC = () => { }> }> } /> + } /> }> } /> @@ -42,4 +44,4 @@ const AppRouter: FC = () => { ); }; -export default AppRouter; +export default AppRouter; \ No newline at end of file From 77e4a575e3b1de4acc1bf19b04e78db46352415f Mon Sep 17 00:00:00 2001 From: balaji-jr Date: Wed, 3 Jul 2024 23:36:04 +0530 Subject: [PATCH 03/34] Implemented basic draggable example --- package.json | 3 + pnpm-lock.yaml | 233 +++++++++++++++++++++++---------- src/main.tsx | 18 +-- src/pages/Dashboards/index.tsx | 57 +++++++- 4 files changed, 228 insertions(+), 83 deletions(-) diff --git a/package.json b/package.json index c0f3e199..bf766857 100644 --- a/package.json +++ b/package.json @@ -43,8 +43,10 @@ "react-beautiful-dnd": "^13.1.1", "react-dom": "^18.2.0", "react-error-boundary": "^4.0.10", + "react-grid-layout": "^1.4.4", "react-query": "^3.39.3", "react-querybuilder": "^6.5.5", + "react-resizable": "^3.0.5", "react-resizable-panels": "^0.0.53", "react-router-dom": "^6.14.0", "react-window": "^1.8.9" @@ -56,6 +58,7 @@ "@types/react": "^18.2.14", "@types/react-beautiful-dnd": "^13.1.4", "@types/react-dom": "^18.2.6", + "@types/react-grid-layout": "^1.3.5", "@types/react-window": "^1.8.5", "@typescript-eslint/eslint-plugin": "^5.60.1", "@typescript-eslint/parser": "^5.60.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2b2251ce..31d3d6e4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,19 +16,19 @@ importers: version: 11.11.1(@types/react@18.2.14)(react@18.2.0) '@mantine/carousel': specifier: ^7.8.1 - version: 7.8.1(@mantine/core@7.8.1)(@mantine/hooks@7.8.1)(embla-carousel-react@7.1.0)(react-dom@18.2.0)(react@18.2.0) + version: 7.8.1(@mantine/core@7.8.1(@mantine/hooks@7.8.1(react@18.2.0))(@types/react@18.2.14)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@mantine/hooks@7.8.1(react@18.2.0))(embla-carousel-react@7.1.0(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@mantine/charts': specifier: ^7.8.1 - version: 7.8.1(@mantine/core@7.8.1)(@mantine/hooks@7.8.1)(react-dom@18.2.0)(react@18.2.0)(recharts@2.12.7) + version: 7.8.1(@mantine/core@7.8.1(@mantine/hooks@7.8.1(react@18.2.0))(@types/react@18.2.14)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@mantine/hooks@7.8.1(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(recharts@2.12.7(react-dom@18.2.0(react@18.2.0))(react@18.2.0)) '@mantine/code-highlight': specifier: ^7.8.1 - version: 7.8.1(@mantine/core@7.8.1)(@mantine/hooks@7.8.1)(react-dom@18.2.0)(react@18.2.0) + version: 7.8.1(@mantine/core@7.8.1(@mantine/hooks@7.8.1(react@18.2.0))(@types/react@18.2.14)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@mantine/hooks@7.8.1(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@mantine/core': specifier: ^7.8.1 - version: 7.8.1(@mantine/hooks@7.8.1)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0) + version: 7.8.1(@mantine/hooks@7.8.1(react@18.2.0))(@types/react@18.2.14)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@mantine/dates': specifier: ^7.8.1 - version: 7.8.1(@mantine/core@7.8.1)(@mantine/hooks@7.8.1)(dayjs@1.11.10)(react-dom@18.2.0)(react@18.2.0) + version: 7.8.1(@mantine/core@7.8.1(@mantine/hooks@7.8.1(react@18.2.0))(@types/react@18.2.14)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@mantine/hooks@7.8.1(react@18.2.0))(dayjs@1.11.10)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@mantine/form': specifier: ^7.8.1 version: 7.8.1(react@18.2.0) @@ -37,10 +37,10 @@ importers: version: 7.8.1(react@18.2.0) '@mantine/notifications': specifier: ^7.8.1 - version: 7.8.1(@mantine/core@7.8.1)(@mantine/hooks@7.8.1)(react-dom@18.2.0)(react@18.2.0) + version: 7.8.1(@mantine/core@7.8.1(@mantine/hooks@7.8.1(react@18.2.0))(@types/react@18.2.14)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@mantine/hooks@7.8.1(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@monaco-editor/react': specifier: ^4.5.1 - version: 4.5.1(monaco-editor@0.48.0)(react-dom@18.2.0)(react@18.2.0) + version: 4.5.1(monaco-editor@0.48.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@tabler/icons-react': specifier: ^3.3.0 version: 3.3.0(react@18.2.0) @@ -91,28 +91,34 @@ importers: version: 18.2.0 react-beautiful-dnd: specifier: ^13.1.1 - version: 13.1.1(react-dom@18.2.0)(react@18.2.0) + version: 13.1.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react-dom: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) react-error-boundary: specifier: ^4.0.10 version: 4.0.10(react@18.2.0) + react-grid-layout: + specifier: ^1.4.4 + version: 1.4.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react-query: specifier: ^3.39.3 - version: 3.39.3(react-dom@18.2.0)(react@18.2.0) + version: 3.39.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react-querybuilder: specifier: ^6.5.5 version: 6.5.5(react@18.2.0) + react-resizable: + specifier: ^3.0.5 + version: 3.0.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react-resizable-panels: specifier: ^0.0.53 - version: 0.0.53(react-dom@18.2.0)(react@18.2.0) + version: 0.0.53(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react-router-dom: specifier: ^6.14.0 - version: 6.14.0(react-dom@18.2.0)(react@18.2.0) + version: 6.14.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react-window: specifier: ^1.8.9 - version: 1.8.9(react-dom@18.2.0)(react@18.2.0) + version: 1.8.9(react-dom@18.2.0(react@18.2.0))(react@18.2.0) devDependencies: '@types/lodash': specifier: ^4.17.0 @@ -132,18 +138,21 @@ importers: '@types/react-dom': specifier: ^18.2.6 version: 18.2.6 + '@types/react-grid-layout': + specifier: ^1.3.5 + version: 1.3.5 '@types/react-window': specifier: ^1.8.5 version: 1.8.5 '@typescript-eslint/eslint-plugin': specifier: ^5.60.1 - version: 5.60.1(@typescript-eslint/parser@5.60.1)(eslint@8.43.0)(typescript@5.1.6) + version: 5.60.1(@typescript-eslint/parser@5.60.1(eslint@8.43.0)(typescript@5.1.6))(eslint@8.43.0)(typescript@5.1.6) '@typescript-eslint/parser': specifier: ^5.60.1 version: 5.60.1(eslint@8.43.0)(typescript@5.1.6) '@vitejs/plugin-react-swc': specifier: ^3.3.2 - version: 3.3.2(vite@4.3.9) + version: 3.3.2(vite@4.3.9(@types/node@20.3.2)(sugarss@4.0.1(postcss@8.4.33))) eslint: specifier: ^8.43.0 version: 8.43.0 @@ -152,10 +161,10 @@ importers: version: 8.8.0(eslint@8.43.0) eslint-plugin-import: specifier: ^2.27.5 - version: 2.27.5(@typescript-eslint/parser@5.60.1)(eslint@8.43.0) + version: 2.27.5(@typescript-eslint/parser@5.60.1(eslint@8.43.0)(typescript@5.1.6))(eslint@8.43.0) eslint-plugin-prettier: specifier: ^4.2.1 - version: 4.2.1(eslint-config-prettier@8.8.0)(eslint@8.43.0)(prettier@2.8.8) + version: 4.2.1(eslint-config-prettier@8.8.0(eslint@8.43.0))(eslint@8.43.0)(prettier@2.8.8) eslint-plugin-react: specifier: ^7.32.2 version: 7.32.2(eslint@8.43.0) @@ -185,7 +194,7 @@ importers: version: 5.1.6 vite: specifier: ^4.3.9 - version: 4.3.9(@types/node@20.3.2) + version: 4.3.9(@types/node@20.3.2)(sugarss@4.0.1(postcss@8.4.33)) packages: @@ -722,6 +731,9 @@ packages: '@types/react-dom@18.2.6': resolution: {integrity: sha512-2et4PDvg6PVCyS7fuTc4gPoksV58bW0RwSxWKcPRcHZf0PRUGq03TKcD/rUHe3azfV6/5/biUBJw+HhCQjaP0A==} + '@types/react-grid-layout@1.3.5': + resolution: {integrity: sha512-WH/po1gcEcoR6y857yAnPGug+ZhkF4PaTUxgAbwfeSH/QOgVSakKHBXoPGad/sEznmkiaK3pqHk+etdWisoeBQ==} + '@types/react-redux@7.1.25': resolution: {integrity: sha512-bAGh4e+w5D8dajd6InASVIyCo4pZLJ66oLb80F9OBLO1gKESbZcRCJpTT6uLXX+HAB57zw1WTdwJdAsewuTweg==} @@ -927,6 +939,10 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + clsx@1.2.1: + resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} + engines: {node: '>=6'} + clsx@2.0.0: resolution: {integrity: sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==} engines: {node: '>=6'} @@ -1255,6 +1271,9 @@ packages: fast-diff@1.3.0: resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + fast-equals@4.0.3: + resolution: {integrity: sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg==} + fast-equals@5.0.1: resolution: {integrity: sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==} engines: {node: '>=6.0.0'} @@ -1902,11 +1921,23 @@ packages: peerDependencies: react: ^18.2.0 + react-draggable@4.4.6: + resolution: {integrity: sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==} + peerDependencies: + react: '>= 16.3.0' + react-dom: '>= 16.3.0' + react-error-boundary@4.0.10: resolution: {integrity: sha512-pvVKdi77j2OoPHo+p3rorgE43OjDWiqFkaqkJz8sJKK6uf/u8xtzuaVfj5qJ2JnDLIgF1De3zY5AJDijp+LVPA==} peerDependencies: react: '>=16.13.1' + react-grid-layout@1.4.4: + resolution: {integrity: sha512-7+Lg8E8O8HfOH5FrY80GCIR1SHTn2QnAYKh27/5spoz+OHhMmEhU/14gIkRzJOtympDPaXcVRX/nT1FjmeOUmQ==} + peerDependencies: + react: '>= 16.3.0' + react-dom: '>= 16.3.0' + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -1974,6 +2005,11 @@ packages: react: ^16.14.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0 + react-resizable@3.0.5: + resolution: {integrity: sha512-vKpeHhI5OZvYn82kXOs1bC8aOXktGU5AmKAgaZS4F5JPburCtbmDPqE7Pzp+1kN4+Wb81LlF33VpGwWwtXem+w==} + peerDependencies: + react: '>= 16.3' + react-router-dom@6.14.0: resolution: {integrity: sha512-YEwlApKwzMMMbGbhh+Q7MsloTldcwMgHxUY/1g0uA62+B1hZo2jsybCWIDCL8zvIDB1FA0pBKY9chHbZHt+2dQ==} engines: {node: '>=14'} @@ -2053,6 +2089,9 @@ packages: resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} engines: {node: '>=0.10'} + resize-observer-polyfill@1.5.1: + resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -2467,9 +2506,10 @@ snapshots: '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0) '@emotion/utils': 1.2.1 '@emotion/weak-memoize': 0.3.1 - '@types/react': 18.2.14 hoist-non-react-statics: 3.3.2 react: 18.2.0 + optionalDependencies: + '@types/react': 18.2.14 '@emotion/serialize@1.1.2': dependencies: @@ -2589,15 +2629,15 @@ snapshots: '@floating-ui/core': 1.6.0 '@floating-ui/utils': 0.2.1 - '@floating-ui/react-dom@2.0.8(react-dom@18.2.0)(react@18.2.0)': + '@floating-ui/react-dom@2.0.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@floating-ui/dom': 1.6.3 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - '@floating-ui/react@0.26.12(react-dom@18.2.0)(react@18.2.0)': + '@floating-ui/react@0.26.12(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: - '@floating-ui/react-dom': 2.0.8(react-dom@18.2.0)(react@18.2.0) + '@floating-ui/react-dom': 2.0.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@floating-ui/utils': 0.2.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -2617,48 +2657,48 @@ snapshots: '@humanwhocodes/object-schema@1.2.1': {} - '@mantine/carousel@7.8.1(@mantine/core@7.8.1)(@mantine/hooks@7.8.1)(embla-carousel-react@7.1.0)(react-dom@18.2.0)(react@18.2.0)': + '@mantine/carousel@7.8.1(@mantine/core@7.8.1(@mantine/hooks@7.8.1(react@18.2.0))(@types/react@18.2.14)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@mantine/hooks@7.8.1(react@18.2.0))(embla-carousel-react@7.1.0(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: - '@mantine/core': 7.8.1(@mantine/hooks@7.8.1)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0) + '@mantine/core': 7.8.1(@mantine/hooks@7.8.1(react@18.2.0))(@types/react@18.2.14)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@mantine/hooks': 7.8.1(react@18.2.0) embla-carousel-react: 7.1.0(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - '@mantine/charts@7.8.1(@mantine/core@7.8.1)(@mantine/hooks@7.8.1)(react-dom@18.2.0)(react@18.2.0)(recharts@2.12.7)': + '@mantine/charts@7.8.1(@mantine/core@7.8.1(@mantine/hooks@7.8.1(react@18.2.0))(@types/react@18.2.14)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@mantine/hooks@7.8.1(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(recharts@2.12.7(react-dom@18.2.0(react@18.2.0))(react@18.2.0))': dependencies: - '@mantine/core': 7.8.1(@mantine/hooks@7.8.1)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0) + '@mantine/core': 7.8.1(@mantine/hooks@7.8.1(react@18.2.0))(@types/react@18.2.14)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@mantine/hooks': 7.8.1(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - recharts: 2.12.7(react-dom@18.2.0)(react@18.2.0) + recharts: 2.12.7(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - '@mantine/code-highlight@7.8.1(@mantine/core@7.8.1)(@mantine/hooks@7.8.1)(react-dom@18.2.0)(react@18.2.0)': + '@mantine/code-highlight@7.8.1(@mantine/core@7.8.1(@mantine/hooks@7.8.1(react@18.2.0))(@types/react@18.2.14)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@mantine/hooks@7.8.1(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: - '@mantine/core': 7.8.1(@mantine/hooks@7.8.1)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0) + '@mantine/core': 7.8.1(@mantine/hooks@7.8.1(react@18.2.0))(@types/react@18.2.14)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@mantine/hooks': 7.8.1(react@18.2.0) clsx: 2.1.0 highlight.js: 11.9.0 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - '@mantine/core@7.8.1(@mantine/hooks@7.8.1)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0)': + '@mantine/core@7.8.1(@mantine/hooks@7.8.1(react@18.2.0))(@types/react@18.2.14)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: - '@floating-ui/react': 0.26.12(react-dom@18.2.0)(react@18.2.0) + '@floating-ui/react': 0.26.12(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@mantine/hooks': 7.8.1(react@18.2.0) clsx: 2.1.0 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-number-format: 5.3.1(react-dom@18.2.0)(react@18.2.0) + react-number-format: 5.3.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react-remove-scroll: 2.5.7(@types/react@18.2.14)(react@18.2.0) react-textarea-autosize: 8.5.3(@types/react@18.2.14)(react@18.2.0) type-fest: 4.16.0 transitivePeerDependencies: - '@types/react' - '@mantine/dates@7.8.1(@mantine/core@7.8.1)(@mantine/hooks@7.8.1)(dayjs@1.11.10)(react-dom@18.2.0)(react@18.2.0)': + '@mantine/dates@7.8.1(@mantine/core@7.8.1(@mantine/hooks@7.8.1(react@18.2.0))(@types/react@18.2.14)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@mantine/hooks@7.8.1(react@18.2.0))(dayjs@1.11.10)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: - '@mantine/core': 7.8.1(@mantine/hooks@7.8.1)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0) + '@mantine/core': 7.8.1(@mantine/hooks@7.8.1(react@18.2.0))(@types/react@18.2.14)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@mantine/hooks': 7.8.1(react@18.2.0) clsx: 2.1.0 dayjs: 1.11.10 @@ -2675,14 +2715,14 @@ snapshots: dependencies: react: 18.2.0 - '@mantine/notifications@7.8.1(@mantine/core@7.8.1)(@mantine/hooks@7.8.1)(react-dom@18.2.0)(react@18.2.0)': + '@mantine/notifications@7.8.1(@mantine/core@7.8.1(@mantine/hooks@7.8.1(react@18.2.0))(@types/react@18.2.14)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@mantine/hooks@7.8.1(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: - '@mantine/core': 7.8.1(@mantine/hooks@7.8.1)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0) + '@mantine/core': 7.8.1(@mantine/hooks@7.8.1(react@18.2.0))(@types/react@18.2.14)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@mantine/hooks': 7.8.1(react@18.2.0) '@mantine/store': 7.8.1(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) + react-transition-group: 4.4.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@mantine/store@7.8.1(react@18.2.0)': dependencies: @@ -2693,7 +2733,7 @@ snapshots: monaco-editor: 0.48.0 state-local: 1.0.7 - '@monaco-editor/react@4.5.1(monaco-editor@0.48.0)(react-dom@18.2.0)(react@18.2.0)': + '@monaco-editor/react@4.5.1(monaco-editor@0.48.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@monaco-editor/loader': 1.3.3(monaco-editor@0.48.0) monaco-editor: 0.48.0 @@ -2850,6 +2890,10 @@ snapshots: dependencies: '@types/react': 18.2.14 + '@types/react-grid-layout@1.3.5': + dependencies: + '@types/react': 18.2.14 + '@types/react-redux@7.1.25': dependencies: '@types/hoist-non-react-statics': 3.3.1 @@ -2871,7 +2915,7 @@ snapshots: '@types/semver@7.5.0': {} - '@typescript-eslint/eslint-plugin@5.60.1(@typescript-eslint/parser@5.60.1)(eslint@8.43.0)(typescript@5.1.6)': + '@typescript-eslint/eslint-plugin@5.60.1(@typescript-eslint/parser@5.60.1(eslint@8.43.0)(typescript@5.1.6))(eslint@8.43.0)(typescript@5.1.6)': dependencies: '@eslint-community/regexpp': 4.5.1 '@typescript-eslint/parser': 5.60.1(eslint@8.43.0)(typescript@5.1.6) @@ -2885,6 +2929,7 @@ snapshots: natural-compare-lite: 1.4.0 semver: 7.5.1 tsutils: 3.21.0(typescript@5.1.6) + optionalDependencies: typescript: 5.1.6 transitivePeerDependencies: - supports-color @@ -2896,6 +2941,7 @@ snapshots: '@typescript-eslint/typescript-estree': 5.60.1(typescript@5.1.6) debug: 4.3.4 eslint: 8.43.0 + optionalDependencies: typescript: 5.1.6 transitivePeerDependencies: - supports-color @@ -2912,6 +2958,7 @@ snapshots: debug: 4.3.4 eslint: 8.43.0 tsutils: 3.21.0(typescript@5.1.6) + optionalDependencies: typescript: 5.1.6 transitivePeerDependencies: - supports-color @@ -2927,6 +2974,7 @@ snapshots: is-glob: 4.0.3 semver: 7.5.1 tsutils: 3.21.0(typescript@5.1.6) + optionalDependencies: typescript: 5.1.6 transitivePeerDependencies: - supports-color @@ -2951,10 +2999,10 @@ snapshots: '@typescript-eslint/types': 5.60.1 eslint-visitor-keys: 3.4.1 - '@vitejs/plugin-react-swc@3.3.2(vite@4.3.9)': + '@vitejs/plugin-react-swc@3.3.2(vite@4.3.9(@types/node@20.3.2)(sugarss@4.0.1(postcss@8.4.33)))': dependencies: '@swc/core': 1.3.67 - vite: 4.3.9(@types/node@20.3.2) + vite: 4.3.9(@types/node@20.3.2)(sugarss@4.0.1(postcss@8.4.33)) transitivePeerDependencies: - '@swc/helpers' @@ -3101,6 +3149,8 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + clsx@1.2.1: {} + clsx@2.0.0: {} clsx@2.1.0: {} @@ -3354,18 +3404,18 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.8.0(@typescript-eslint/parser@5.60.1)(eslint-import-resolver-node@0.3.7)(eslint@8.43.0): + eslint-module-utils@2.8.0(@typescript-eslint/parser@5.60.1(eslint@8.43.0)(typescript@5.1.6))(eslint-import-resolver-node@0.3.7)(eslint@8.43.0): dependencies: - '@typescript-eslint/parser': 5.60.1(eslint@8.43.0)(typescript@5.1.6) debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 5.60.1(eslint@8.43.0)(typescript@5.1.6) eslint: 8.43.0 eslint-import-resolver-node: 0.3.7 transitivePeerDependencies: - supports-color - eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.60.1)(eslint@8.43.0): + eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.60.1(eslint@8.43.0)(typescript@5.1.6))(eslint@8.43.0): dependencies: - '@typescript-eslint/parser': 5.60.1(eslint@8.43.0)(typescript@5.1.6) array-includes: 3.1.6 array.prototype.flat: 1.3.1 array.prototype.flatmap: 1.3.1 @@ -3373,7 +3423,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.43.0 eslint-import-resolver-node: 0.3.7 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.60.1)(eslint-import-resolver-node@0.3.7)(eslint@8.43.0) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.60.1(eslint@8.43.0)(typescript@5.1.6))(eslint-import-resolver-node@0.3.7)(eslint@8.43.0) has: 1.0.3 is-core-module: 2.12.1 is-glob: 4.0.3 @@ -3382,17 +3432,20 @@ snapshots: resolve: 1.22.2 semver: 6.3.0 tsconfig-paths: 3.14.2 + optionalDependencies: + '@typescript-eslint/parser': 5.60.1(eslint@8.43.0)(typescript@5.1.6) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-prettier@4.2.1(eslint-config-prettier@8.8.0)(eslint@8.43.0)(prettier@2.8.8): + eslint-plugin-prettier@4.2.1(eslint-config-prettier@8.8.0(eslint@8.43.0))(eslint@8.43.0)(prettier@2.8.8): dependencies: eslint: 8.43.0 - eslint-config-prettier: 8.8.0(eslint@8.43.0) prettier: 2.8.8 prettier-linter-helpers: 1.0.0 + optionalDependencies: + eslint-config-prettier: 8.8.0(eslint@8.43.0) eslint-plugin-react-hooks@4.6.0(eslint@8.43.0): dependencies: @@ -3515,6 +3568,8 @@ snapshots: fast-diff@1.3.0: {} + fast-equals@4.0.3: {} + fast-equals@5.0.1: {} fast-glob@3.2.12: @@ -4132,7 +4187,7 @@ snapshots: raf-schd@4.0.3: {} - react-beautiful-dnd@13.1.1(react-dom@18.2.0)(react@18.2.0): + react-beautiful-dnd@13.1.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.21.5 css-box-model: 1.2.1 @@ -4140,7 +4195,7 @@ snapshots: raf-schd: 4.0.3 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-redux: 7.2.9(react-dom@18.2.0)(react@18.2.0) + react-redux: 7.2.9(react-dom@18.2.0(react@18.2.0))(react@18.2.0) redux: 4.2.1 use-memo-one: 1.1.3(react@18.2.0) transitivePeerDependencies: @@ -4152,27 +4207,46 @@ snapshots: react: 18.2.0 scheduler: 0.23.0 + react-draggable@4.4.6(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + clsx: 1.2.1 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-error-boundary@4.0.10(react@18.2.0): dependencies: '@babel/runtime': 7.21.5 react: 18.2.0 + react-grid-layout@1.4.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + clsx: 2.1.1 + fast-equals: 4.0.3 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-draggable: 4.4.6(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react-resizable: 3.0.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + resize-observer-polyfill: 1.5.1 + react-is@16.13.1: {} react-is@17.0.2: {} - react-number-format@5.3.1(react-dom@18.2.0)(react@18.2.0): + react-number-format@5.3.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: prop-types: 15.8.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-query@3.39.3(react-dom@18.2.0)(react@18.2.0): + react-query@3.39.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.21.5 broadcast-channel: 3.7.0 match-sorter: 6.3.1 react: 18.2.0 + optionalDependencies: react-dom: 18.2.0(react@18.2.0) react-querybuilder@6.5.5(react@18.2.0): @@ -4181,7 +4255,7 @@ snapshots: immer: 10.0.3 react: 18.2.0 - react-redux@7.2.9(react-dom@18.2.0)(react@18.2.0): + react-redux@7.2.9(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.21.5 '@types/react-redux': 7.1.25 @@ -4189,32 +4263,43 @@ snapshots: loose-envify: 1.4.0 prop-types: 15.8.1 react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) react-is: 17.0.2 + optionalDependencies: + react-dom: 18.2.0(react@18.2.0) react-remove-scroll-bar@2.3.4(@types/react@18.2.14)(react@18.2.0): dependencies: - '@types/react': 18.2.14 react: 18.2.0 react-style-singleton: 2.2.1(@types/react@18.2.14)(react@18.2.0) tslib: 2.6.2 + optionalDependencies: + '@types/react': 18.2.14 react-remove-scroll@2.5.7(@types/react@18.2.14)(react@18.2.0): dependencies: - '@types/react': 18.2.14 react: 18.2.0 react-remove-scroll-bar: 2.3.4(@types/react@18.2.14)(react@18.2.0) react-style-singleton: 2.2.1(@types/react@18.2.14)(react@18.2.0) tslib: 2.6.2 use-callback-ref: 1.3.0(@types/react@18.2.14)(react@18.2.0) use-sidecar: 1.1.2(@types/react@18.2.14)(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.14 - react-resizable-panels@0.0.53(react-dom@18.2.0)(react@18.2.0): + react-resizable-panels@0.0.53(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-router-dom@6.14.0(react-dom@18.2.0)(react@18.2.0): + react-resizable@3.0.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + prop-types: 15.8.1 + react: 18.2.0 + react-draggable: 4.4.6(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + transitivePeerDependencies: + - react-dom + + react-router-dom@6.14.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@remix-run/router': 1.7.0 react: 18.2.0 @@ -4226,21 +4311,22 @@ snapshots: '@remix-run/router': 1.7.0 react: 18.2.0 - react-smooth@4.0.1(react-dom@18.2.0)(react@18.2.0): + react-smooth@4.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: fast-equals: 5.0.1 prop-types: 15.8.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) + react-transition-group: 4.4.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react-style-singleton@2.2.1(@types/react@18.2.14)(react@18.2.0): dependencies: - '@types/react': 18.2.14 get-nonce: 1.0.1 invariant: 2.2.4 react: 18.2.0 tslib: 2.6.2 + optionalDependencies: + '@types/react': 18.2.14 react-textarea-autosize@8.5.3(@types/react@18.2.14)(react@18.2.0): dependencies: @@ -4251,7 +4337,7 @@ snapshots: transitivePeerDependencies: - '@types/react' - react-transition-group@4.4.5(react-dom@18.2.0)(react@18.2.0): + react-transition-group@4.4.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.21.5 dom-helpers: 5.2.1 @@ -4260,7 +4346,7 @@ snapshots: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-window@1.8.9(react-dom@18.2.0)(react@18.2.0): + react-window@1.8.9(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.21.5 memoize-one: 5.2.1 @@ -4275,7 +4361,7 @@ snapshots: dependencies: decimal.js-light: 2.5.1 - recharts@2.12.7(react-dom@18.2.0)(react@18.2.0): + recharts@2.12.7(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: clsx: 2.1.1 eventemitter3: 4.0.7 @@ -4283,7 +4369,7 @@ snapshots: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) react-is: 16.13.1 - react-smooth: 4.0.1(react-dom@18.2.0)(react@18.2.0) + react-smooth: 4.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) recharts-scale: 0.4.5 tiny-invariant: 1.3.3 victory-vendor: 36.9.2 @@ -4304,6 +4390,8 @@ snapshots: repeat-string@1.6.1: {} + resize-observer-polyfill@1.5.1: {} + resolve-from@4.0.0: {} resolve@1.22.2: @@ -4507,9 +4595,10 @@ snapshots: use-callback-ref@1.3.0(@types/react@18.2.14)(react@18.2.0): dependencies: - '@types/react': 18.2.14 react: 18.2.0 tslib: 2.6.2 + optionalDependencies: + '@types/react': 18.2.14 use-composed-ref@1.3.0(react@18.2.0): dependencies: @@ -4517,14 +4606,16 @@ snapshots: use-isomorphic-layout-effect@1.1.2(@types/react@18.2.14)(react@18.2.0): dependencies: - '@types/react': 18.2.14 react: 18.2.0 + optionalDependencies: + '@types/react': 18.2.14 use-latest@1.2.1(@types/react@18.2.14)(react@18.2.0): dependencies: - '@types/react': 18.2.14 react: 18.2.0 use-isomorphic-layout-effect: 1.1.2(@types/react@18.2.14)(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.14 use-memo-one@1.1.3(react@18.2.0): dependencies: @@ -4532,10 +4623,11 @@ snapshots: use-sidecar@1.1.2(@types/react@18.2.14)(react@18.2.0): dependencies: - '@types/react': 18.2.14 detect-node-es: 1.1.0 react: 18.2.0 tslib: 2.6.2 + optionalDependencies: + '@types/react': 18.2.14 util-deprecate@1.0.2: {} @@ -4556,14 +4648,15 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - vite@4.3.9(@types/node@20.3.2): + vite@4.3.9(@types/node@20.3.2)(sugarss@4.0.1(postcss@8.4.33)): dependencies: - '@types/node': 20.3.2 esbuild: 0.17.19 postcss: 8.4.33 rollup: 3.23.0 optionalDependencies: + '@types/node': 20.3.2 fsevents: 2.3.3 + sugarss: 4.0.1(postcss@8.4.33) which-boxed-primitive@1.0.2: dependencies: diff --git a/src/main.tsx b/src/main.tsx index 3ddf0b78..f05a59ee 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -16,13 +16,13 @@ import { QueryClient, QueryClientProvider } from 'react-query'; const queryClient = new QueryClient(); ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( - - - - - - - - - + + + + + + + + + , ); diff --git a/src/pages/Dashboards/index.tsx b/src/pages/Dashboards/index.tsx index c3c1188d..a6d6e48e 100644 --- a/src/pages/Dashboards/index.tsx +++ b/src/pages/Dashboards/index.tsx @@ -1,12 +1,61 @@ import { Box } from '@mantine/core'; +import 'react-grid-layout/css/styles.css'; +import 'react-resizable/css/styles.css'; +import GridLayout from 'react-grid-layout'; + +const BasicSample = () => { + const layout = [ + { i: 'a', x: 0, y: 0, w: 3, h: 1, minH: 1 }, + { i: 'b', x: 3, y: 0, w: 4, h: 1, minH: 1 }, + { i: 'c', x: 7, y: 0, w: 6, h: 1, minH: 1 }, + { i: 'd', x: 0, y: 1, w: 8, h: 1, minH: 1 }, + { i: 'e', x: 8, y: 1, w: 4, h: 1, minH: 1 }, + ]; + + return ( + +
+ Item A +
+
+ Item B +
+
+ Item C +
+
+ Item D +
+
+ Item E +
+
+ ); +}; const Dashboards = () => { return ( - - {/*
-
*/} + + ); }; -export default Dashboards; \ No newline at end of file +export default Dashboards; From a0b404a53bc2771ed66df7f3aed0796c3bd07360 Mon Sep 17 00:00:00 2001 From: balaji-jr Date: Mon, 15 Jul 2024 11:55:38 +0530 Subject: [PATCH 04/34] wip - basic implementation of components and chart transformations --- package.json | 1 + pnpm-lock.yaml | 39 ++++ src/constants/theme.ts | 4 +- src/pages/Dashboards/Charts.tsx | 73 +++++++ src/pages/Dashboards/CreateTileForm.tsx | 193 ++++++++++++++++++ src/pages/Dashboards/Dashboard.tsx | 63 ++++++ src/pages/Dashboards/SideBar.tsx | 62 ++++++ src/pages/Dashboards/Tile.tsx | 31 +++ src/pages/Dashboards/Toolbar.tsx | 33 +++ src/pages/Dashboards/VizEditorModal.tsx | 139 +++++++++++++ src/pages/Dashboards/index.tsx | 49 +---- .../providers/DashboardsProvider.ts | 137 +++++++++++++ src/pages/Dashboards/styles/Form.module.css | 27 +++ .../Dashboards/styles/ReactGridLayout.css | 4 + .../Dashboards/styles/VizEditor.module.css | 28 +++ .../Dashboards/styles/dashboard.module.css | 0 .../Dashboards/styles/sidebar.module.css | 25 +++ src/pages/Dashboards/styles/tile.module.css | 31 +++ .../Dashboards/styles/toolbar.module.css | 25 +++ .../components/Querier/QueryCodeEditor.tsx | 2 +- src/pages/Stream/index.tsx | 2 +- src/routes/elements.tsx | 7 +- src/utils/exportImage.ts | 18 ++ 23 files changed, 947 insertions(+), 46 deletions(-) create mode 100644 src/pages/Dashboards/Charts.tsx create mode 100644 src/pages/Dashboards/CreateTileForm.tsx create mode 100644 src/pages/Dashboards/Dashboard.tsx create mode 100644 src/pages/Dashboards/SideBar.tsx create mode 100644 src/pages/Dashboards/Tile.tsx create mode 100644 src/pages/Dashboards/Toolbar.tsx create mode 100644 src/pages/Dashboards/VizEditorModal.tsx create mode 100644 src/pages/Dashboards/providers/DashboardsProvider.ts create mode 100644 src/pages/Dashboards/styles/Form.module.css create mode 100644 src/pages/Dashboards/styles/ReactGridLayout.css create mode 100644 src/pages/Dashboards/styles/VizEditor.module.css create mode 100644 src/pages/Dashboards/styles/dashboard.module.css create mode 100644 src/pages/Dashboards/styles/sidebar.module.css create mode 100644 src/pages/Dashboards/styles/tile.module.css create mode 100644 src/pages/Dashboards/styles/toolbar.module.css create mode 100644 src/utils/exportImage.ts diff --git a/package.json b/package.json index bf766857..6ea7fb65 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "axios": "^1.4.0", "dayjs": "^1.11.10", "embla-carousel-react": "7.1.0", + "html2canvas": "^1.4.1", "http-status-codes": "^2.2.0", "immer": "^10.0.2", "js-cookie": "^3.0.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 31d3d6e4..bae465d5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,6 +56,9 @@ importers: embla-carousel-react: specifier: 7.1.0 version: 7.1.0(react@18.2.0) + html2canvas: + specifier: ^1.4.1 + version: 1.4.1 http-status-codes: specifier: ^2.2.0 version: 2.2.0 @@ -898,6 +901,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + base64-arraybuffer@1.0.2: + resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} + engines: {node: '>= 0.6.0'} + big-integer@1.6.52: resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} engines: {node: '>=0.6'} @@ -997,6 +1004,9 @@ packages: css-box-model@1.2.1: resolution: {integrity: sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==} + css-line-break@2.1.0: + resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -1442,6 +1452,10 @@ packages: hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + html2canvas@1.4.1: + resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==} + engines: {node: '>=8.0.0'} + http-status-codes@2.2.0: resolution: {integrity: sha512-feERVo9iWxvnejp3SEfm/+oNG517npqL2/PIA8ORjyOZjGC7TwCRQsZylciLS64i6pJ0wRYz3rkXLRwbtFa8Ng==} @@ -2226,6 +2240,9 @@ packages: engines: {node: '>=12.17'} hasBin: true + text-segmentation@1.0.3: + resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==} + text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} @@ -2349,6 +2366,9 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + utrie@1.0.2: + resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==} + victory-vendor@36.9.2: resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} @@ -3098,6 +3118,8 @@ snapshots: balanced-match@1.0.2: {} + base64-arraybuffer@1.0.2: {} + big-integer@1.6.52: {} brace-expansion@1.1.11: @@ -3209,6 +3231,10 @@ snapshots: dependencies: tiny-invariant: 1.3.1 + css-line-break@2.1.0: + dependencies: + utrie: 1.0.2 + cssesc@3.0.0: {} csstype@3.1.2: {} @@ -3737,6 +3763,11 @@ snapshots: dependencies: react-is: 16.13.1 + html2canvas@1.4.1: + dependencies: + css-line-break: 2.1.0 + text-segmentation: 1.0.3 + http-status-codes@2.2.0: {} human-signals@1.1.1: {} @@ -4527,6 +4558,10 @@ snapshots: typical: 7.1.1 wordwrapjs: 5.1.0 + text-segmentation@1.0.3: + dependencies: + utrie: 1.0.2 + text-table@0.2.0: {} tiny-invariant@1.3.1: {} @@ -4631,6 +4666,10 @@ snapshots: util-deprecate@1.0.2: {} + utrie@1.0.2: + dependencies: + base64-arraybuffer: 1.0.2 + victory-vendor@36.9.2: dependencies: '@types/d3-array': 3.2.1 diff --git a/src/constants/theme.ts b/src/constants/theme.ts index 973f6e51..6af20fc2 100644 --- a/src/constants/theme.ts +++ b/src/constants/theme.ts @@ -7,4 +7,6 @@ export const LOGS_SECONDARY_TOOLBAR_HEIGHT = 68; export const STREAM_PRIMARY_TOOLBAR_CONTAINER_HEIGHT = 48; export const STREAM_PRIMARY_TOOLBAR_HEIGHT = 26; export const STREAM_SECONDARY_TOOLBAR_HRIGHT = 70; -export const SECONDARY_SIDEBAR_WIDTH = 50; \ No newline at end of file +export const SECONDARY_SIDEBAR_WIDTH = 50; +export const DASHBOARDS_SIDEBAR_WIDTH = 200; +export const DASHBOARD_TOOLBAR_HEIGHT = 50; \ No newline at end of file diff --git a/src/pages/Dashboards/Charts.tsx b/src/pages/Dashboards/Charts.tsx new file mode 100644 index 00000000..f4318966 --- /dev/null +++ b/src/pages/Dashboards/Charts.tsx @@ -0,0 +1,73 @@ +import { DonutChart } from '@mantine/charts'; +import _ from 'lodash'; + +export const chartColorsMap = { + 'black': 'dark.6', + 'gray': 'gray.6', + 'red': 'red.6', + 'pink': 'pink.6', + 'grape': 'grape.6', + 'violet': 'violet.6', + 'indigo': 'indigo.6', + 'cyan': 'cyan.6', + 'blue': 'blue.6', + 'teal': 'teal.6', + 'green': 'green.6', + 'lime': 'lime.6', + 'yellow': 'yellow.6', + 'orange': 'orange.6', +} + +export const colors = ['red', 'pink', 'grape', 'violet', 'indigo', 'cyan', 'blue', 'teal', 'green', 'lime', 'yellow', 'orange'] +export const nullColor = 'gray' + +export const getVizComponent = (viz: string) => { + if (viz === 'donut-chart') { + return charts.Donut; + } +}; + +export type DonutData = { + name: string; + value: number; + color: string; +}[]; + +type RawData = { + [key: string]: number; + [key: number]: number; +}; + +export const sanitizeDonutData = (data: RawData[], colorConfig: {[key: string]: string}): DonutData => { + if (!_.isArray(data)) return []; + if (!_.isObject(data[0])) return []; + + const firstRecord = data[0] || {}; + const topNKeys = _.keys(firstRecord).slice(0, 5); + const topNObject = _.pick(firstRecord, topNKeys); + const restObject = _.omit(firstRecord, topNKeys); + + let usedColors: string[] = []; + const topNArcs = _.reduce( + topNObject, + (acc: DonutData, value, key) => { + const color = colorConfig[key] || _.sample(_.difference(colors, usedColors)) || nullColor; + usedColors = [...usedColors, color]; + return [...acc, { name: key, value, color: chartColorsMap[color] || 'gray.6' }]; + }, + [], + ); + + const restArcValue = _.sum(_.values(restObject)); + return [...topNArcs, { name: 'Others', value: restArcValue, color: 'gray.6' }]; +} + +const Donut = (props: { data: DonutData }) => { + return ; +}; + +const charts = { + Donut, +}; + +export default charts; diff --git a/src/pages/Dashboards/CreateTileForm.tsx b/src/pages/Dashboards/CreateTileForm.tsx new file mode 100644 index 00000000..3b017303 --- /dev/null +++ b/src/pages/Dashboards/CreateTileForm.tsx @@ -0,0 +1,193 @@ +import { Box, Button, Select, Stack, Text, TextInput } from '@mantine/core'; +import classes from './styles/Form.module.css'; +import { useForm, UseFormReturnType } from '@mantine/form'; +import { Tile } from './providers/DashboardsProvider'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useAppStore } from '@/layouts/MainLayout/providers/AppProvider'; +import _ from 'lodash'; +import { SchemaList } from '../Stream/components/Querier/QueryCodeEditor'; +import { getLogStreamSchema } from '@/api/logStream'; +import { Field } from '@/@types/parseable/dataType'; +import { Editor } from '@monaco-editor/react'; +import VizEditorModal from './VizEditorModal'; + +const SectionHeader = (props: { title: string }) => { + return ( + + {props.title} + + ); +}; + +const VisPreview = () => { + return ( + + + + ); +}; + +const DataPreview = () => { + return ( + + + + ); +}; + +interface FormOpts extends Omit {} + +const useTileForm = (opts: FormOpts) => { + const form = useForm({ + mode: 'controlled', + initialValues: opts, + validate: {}, + validateInputOnChange: true, + validateInputOnBlur: true, + }); + + const onChangeValue = useCallback((key: string, value: any) => { + form.setFieldValue(key, value); + }, []); + + return { form, onChangeValue }; +}; + +type TileFormType = UseFormReturnType FormOpts>; + +const fetchStreamFields = async (stream: string, setFields: (fields: Field[]) => void) => { + try { + const res = await getLogStreamSchema(stream); + setFields(res.data.fields); + } catch { + setFields([]); + } +}; + +const Config = (props: { form: TileFormType }) => { + const { form } = props; + const { stream } = form.getValues(); + const [fields, setFields] = useState([]); + const [userSpecificStreams] = useAppStore((store) => store.userSpecificStreams); + const allStreams = useMemo( + () => _.map(userSpecificStreams, (stream) => ({ label: stream.name, value: stream.name })), + [userSpecificStreams], + ); + + useEffect(() => { + setFields([]); + if (_.size(stream) > 0) { + fetchStreamFields(stream, (fields: Field[]) => setFields(fields)); + } + }, [stream]); + + return ( + + + + + ({ label: _.get(vizLabelMap, viz, viz), value: viz }))} + classNames={{ label: classes.fieldTitle }} + label="Type" + placeholder="Type" + key="type" + {...props.form.getInputProps('type')} + style={{ width: '40%' }} + /> + + Colors + + {_.map(dataKeys, (dataKey) => { + return ( + + + ({ label: _.get(vizLabelMap, viz, viz), value: viz }))} classNames={{ label: classes.fieldTitle }} label="Type" placeholder="Type" - key="type" - {...props.form.getInputProps('type')} - style={{ width: '40%' }} + key="visualization.type" + {...props.form.getInputProps('visualization.type')} + style={{ width: '50%' }} + /> + ({ label: field, value: field }))} + classNames={{ label: classes.fieldTitle }} + label="Name" + placeholder="Name" + key="visualization.circularChartConfig.nameKey" + {...props.form.getInputProps('visualization.circularChartConfig.nameKey')} + style={{ width: '50%' }} + /> + + + + - - - - - - + {showLoader ? ( + + + + ) : ( + <> + + + + + + + + )} diff --git a/src/pages/Dashboards/CreateTileForm.tsx b/src/pages/Dashboards/CreateTileForm.tsx index 7bed5ae6..535c1e2f 100644 --- a/src/pages/Dashboards/CreateTileForm.tsx +++ b/src/pages/Dashboards/CreateTileForm.tsx @@ -18,9 +18,9 @@ import { sanitiseSqlString } from '@/utils/sanitiseSqlString'; const selectStreamWarningText = 'Select a stream to continue'; const validateQueryWarningText = 'Validate query to continue'; -const emptyVizWarning = 'Select visualization for the data'; +const emptyVizWarning = 'No visualization selected for the tile'; -const {toggleVizEditorModal} = dashboardsStoreReducers; +const {toggleVizEditorModal, toggleCreateTileModal} = dashboardsStoreReducers; const SectionHeader = (props: { title: string; actionBtnProps?: { label: string; onClick: () => void } }) => { const { title, actionBtnProps } = props; @@ -129,7 +129,13 @@ const useTileForm = (opts: FormOpts) => { const form = useForm({ mode: 'controlled', initialValues: opts, - validate: {}, + validate: { + name: (val) => _.isEmpty(val) ? "Cannot be empty" : null, + stream: (val) => _.isEmpty(val) ? "Cannot be empty" : null, + description: (val) => _.isEmpty(val) ? "Cannot be empty" : null, + query: (val) => _.isEmpty(val) ? "Cannot be empty" : null, + isQueryValidated: (val) => val ? null : "Query not validated" + }, validateInputOnChange: true, validateInputOnBlur: true, }); @@ -197,7 +203,7 @@ const Query = (props: { form: TileFormType; onChangeValue: (key: string, value: const now = new Date(); const santizedQuery = sanitiseSqlString(query); onChangeValue('query', santizedQuery); - fetchTileData({ query: santizedQuery, startTime: new Date(now.getTime() - 24 * 60 * 60 * 1000), endTime: now }); + fetchTileData({ query: santizedQuery, startTime: new Date(now.getTime() - 24 * 3 * 60 * 60 * 1000), endTime: now }); }, [query]); const onEditorChange = useCallback((query: string | undefined) => { @@ -289,11 +295,10 @@ const Config = (props: { form: TileFormType; onChangeValue: (key: string, value: return ( - + @@ -323,11 +326,11 @@ const Config = (props: { form: TileFormType; onChangeValue: (key: string, value: }; const defaultTileOpts = { - name: '', - description: '', + name: 'hello', + description: 'hello', stream: 'teststream', isQueryValidated: false, - query: 'SELECT level, COUNT(*) AS level_count FROM teststream GROUP BY level;', + query: "select * from teststream", data: null, visualization: { type: 'donut-chart', @@ -341,15 +344,33 @@ const defaultTileOpts = { const CreateTileForm = () => { const { form, onChangeValue } = useTileForm(defaultTileOpts); + const [, setDashbaordsStore] = useDashboardsStore(store => null); + + const closeEditForm = useCallback(() => { + setDashbaordsStore(store => toggleCreateTileModal(store, false)); + }, []); return ( - - - + + + Create Tile + + + + + + + + - - - + + + + + + + + ); diff --git a/src/pages/Dashboards/Dashboard.tsx b/src/pages/Dashboards/Dashboard.tsx index 0ed82359..ba5a3fb0 100644 --- a/src/pages/Dashboards/Dashboard.tsx +++ b/src/pages/Dashboards/Dashboard.tsx @@ -9,25 +9,27 @@ import Tile from './Tile'; // import classes from './styles/tile.module.css'; import classes from './styles/Dashboard.module.css' import CreateTileForm from './CreateTileForm'; -import { useDashboardsStore, dashboardsStoreReducers } from './providers/DashboardsProvider'; +import { useDashboardsStore, dashboardsStoreReducers, genLayout } from './providers/DashboardsProvider'; import _ from 'lodash'; import { IconChartBar } from '@tabler/icons-react'; import { useCallback } from 'react'; const {toggleCreateDashboardModal, toggleCreateTileModal} = dashboardsStoreReducers; -const BasicSample = () => { - const layout = [ - { i: 'a', x: 0, y: 0, w: 4, h: 1, minH: 1 }, - // { i: 'a', x: 0, y: 0, w: 4, h: 1, minH: 1 }, - // { i: 'a', x: 0, y: 0, w: 4, h: 1, minH: 1 }, - // { i: 'b', x: 4, y: 0, w: 4, h: 1, minH: 1 }, - // { i: 'c', x: 8, y: 0, w: 4, h: 1, minH: 1 }, - // { i: 'd', x: 12, y: 0, w: 4, h: 1, minH: 1 }, - ]; +const TilesView = () => { + const [activeDashboard] = useDashboardsStore(store => store.activeDashboard); + const hasNoTiles = _.size(activeDashboard?.tiles) < 1; + + if (hasNoTiles || !activeDashboard) return ; + + + const {tiles} = activeDashboard; + + // debug - memo + const layout = genLayout(tiles); return ( - + { rowHeight={300} width={window.innerWidth - NAVBAR_WIDTH - DASHBOARDS_SIDEBAR_WIDTH} isResizable={false} - margin={[16,16]} + margin={[16, 16]} containerPadding={[20, 10]} compactType="horizontal" - isDraggable={true} - > -
- -
+ isDraggable={false}> + {_.map(layout, (item) => { + return ( +
+ +
+ ); + })} + {/*
Item B
@@ -88,14 +97,14 @@ const NoTilesView = () => { }, []) return ( - + {/* */} Create Tile Title Placeholder Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco - + {/* */} @@ -105,22 +114,14 @@ const NoTilesView = () => { const Dashboard = () => { const [dashboards] = useDashboardsStore(store => store.dashboards); - const [activeDashboard] = useDashboardsStore(store => store.activeDashboard) - const hasNoTiles = _.size(activeDashboard?.tiles) < 1; + const [createTileFormOpen] = useDashboardsStore(store => store.createTileFormOpen) if (_.isEmpty(dashboards)) return ; return ( - - {/* {hasNoTiles ? ( - - ) : ( - <> - - - )} */} + {createTileFormOpen ? : } ); }; diff --git a/src/pages/Dashboards/SideBar.tsx b/src/pages/Dashboards/SideBar.tsx index 63426823..fe36cfe5 100644 --- a/src/pages/Dashboards/SideBar.tsx +++ b/src/pages/Dashboards/SideBar.tsx @@ -6,7 +6,7 @@ import { Dashboard, useDashboardsStore, dashboardsStoreReducers } from './provid import { useCallback, useEffect, useState } from 'react'; import _ from 'lodash'; -const {selectDashboard} = dashboardsStoreReducers; +const {selectDashboard, toggleCreateDashboardModal} = dashboardsStoreReducers; interface DashboardItemProps extends Dashboard { activeDashboardId: undefined | string; @@ -14,58 +14,58 @@ interface DashboardItemProps extends Dashboard { } const DashboardListItem = (props: DashboardItemProps) => { - const { name, id, pinned, tiles, activeDashboardId, onSelect } = props; + const { name, dashboard_id, tiles, activeDashboardId, onSelect } = props; const totalTiles = _.size(tiles); - const isActive = id === activeDashboardId; + const isActive = dashboard_id === activeDashboardId; + + const selectDashboard = useCallback(() => onSelect(dashboard_id), []) return ( - onSelect(id)}> + {name} - {`${totalTiles} Tile${totalTiles > 1 ? 's' : ''}`} + {`${totalTiles} Tile${totalTiles === 1 ? '' : 's'}`} ); }; const DashboardList = () => { const [dashboards, setDashbaordsStore] = useDashboardsStore((store) => store.dashboards); - const [activeDashboardId] = useDashboardsStore(store => store.activeDashboard?.id) - // useEffect(() => { - // const { pinned, unpinned } = _.reduce( - // dashboards, - // (acc: { pinned: Dashboard[]; unpinned: Dashboard[] }, dashboard: Dashboard) => { - // const { pinned, unpinned } = acc; - // if (dashboard.pinned) { - // return { pinned: [...pinned, dashboard], unpinned }; - // } else { - // return { pinned, unpinned: [...unpinned, dashboard] }; - // } - // }, - // { pinned: [], unpinned: [] }, - // ); - - // }, [dashboards]); + const [activeDashboardId] = useDashboardsStore((store) => store.activeDashboard?.dashboard_id); const onSelectDashboardId = useCallback((dashboardId: string) => { - setDashbaordsStore(store => selectDashboard(store, dashboardId)) - }, []) + setDashbaordsStore((store) => selectDashboard(store, dashboardId)); + }, []); return ( {_.map(dashboards, (dashboard) => { - return ; + return ( + + ); })} ); }; const SideBar = () => { - const [dashboards] = useDashboardsStore(store => store.dashboards); + const [dashboards, setDashbaordsStore] = useDashboardsStore((store) => store.dashboards); + + const openCreateStreamModal = useCallback(() => { + setDashbaordsStore((store) => toggleCreateDashboardModal(store, true)); + }, []); if (_.isEmpty(dashboards)) return null; + return ( - + + + ); diff --git a/src/pages/Dashboards/Table.tsx b/src/pages/Dashboards/Table.tsx new file mode 100644 index 00000000..50afa69f --- /dev/null +++ b/src/pages/Dashboards/Table.tsx @@ -0,0 +1,49 @@ +import { Stack, Table, Text } from "@mantine/core" +import { Tile } from "./providers/DashboardsProvider"; +import { TileQueryResponse } from "@/@types/parseable/api/dashboards"; +import classes from './styles/Table.module.css' +import _ from "lodash"; +import { useEffect, useRef, useState } from "react"; + +const makeRowData = (data: TileQueryResponse) => { + const {fields, records} = data; + return _.map(records, rec => { + return _.at(rec, fields); + }) +} + +const TableViz = (props: { tile: Tile; data: TileQueryResponse }) => { + const { + tile, + data: { fields, records }, + } = props; + const rowData = makeRowData({ fields, records }); + + const containerRef = useRef(null); + const [initialHeight, setInitialHeight] = useState(0); + + useEffect(() => { + if (containerRef.current) { + setInitialHeight(containerRef.current.offsetHeight); + } + }, []); + + return ( + + +
URLhttps://demo.parseable.iohttps://demo.parseable.com
Username
+ + + ); +}; + +export default TableViz; \ No newline at end of file diff --git a/src/pages/Dashboards/Tile.tsx b/src/pages/Dashboards/Tile.tsx index dfbc0f4d..bba026b6 100644 --- a/src/pages/Dashboards/Tile.tsx +++ b/src/pages/Dashboards/Tile.tsx @@ -1,31 +1,102 @@ -import { px, Stack, Text } from "@mantine/core" -import classes from './styles/tile.module.css' -import { IconDotsVertical } from "@tabler/icons-react"; -import charts, { getVizComponent } from "./Charts"; -import handleCapture from "@/utils/exportImage"; - -const TileConatiner = () => { - const View = getVizComponent("donut-chart") - return ( - - - - ) -} - -const Tile = () => { +import { Loader, px, Stack, Text } from '@mantine/core'; +import classes from './styles/tile.module.css'; +import { IconAlertTriangle, IconDotsVertical } from '@tabler/icons-react'; +import charts, { getVizComponent, isCircularChart, renderCircularChart, renderGraph } from './Charts'; +import handleCapture from '@/utils/exportImage'; +import { Tile, useDashboardsStore } from './providers/DashboardsProvider'; +import _ from 'lodash'; +import { useTileQuery } from '@/hooks/useDashboards'; +import { useCallback, useEffect, useState } from 'react'; +import { TileQueryResponse } from '@/@types/parseable/api/dashboards'; +import { sanitiseSqlString } from '@/utils/sanitiseSqlString'; +import Table from './Table'; + +const NoDataView = () => { + return ( + + + No data to display + + ); +}; + +const LoadingView = () => { + return ( + + + + ); +}; + +const CircularChart = (props: { tile: Tile; data: TileQueryResponse }) => { + const { tile, data } = props; + const { + visualization: { type, circularChartConfig }, + } = tile; + const { nameKey = '', valueKey = '' } = circularChartConfig; + return ( + + {renderCircularChart({ queryResponse: data, nameKey, valueKey, chart: type })} + + ); +}; + +const Graph = (props: { tile: Tile; data: TileQueryResponse }) => { + const { tile, data } = props; + const { + visualization: { type, graphConfig }, + } = tile; + const { xKey, yKeys } = graphConfig; return ( - + + {renderGraph({ queryResponse: data, xKey, yKeys, chart: type })} + + ); +}; + +const Tile = (props: { id: string }) => { + const [tileData, setTileData] = useState(); + const [activeDashboard] = useDashboardsStore((store) => store.activeDashboard); + const tile = _.chain(activeDashboard) + .get('tiles', []) + .find((tile) => tile.id === props.id) + .value(); + + const onQuerySuccess = useCallback((data: TileQueryResponse) => { + setTileData(data); + }, []); + + const { fetchTileData, isLoading } = useTileQuery({ onSuccess: onQuerySuccess }); + + useEffect(() => { + const now = new Date(); + // debug // should notify + const santizedQuery = sanitiseSqlString(tile.query, false); + fetchTileData({ query: santizedQuery, startTime: new Date(now.getTime() - 24 * 3 * 60 * 60 * 1000), endTime: now }); + }, []); + + const hasData = !_.isEmpty(tileData); + const Viz = tile.visualization.type === 'table' ? Table : isCircularChart(tile.visualization.type) ? CircularChart : Graph; + return ( + - - Error status - Description for the tile + + + {tile.name} + + {tile.description} - + - + {isLoading && } + {!hasData && !isLoading && } + {!isLoading && hasData && ( + + + + )} ); -} +}; -export default Tile; \ No newline at end of file +export default Tile; diff --git a/src/pages/Dashboards/Toolbar.tsx b/src/pages/Dashboards/Toolbar.tsx index 490b4267..9a73a2d0 100644 --- a/src/pages/Dashboards/Toolbar.tsx +++ b/src/pages/Dashboards/Toolbar.tsx @@ -2,12 +2,20 @@ import TimeRange from '@/components/Header/TimeRange'; import { Box, Button, Stack, Text } from '@mantine/core'; import { IconPencil } from '@tabler/icons-react'; import classes from './styles/toolbar.module.css'; -import { useDashboardsStore } from './providers/DashboardsProvider'; +import { useDashboardsStore, dashboardsStoreReducers } from './providers/DashboardsProvider'; +import { useCallback } from 'react'; + +const {toggleEditDashboardModal} = dashboardsStoreReducers; const EditLayoutButton = () => { + const [createTileFormOpen] = useDashboardsStore(store => store.createTileFormOpen) return ( - @@ -15,13 +23,28 @@ const EditLayoutButton = () => { }; const Toolbar = () => { - const [dashboardName] = useDashboardsStore((store) => store.activeDashboard?.name); + const [activeDashboard, setDashbaordsStore] = useDashboardsStore((store) => store.activeDashboard); + const openEditDashboardModal = useCallback(() => { + setDashbaordsStore((store) => toggleEditDashboardModal(store, true)) + }, []) + + if (!activeDashboard) return null; + + const {name, description} = activeDashboard; return ( - {dashboardName} + + + {name} + + + + + {description} + diff --git a/src/pages/Dashboards/VizEditorModal.tsx b/src/pages/Dashboards/VizEditorModal.tsx index 393bcbe2..72a79683 100644 --- a/src/pages/Dashboards/VizEditorModal.tsx +++ b/src/pages/Dashboards/VizEditorModal.tsx @@ -1,11 +1,11 @@ -import { Modal, Select, Stack, Text, TextInput } from '@mantine/core'; +import { Modal, MultiSelect, Select, Stack, Text, TextInput } from '@mantine/core'; import classes from './styles/VizEditor.module.css'; import { useDashboardsStore, Visualization, dashboardsStoreReducers, tileSizes } from './providers/DashboardsProvider'; import { useForm, UseFormReturnType } from '@mantine/form'; import { useCallback, useEffect, useState } from 'react'; import { visualizations } from './providers/DashboardsProvider'; import _ from 'lodash'; -import { colors as defaultColors, DonutData, isCircularChart, renderCircularChart, sanitizeDonutData, sanitizeDonutData2 } from './Charts'; +import { colors as defaultColors, DonutData, isCircularChart, renderCircularChart, renderGraph, sanitizeDonutData, sanitizeDonutData2 } from './Charts'; import charts from './Charts'; import { FormOpts, TileFormType, TileQuery, TileQueryResponse } from '@/@types/parseable/api/dashboards'; import { IconAlertTriangle } from '@tabler/icons-react'; @@ -35,8 +35,15 @@ const Viz = (props: {form: TileFormType}) => { // setChartData(sanitizeDonutData([data], form.values.colors)) // }, [data, form.values.colors]) - const renderFn = isCircularChart(type) ? renderCircularChart : renderCircularChart; - + const renderFn = isCircularChart(type) ? renderCircularChart : renderGraph; + const chartOpts = isCircularChart(type) + ? { + queryResponse: data, + nameKey: visualization.circularChartConfig.nameKey, + valueKey: visualization.circularChartConfig.valueKey, + chart: type, + } + : {queryResponse: data, xKey: visualization.graphConfig.xAxis, yKeys: visualization.graphConfig.yAxis, chart: 'line-chart'}; return ( @@ -44,7 +51,7 @@ const Viz = (props: {form: TileFormType}) => { showWarning && } */} { - renderFn({queryResponse: data, nameKey: visualization.circularChartConfig.nameKey, valueKey: visualization.circularChartConfig.valueKey, chart: type}) + renderFn(chartOpts) } {/* */} @@ -58,6 +65,7 @@ const vizLabelMap = { 'table': 'Table', 'line-chart': 'Line Chart', 'bar-chart': 'Bar Chart', + 'area-chart': 'Area Chart' } const sizeLabelMap = { @@ -124,13 +132,46 @@ const CircularChartConfig = (props: {form: TileFormType}) => { ); } +const GraphConfig = (props: {form: TileFormType}) => { + const {form: {values: {data}}} = props; + + return ( + +
+ {hasNoData ? ( + + ) : ( +
+ )} ); diff --git a/src/pages/Dashboards/VizEditorModal.tsx b/src/pages/Dashboards/VizEditorModal.tsx index 72a79683..a43caf81 100644 --- a/src/pages/Dashboards/VizEditorModal.tsx +++ b/src/pages/Dashboards/VizEditorModal.tsx @@ -1,6 +1,6 @@ import { Modal, MultiSelect, Select, Stack, Text, TextInput } from '@mantine/core'; import classes from './styles/VizEditor.module.css'; -import { useDashboardsStore, Visualization, dashboardsStoreReducers, tileSizes } from './providers/DashboardsProvider'; +import { useDashboardsStore, Visualization, dashboardsStoreReducers, tileSizes, circularChartTypes } from './providers/DashboardsProvider'; import { useForm, UseFormReturnType } from '@mantine/form'; import { useCallback, useEffect, useState } from 'react'; import { visualizations } from './providers/DashboardsProvider'; @@ -9,10 +9,10 @@ import { colors as defaultColors, DonutData, isCircularChart, renderCircularChar import charts from './Charts'; import { FormOpts, TileFormType, TileQuery, TileQueryResponse } from '@/@types/parseable/api/dashboards'; import { IconAlertTriangle } from '@tabler/icons-react'; - +import TableViz from './Table'; const {toggleVizEditorModal} = dashboardsStoreReducers; -const inValidVizType = 'Select a chart type' +const inValidVizType = 'Select a visualization type'; const WarningView = (props: { msg: string | null }) => { return ( @@ -23,38 +23,57 @@ const WarningView = (props: { msg: string | null }) => { ); }; +const CircularChart = (props: { form: TileFormType }) => { + const { + visualization: { type, circularChartConfig }, + data, + } = props.form.values; + const nameKey = _.get(circularChartConfig, 'nameKey', ''); + const valueKey = _.get(circularChartConfig, 'valueKey', ''); + + return ( + + {renderCircularChart({ queryResponse: data, nameKey, valueKey, chart: type })} + + ); +}; + +const Graph = (props: { form: TileFormType }) => { + const { + visualization: { type, graphConfig }, + data, + } = props.form.values; + const xKey = _.get(graphConfig, 'xAxis', ''); + const yKeys = _.get(graphConfig, 'yAxis', []); + return ( + {renderGraph({ queryResponse: data, xKey, yKeys, chart: type })} + ); +}; + +const Table = (props: {form: TileFormType }) => { + const data = props.form.values.data; + return ( + + + + + + ); +}; + const Viz = (props: {form: TileFormType}) => { const {form: {values: {visualization, data}}} = props; const {type} = visualization; const isValidVizType = _.includes(visualizations, type); const showWarning = !isValidVizType; - // const [chartData, setChartData] = useState([]); + const Viss = type === 'table' ? Table : isCircularChart(type) ? CircularChart : Graph; - // useEffect(() => { - // setChartData(sanitizeDonutData([data], form.values.colors)) - // }, [data, form.values.colors]) - - const renderFn = isCircularChart(type) ? renderCircularChart : renderGraph; - const chartOpts = isCircularChart(type) - ? { - queryResponse: data, - nameKey: visualization.circularChartConfig.nameKey, - valueKey: visualization.circularChartConfig.valueKey, - chart: type, - } - : {queryResponse: data, xKey: visualization.graphConfig.xAxis, yKeys: visualization.graphConfig.yAxis, chart: 'line-chart'}; return ( - - - {/* { - showWarning && - } */} - { - renderFn(chartOpts) - } - {/* */} - + + + {showWarning ? : } + ); }; @@ -161,6 +180,8 @@ const GraphConfig = (props: {form: TileFormType}) => { } const TickConfig = (props: {form: TileFormType}) => { + if (props.form.values.visualization.type === 'table') return null; + return ( @@ -207,19 +228,6 @@ const Config = (props: {form: TileFormType, updateColors: (key: string, value: s ); }; -const data = { - info: 789, - warn: 364, - error: 456, - debug: 123, - critical: 78, - notice: 567, - alert: 231, - emergency: 45, - trace: 890, - fatal: 132, -}; - const useVizForm = (opts: Visualization) => { const form = useForm({ mode: 'controlled', @@ -250,13 +258,11 @@ type VizFormReturnType = UseFormReturnType { const {form} = props; - // const {form, updateColors} = useVizForm({type: 'donut-chart', colors: {}}); const [vizEditorModalOpen] = useDashboardsStore((store) => store.vizEditorModalOpen); const [, setDashboardStore] = useDashboardsStore((_store) => null); const closeVizModal = useCallback(() => { setDashboardStore((store) => toggleVizEditorModal(store, false)); }, []); - const showWarning = _.isEmpty(data); return ( { +export const sanitiseSqlString = (sqlString: string, shouldNotify:boolean = true, limit: number = LOAD_LIMIT): string => { const withoutComments = sqlString.replace(/--.*$/gm, ''); const withoutNewLines = withoutComments.replace(/\n/g, ' '); const withoutTrailingSemicolon = withoutNewLines.replace(/;/g, '').trim(); @@ -10,14 +10,14 @@ export const sanitiseSqlString = (sqlString: string, shouldNotify:boolean = true if (limitRegex.test(withoutTrailingSemicolon)) { return withoutTrailingSemicolon.replace(limitRegex, (match, p1) => { const currentLimit = parseInt(p1, 10); - if (currentLimit > LOAD_LIMIT) { - shouldNotify && notify({ message: `Limit exceeds the default load limit. Replacing with default limit - ${LOAD_LIMIT}` }); - return `LIMIT ${LOAD_LIMIT}`; + if (currentLimit > limit) { + shouldNotify && notify({ message: `Limit exceeds the default load limit. Replacing with default limit - ${limit}` }); + return `LIMIT ${limit}`; } return match; }); } - shouldNotify && notify({ message: `Default limit used i.e - ${LOAD_LIMIT}` }); - return `${withoutTrailingSemicolon} LIMIT ${LOAD_LIMIT}`; + shouldNotify && notify({ message: `Default limit used i.e - ${limit}` }); + return `${withoutTrailingSemicolon} LIMIT ${limit}`; }; \ No newline at end of file From 41a0a9edfa1ce5cfc773d1700446d48cb74d3bd4 Mon Sep 17 00:00:00 2001 From: balaji-jr Date: Wed, 31 Jul 2024 09:49:55 +0530 Subject: [PATCH 08/34] added validation for create tile form --- src/@types/parseable/api/dashboards.ts | 1 + src/hooks/useDashboards.tsx | 2 +- src/pages/Dashboards/Charts.tsx | 4 +- src/pages/Dashboards/CreateDashboardModal.tsx | 2 - src/pages/Dashboards/CreateTileForm.tsx | 217 ++++++++++++++---- src/pages/Dashboards/Dashboard.tsx | 49 ++-- src/pages/Dashboards/Tile.tsx | 2 +- src/pages/Dashboards/VizEditorModal.tsx | 14 +- src/pages/Dashboards/index.tsx | 6 +- .../providers/DashboardsProvider.ts | 3 +- src/pages/Dashboards/styles/Form.module.css | 9 + .../Dashboards/styles/VizEditor.module.css | 2 +- 12 files changed, 226 insertions(+), 85 deletions(-) diff --git a/src/@types/parseable/api/dashboards.ts b/src/@types/parseable/api/dashboards.ts index b68b34bf..0c4e54cd 100644 --- a/src/@types/parseable/api/dashboards.ts +++ b/src/@types/parseable/api/dashboards.ts @@ -42,6 +42,7 @@ export interface FormOpts extends Omit { isQueryValidated: boolean; data: TileQueryResponse; visualization: Visualization; + dashboardId: string | null; } export type TileFormType = UseFormReturnType FormOpts>; diff --git a/src/hooks/useDashboards.tsx b/src/hooks/useDashboards.tsx index c6507447..042ecc86 100644 --- a/src/hooks/useDashboards.tsx +++ b/src/hooks/useDashboards.tsx @@ -102,8 +102,8 @@ export const useTileQuery = (opts: { onSuccess: (data: TileQueryResponse) => voi onSuccess(res.data); setFetchState({ isLoading: false, isError: false, isSuccess: true }); } catch (e) { - console.log(e); setFetchState({ isLoading: false, isError: true, isSuccess: false }); + notifyError({message: _.isString(e.response.data) ? e.response.data : ''}) } }, [onSuccess], diff --git a/src/pages/Dashboards/Charts.tsx b/src/pages/Dashboards/Charts.tsx index ae7bc324..77b1bd19 100644 --- a/src/pages/Dashboards/Charts.tsx +++ b/src/pages/Dashboards/Charts.tsx @@ -2,7 +2,7 @@ import { TileData, TileQueryResponse, TileRecord } from '@/@types/parseable/api/ import { AreaChart, BarChart, DonutChart, LineChart, PieChart } from '@mantine/charts'; import { Stack, Text } from '@mantine/core'; import _ from 'lodash'; -import { circularChartTypes } from './providers/DashboardsProvider'; +import { circularChartTypes, graphTypes } from './providers/DashboardsProvider'; import { IconAlertTriangle } from '@tabler/icons-react'; import classes from './styles/Charts.module.css' @@ -67,6 +67,7 @@ export type SeriesType = { }[]; export const isCircularChart = (viz: string) => _.includes(circularChartTypes, viz); +export const isGraph = (viz: string) => _.includes(graphTypes, viz); const invalidConfigMsg = "Invalid chart config" const noDataMsg = "No data available" @@ -194,7 +195,6 @@ const Line = (props: { data: TileData; dataKey: string; series: SeriesType }) => }; const Bar = (props: { data: TileData; dataKey: string; series: SeriesType }) => { - console.log(props, "bar bar") return ( ); diff --git a/src/pages/Dashboards/CreateDashboardModal.tsx b/src/pages/Dashboards/CreateDashboardModal.tsx index c999f836..e1a13314 100644 --- a/src/pages/Dashboards/CreateDashboardModal.tsx +++ b/src/pages/Dashboards/CreateDashboardModal.tsx @@ -107,8 +107,6 @@ const CreateDashboardModal = () => { } }, [form.values, editMode, createMode, timeRangeOptions]); - console.log() - return ( { + const { stream, dashboardId, isQueryValidated, data, visualization} = form.values; + const hasVizConfigErrors = _.some(_.keys(form.errors), key => _.startsWith(key, 'visualization.')); + // form.validateField('visualization') + + const hasNoData = _.isEmpty(data) || _.isEmpty(data.records); + if (_.isEmpty(dashboardId)) { + return selectDashboardWarningText; + } else if (_.isEmpty(stream)) { + return selectStreamWarningText; + } else if (configType === 'data' || configType === 'viz') { + if (!isQueryValidated) { + return validateQueryWarningText; + } + if (configType === 'viz') { + if (hasNoData) { + return noDataWarning; + } else if (hasVizConfigErrors || _.isEmpty(visualization)) { + return invalidVizConfig; + } else if (_.isEmpty(visualization.type)) { + return emptyVizWarning; + } + } + } else { + return null; + } + + return null; +} + const SectionHeader = (props: { title: string; actionBtnProps?: { label: string; onClick: () => void } }) => { const { title, actionBtnProps } = props; return ( {title} {actionBtnProps && ( - + // - + // )} ); @@ -52,13 +89,18 @@ const EmptyVizView = (props: { msg: string | null }) => { const openVizModal = useCallback(() => { setDashboardStore((store) => toggleVizEditorModal(store, true)); }, []); + + if (!_.includes([emptyVizWarning, invalidVizConfig], props.msg)) return + return ( {props.msg} - + {_.includes([emptyVizWarning, invalidVizConfig], props.msg) && ( + + )} ); }; @@ -66,19 +108,36 @@ const EmptyVizView = (props: { msg: string | null }) => { const VisPreview = (props: { form: TileFormType }) => { const { form: { - values: { isQueryValidated, stream, data }, + values: { visualization }, }, } = props; - const isValidStream = !_.isEmpty(stream); - const showWarning = (!isValidStream || !isQueryValidated) && _.isEmpty(data); - const hasNoViz = true; - const errorMsg = showWarning ? (!isValidStream ? selectStreamWarningText : validateQueryWarningText) : null; + const errorMsg = getErrorMsg(props.form, 'viz'); + + useEffect(() => { + props.form.validateField('visualization'); + }, [visualization]); + + const [, setDashbaordsStore] = useDashboardsStore((store) => null); + + const openVizModal = useCallback(() => setDashbaordsStore((store) => toggleVizEditorModal(store, true)), []); + const sectionHeaderProps = { + title: 'Visualization Preview', + ...(errorMsg ? {} : { actionBtnProps: { label: 'Edit', onClick: openVizModal } }), + }; + + console.log(sectionHeaderProps); return ( - + - - {showWarning ? : hasNoViz ? : null} + + {errorMsg ? ( + + ) : ( + + + + )} ); }; @@ -86,10 +145,9 @@ const VisPreview = (props: { form: TileFormType }) => { const DataPreview = (props: { form: TileFormType }) => { const { form: { - values: { isQueryValidated, stream, data }, + values: { data }, }, } = props; - const isValidStream = !_.isEmpty(stream); const containerRef = useRef(null); const [containerSize, setContainerSize] = useState({ height: 0, width: 0 }); useEffect(() => { @@ -100,15 +158,14 @@ const DataPreview = (props: { form: TileFormType }) => { }); } }, []); - // const showWarning = false; - const showWarning = (!isValidStream || !isQueryValidated) && _.isEmpty(data); - const errorMsg = showWarning ? (!isValidStream ? selectStreamWarningText : validateQueryWarningText) : null; + const errorMsg = getErrorMsg(props.form, 'data'); + return ( - {showWarning ? ( + {errorMsg ? ( ) : ( { stream: (val) => (_.isEmpty(val) ? 'Cannot be empty' : null), description: (val) => (_.isEmpty(val) ? 'Cannot be empty' : null), query: (val) => (_.isEmpty(val) ? 'Cannot be empty' : null), - isQueryValidated: (val) => (val ? null : 'Query not validated'), + isQueryValidated: (val) => (val === true ? null : 'Query not validated'), + dashboardId: (val) => (_.isEmpty(val) ? 'Cannot be empty' : null), + visualization: { + type: (val) => _.isEmpty(val) ? 'Cannot be empty' : null, + size: (val) => _.isEmpty(val) ? 'Cannot be empty' : null, + circularChartConfig: { + nameKey: (value, values, path) => { + const {visualization: {type, circularChartConfig}} = values; + if (isCircularChart(type)) { + const nameKey = _.get(circularChartConfig, 'nameKey', null); + return _.isEmpty(nameKey) ? 'Cannot be empty' : null + } + }, + valueKey: (value, values, path) => { + const {visualization: {type, circularChartConfig}} = values; + if (isCircularChart(type)) { + const valueKey = _.get(circularChartConfig, 'valueKey', null); + return _.isEmpty(valueKey) ? 'Cannot be empty' : null + } + }, + }, + graphConfig: { + xAxis: (value, values, path) => { + const {visualization: {type, graphConfig}} = values; + if (isGraph(type)) { + const xAxis = _.get(graphConfig, 'xAxis', null); + return _.isEmpty(xAxis) ? 'Cannot be empty' : null + } + }, + yAxis: (value, values, path) => { + const {visualization: {type, graphConfig}} = values; + if (isGraph(type)) { + const yAxis = _.get(graphConfig, 'yAxis', null); + return _.isEmpty(yAxis) ? 'Cannot be empty' : null + } + }, + } + } }, validateInputOnChange: true, validateInputOnBlur: true, @@ -171,14 +265,18 @@ const fetchStreamFields = async (stream: string, setFields: (fields: Field[]) => const Query = (props: { form: TileFormType; onChangeValue: (key: string, value: any) => void }) => { const { form: { - values: { stream, query, isQueryValidated }, + values: { stream, query, isQueryValidated, dashboardId }, }, onChangeValue, } = props; + const [llmActive] = useAppStore((store) => store.instanceConfig?.llmActive); const containerRef = useRef(null); const [fields, setFields] = useState([]); const [initialHeight, setInitialHeight] = useState(0); const isValidStream = !_.isEmpty(stream); + const [dashboards] = useDashboardsStore(store => store.dashboards); + const [timeRange] = useLogsStore(store => store.timeRange) + useEffect(() => { setFields([]); if (_.size(stream) > 0) { @@ -193,18 +291,24 @@ const Query = (props: { form: TileFormType; onChangeValue: (key: string, value: }, [isValidStream]); const onFetchTileSuccess = useCallback((data: TileQueryResponse) => { - onChangeValue('isQueryValidated', true); - onChangeValue('data', data); + props.form.setFieldValue('isQueryValidated', true) + props.form.setFieldValue('data', data) + props.form.validate() }, []); - const { fetchTileData, isLoading, isError, isSuccess } = useTileQuery({ onSuccess: onFetchTileSuccess }); + const { fetchTileData, isLoading } = useTileQuery({ onSuccess: onFetchTileSuccess }); const validateQuery = useCallback(() => { - const now = new Date(); - const santizedQuery = sanitiseSqlString(query); + if (_.isEmpty(dashboardId)) return; + + const selectedDashboard = _.find(dashboards, (dashboard) => dashboard.dashboard_id === dashboardId); + if (_.isEmpty(selectedDashboard)) return; + + const santizedQuery = sanitiseSqlString(query, true, 100); onChangeValue('query', santizedQuery); - fetchTileData({ query: santizedQuery, startTime: new Date(now.getTime() - 24 * 3 * 60 * 60 * 1000), endTime: now }); - }, [query]); + const { from, to } = selectedDashboard.time_filter || { from: timeRange.startTime, to: timeRange.endTime }; + fetchTileData({ query: santizedQuery, startTime: dayjs(from).toDate(), endTime: dayjs(to).toDate() }); + }, [query, dashboardId, dashboards]); const onEditorChange = useCallback((query: string | undefined) => { onChangeValue('query', query || ''); @@ -212,13 +316,15 @@ const Query = (props: { form: TileFormType; onChangeValue: (key: string, value: // onChangeValue('data', null) }, []); + const errorMsg = getErrorMsg(props.form, 'basic') + return ( Query - {!isValidStream && {selectStreamWarningText}} + {errorMsg && {errorMsg}} - {true ? ( + {llmActive ? ( Validate Query @@ -288,10 +395,15 @@ const Query = (props: { form: TileFormType; onChangeValue: (key: string, value: const Config = (props: { form: TileFormType; onChangeValue: (key: string, value: any) => void }) => { const { form, onChangeValue } = props; const [userSpecificStreams] = useAppStore((store) => store.userSpecificStreams); + const [dashboards] = useDashboardsStore(store => store.dashboards) const allStreams = useMemo( () => _.map(userSpecificStreams, (stream) => ({ label: stream.name, value: stream.name })), [userSpecificStreams], ); + const allDashboards = useMemo( + () => _.map(dashboards, (dashboard) => ({ label: dashboard.name, value: dashboard.dashboard_id })), + [dashboards], + ); return ( @@ -301,7 +413,15 @@ const Config = (props: { form: TileFormType; onChangeValue: (key: string, value: label="Name" key="name" {...form.getInputProps('name')} - style={{ width: '50%' }} + style={{ width: '33%' }} + /> + @@ -326,12 +446,14 @@ const Config = (props: { form: TileFormType; onChangeValue: (key: string, value: }; const defaultTileOpts = { - name: 'hello', - description: 'hello', - stream: 'teststream', + name: '', + description: '', + stream: '', isQueryValidated: false, - query: 'select * from teststream', + isVizValidated: false, + query: '', data: null, + dashboardId: null, visualization: { type: 'donut-chart', size: 'sm', @@ -351,15 +473,14 @@ const CreateTileForm = () => { }, []); return ( - + - Create Tile - - - - + + + Create Tile + + + {/* */} ); -} +}; const NoTilesView = () => { - const [, setDashbaordsStore] = useDashboardsStore(store => null); + const [, setDashbaordsStore] = useDashboardsStore((store) => null); const openCreateTileModal = useCallback(() => { - setDashbaordsStore(store => toggleCreateTileModal(store, true)) - }, []) + setDashbaordsStore((store) => toggleCreateTileModal(store, true)); + }, []); return ( - {/* */} Create Tile Title Placeholder - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore + magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco + - {/* */} ); -} +}; const Dashboard = () => { - const [dashboards] = useDashboardsStore(store => store.dashboards); - const [createTileFormOpen] = useDashboardsStore(store => store.createTileFormOpen) - - if (_.isEmpty(dashboards)) return ; + const [dashboards] = useDashboardsStore((store) => store.dashboards); + if (_.isEmpty(dashboards)) return ; return ( - {createTileFormOpen ? : } + ); }; diff --git a/src/pages/Dashboards/Tile.tsx b/src/pages/Dashboards/Tile.tsx index bba026b6..c1521efd 100644 --- a/src/pages/Dashboards/Tile.tsx +++ b/src/pages/Dashboards/Tile.tsx @@ -71,7 +71,7 @@ const Tile = (props: { id: string }) => { useEffect(() => { const now = new Date(); // debug // should notify - const santizedQuery = sanitiseSqlString(tile.query, false); + const santizedQuery = sanitiseSqlString(tile.query, false, 100); fetchTileData({ query: santizedQuery, startTime: new Date(now.getTime() - 24 * 3 * 60 * 60 * 1000), endTime: now }); }, []); diff --git a/src/pages/Dashboards/VizEditorModal.tsx b/src/pages/Dashboards/VizEditorModal.tsx index a43caf81..484f6a26 100644 --- a/src/pages/Dashboards/VizEditorModal.tsx +++ b/src/pages/Dashboards/VizEditorModal.tsx @@ -61,7 +61,7 @@ const Table = (props: {form: TileFormType }) => { ); }; -const Viz = (props: {form: TileFormType}) => { +export const Viz = (props: {form: TileFormType}) => { const {form: {values: {visualization, data}}} = props; const {type} = visualization; @@ -95,6 +95,10 @@ const sizeLabelMap = { } const BasicConfig = (props: {form: TileFormType}) => { + useEffect(() => { + props.form.validate(); + }, [props.form.values.visualization.type]) + return ( @@ -125,7 +129,7 @@ const BasicConfig = (props: {form: TileFormType}) => { } const CircularChartConfig = (props: {form: TileFormType}) => { - const {form: {values: {data}}} = props; + const {form: {values: {data}, errors}} = props; return ( @@ -180,7 +184,7 @@ const GraphConfig = (props: {form: TileFormType}) => { } const TickConfig = (props: {form: TileFormType}) => { - if (props.form.values.visualization.type === 'table') return null; + if (props.form.values.visualization.type === 'table' || _.isEmpty(props.form.values.visualization.type)) return null; return ( @@ -273,7 +277,9 @@ const VizEditorModal = (props: {form: TileFormType}) => { styles={{ body: { padding: '0 1rem' }, header: { padding: '1rem', paddingBottom: '0' } }} classNames={{ title: classes.modalTitle }}> - + + + diff --git a/src/pages/Dashboards/index.tsx b/src/pages/Dashboards/index.tsx index 8d1c2750..5599b4b9 100644 --- a/src/pages/Dashboards/index.tsx +++ b/src/pages/Dashboards/index.tsx @@ -7,6 +7,7 @@ import { useDashboardsStore } from './providers/DashboardsProvider'; import CreateDashboardModal from './CreateDashboardModal'; import { useEffect } from 'react'; import { useDashboardsQuery } from '@/hooks/useDashboards'; +import CreateTileForm from './CreateTileForm'; const LoadingView = () => { return ( @@ -17,7 +18,8 @@ const LoadingView = () => { } const Dashboards = () => { - const [dashboards] = useDashboardsStore(store => store.dashboards); + const [dashboards] = useDashboardsStore((store) => store.dashboards); + const [createTileFormOpen] = useDashboardsStore((store) => store.createTileFormOpen); const { fetchDashboards } = useDashboardsQuery(); useEffect(() => { fetchDashboards(); @@ -34,6 +36,8 @@ const Dashboards = () => { }}> {dashboards === null ? ( + ) : createTileFormOpen ? ( + ) : ( <> diff --git a/src/pages/Dashboards/providers/DashboardsProvider.ts b/src/pages/Dashboards/providers/DashboardsProvider.ts index 13301c86..6c9010c9 100644 --- a/src/pages/Dashboards/providers/DashboardsProvider.ts +++ b/src/pages/Dashboards/providers/DashboardsProvider.ts @@ -10,6 +10,7 @@ export type TileSize = (typeof tileSizes)[number]; // viz type constants export const visualizations = ['pie-chart', 'donut-chart', 'line-chart', 'bar-chart', 'area-chart', 'table'] as const; export const circularChartTypes = ['pie-chart', 'donut-chart']; +export const graphTypes = ['line-chart', 'bar-chart', 'area-chart'] // vize size constants export const tileSizeWidthMap = { sm: 4, md: 6, lg: 8, xl: 12 }; @@ -73,7 +74,7 @@ export type Visualization = { [key: string | number]: string; }; circularChartConfig: {} | { nameKey: string; valueKey: string }; - graphConfig: {} | {}; + graphConfig: {} | {xAxis: string; yAxis: string}; tableConfig: {} | {}; }; diff --git a/src/pages/Dashboards/styles/Form.module.css b/src/pages/Dashboards/styles/Form.module.css index 51cc68a1..6c9a023c 100644 --- a/src/pages/Dashboards/styles/Form.module.css +++ b/src/pages/Dashboards/styles/Form.module.css @@ -7,6 +7,10 @@ .sectionHeader { border-bottom: 1px solid var(--mantine-color-gray-2); padding: 0.75rem; + flex-direction: row; + align-items: center; + justify-content: space-between; + height: 3rem; } .configContainer { @@ -66,4 +70,9 @@ .warningIcon { color: var(--mantine-color-gray-5); +} + +.arrowLeftIcon { + color: var(--mantine-color-gray-7); + cursor: pointer; } \ No newline at end of file diff --git a/src/pages/Dashboards/styles/VizEditor.module.css b/src/pages/Dashboards/styles/VizEditor.module.css index 6e320197..642ee780 100644 --- a/src/pages/Dashboards/styles/VizEditor.module.css +++ b/src/pages/Dashboards/styles/VizEditor.module.css @@ -10,8 +10,8 @@ } .vizContainer { - width: 40%; justify-content: center; + height: 100%; } .configContainer { From 26481b291e02b25294b65f47b35fd1e6c4e4db0b Mon Sep 17 00:00:00 2001 From: balaji-jr Date: Fri, 2 Aug 2024 03:59:18 +0530 Subject: [PATCH 09/34] Added tile controls --- src/@types/parseable/api/dashboards.ts | 75 +++++--- src/pages/Dashboards/Charts.tsx | 50 +++-- src/pages/Dashboards/CreateTileForm.tsx | 176 ++++++++++++------ src/pages/Dashboards/Dashboard.tsx | 11 +- src/pages/Dashboards/Tile.tsx | 175 +++++++++++++++-- src/pages/Dashboards/Toolbar.tsx | 125 +++++++++++-- src/pages/Dashboards/VizEditorModal.tsx | 53 +++--- .../providers/DashboardsProvider.ts | 129 ++++++------- src/pages/Dashboards/styles/tile.module.css | 23 +++ .../Dashboards/styles/toolbar.module.css | 32 +++- src/pages/Stream/providers/LogsProvider.tsx | 2 +- src/utils/exportImage.ts | 35 ++-- 12 files changed, 642 insertions(+), 244 deletions(-) diff --git a/src/@types/parseable/api/dashboards.ts b/src/@types/parseable/api/dashboards.ts index 0c4e54cd..1709f5ce 100644 --- a/src/@types/parseable/api/dashboards.ts +++ b/src/@types/parseable/api/dashboards.ts @@ -1,29 +1,47 @@ -import { Tile, Visualization } from "@/pages/Dashboards/providers/DashboardsProvider"; import { UseFormReturnType } from "@mantine/form"; -export type CreateDashboardType = { - name: string; - description: string; - refresh_interval: number; - tiles: Tile[]; - time_filter: null | { - from: string; - to: string; - } +export type VizType = (typeof visualizations)[number]; +export type TileSize = (typeof tileSizes)[number]; + +// viz type constants +export const visualizations = ['pie-chart', 'donut-chart', 'line-chart', 'bar-chart', 'area-chart', 'table'] as const; +export const circularChartTypes = ['pie-chart', 'donut-chart']; +export const graphTypes = ['line-chart', 'bar-chart', 'area-chart'] + +// vize size constants +export const tileSizeWidthMap = { sm: 4, md: 6, lg: 8, xl: 12 }; +export const tileSizes = ['sm', 'md', 'lg', 'xl']; + +export type ColorConfig = { + field_name: string; + color_palette: string; } +export type Visualization = { + visualization_type: VizType; + size: TileSize; + circular_chart_config?: {} | { name_key: string; value_key: string }; + graph_config?: {} | {xAxis: string; yAxis: string}; + color_config: ColorConfig[]; +}; -export type UpdateDashboardType = { +export type Dashboard = { name: string; - description: string; - refresh_interval: number; - tiles: Tile[]; - dashboard_id: string; - time_filter: null | { - from: string; - to: string; - } -} + description: string; + refresh_interval: number; + tiles: Tile[]; + dashboard_id: string; + time_filter: null | { + from: string; + to: string; + }; +}; + +export type CreateDashboardType = Omit; + +export type UpdateDashboardType = Omit & { + tiles: EditTileType[]; +}; export type TileQuery = {query: string, startTime: Date, endTime: Date} @@ -38,11 +56,26 @@ export type TileQueryResponse = { records: TileData } -export interface FormOpts extends Omit { +export interface FormOpts extends Omit { isQueryValidated: boolean; data: TileQueryResponse; visualization: Visualization; dashboardId: string | null; + tile_id?: string; } export type TileFormType = UseFormReturnType FormOpts>; + +export type Tile = { + name: string; + description: string; + stream: string; + visualization: Visualization; + query: string; + tile_id: string; + order: number; +}; + +export type EditTileType = Omit & { + tile_id?: string; +}; \ No newline at end of file diff --git a/src/pages/Dashboards/Charts.tsx b/src/pages/Dashboards/Charts.tsx index 77b1bd19..3fb843bd 100644 --- a/src/pages/Dashboards/Charts.tsx +++ b/src/pages/Dashboards/Charts.tsx @@ -2,9 +2,11 @@ import { TileData, TileQueryResponse, TileRecord } from '@/@types/parseable/api/ import { AreaChart, BarChart, DonutChart, LineChart, PieChart } from '@mantine/charts'; import { Stack, Text } from '@mantine/core'; import _ from 'lodash'; -import { circularChartTypes, graphTypes } from './providers/DashboardsProvider'; +// import { circularChartTypes, graphTypes } from './providers/DashboardsProvider'; +import { circularChartTypes, graphTypes } from '@/@types/parseable/api/dashboards'; import { IconAlertTriangle } from '@tabler/icons-react'; import classes from './styles/Charts.module.css' +import { CodeHighlight } from '@mantine/code-highlight'; export const chartColorsMap = { 'black': 'dark.6', @@ -86,61 +88,75 @@ const validateCircularChartData = (data: CircularChartData) => { return _.every(data, d => _.isNumber(d.value)) } -export const renderCircularChart = (opts: {queryResponse: TileQueryResponse | null, nameKey: string, valueKey: string, chart: string }) => { - const { queryResponse, nameKey, valueKey, chart } = opts; +export const renderCircularChart = (opts: {queryResponse: TileQueryResponse | null, name_key: string, value_key: string, chart: string }) => { + const { queryResponse, name_key, value_key, chart } = opts; const VizComponent = getVizComponent(chart); - const data = makeCircularChartData(queryResponse?.records || [], nameKey, valueKey); + const data = makeCircularChartData(queryResponse?.records || [], name_key, value_key); - const isInvalidKey = _.isEmpty(nameKey) || _.isEmpty(valueKey); + const isInvalidKey = _.isEmpty(name_key) || _.isEmpty(value_key); const hasNoData = _.isEmpty(data); const isValidData = validateCircularChartData(data); const warningMsg = isInvalidKey ? invalidConfigMsg : hasNoData ? noDataMsg : !isValidData ? invalidDataMsg : null; return ( - + {warningMsg ? : VizComponent ? : null} ); } -export const renderGraph = (opts: {queryResponse: TileQueryResponse | null, xKey: string, yKeys: string[], chart}) => { - const { queryResponse, xKey, yKeys, chart } = opts; +export const renderJsonView = (opts: {queryResponse: TileQueryResponse | null}) => { + return ( + + + + ); +} + +export const renderGraph = (opts: {queryResponse: TileQueryResponse | null, x_key: string, y_key: string[], chart}) => { + const { queryResponse, x_key, y_key, chart } = opts; const VizComponent = getVizComponent(chart); - const seriesData = makeSeriesData(queryResponse?.records || [], yKeys); + const seriesData = makeSeriesData(queryResponse?.records || [], y_key); const data = queryResponse?.records || [] - const isInvalidKey = _.isEmpty(xKey) || _.isEmpty(yKeys); + const isInvalidKey = _.isEmpty(x_key) || _.isEmpty(y_key); const hasNoData = _.isEmpty(seriesData) || _.isEmpty(data); const warningMsg = isInvalidKey ? invalidConfigMsg : hasNoData ? noDataMsg : null; return ( - {warningMsg ? : VizComponent ? : null} + {warningMsg ? : VizComponent ? : null} ); // return ( // - // {VizComponent ? : null} + // {VizComponent ? : null} // // ); } -export const makeCircularChartData = (data: TileData | null, nameKey: string, valueKey: string): CircularChartData => { +export const makeCircularChartData = (data: TileData | null, name_key: string, value_key: string): CircularChartData => { if (!_.isArray(data)) return []; const topN = 5; const chartData = _.reduce( data, (acc, rec: TileRecord) => { - if (!_.has(rec, nameKey) || !_.has(rec, valueKey)) { + if (!_.has(rec, name_key) || !_.has(rec, value_key)) { return acc; } return { ...acc, - [rec[nameKey]]: rec[valueKey], + [rec[name_key]]: rec[value_key], }; }, {}, @@ -165,13 +181,13 @@ export const makeCircularChartData = (data: TileData | null, nameKey: string, va return [...topNArcs, ...(restArcValue !== 0 ? [{ name: 'Others', value: restArcValue, color: 'gray.6' }] : [])]; } -const makeSeriesData = (data: TileData | null, yKeys: string[]) => { +const makeSeriesData = (data: TileData | null, y_key: string[]) => { if (!_.isArray(data)) return []; let usedColors: string[] = []; return _.reduce( - yKeys, + y_key, (acc, key: string, index: number) => { const color = _.difference(colors, usedColors)[index] || nullColor; return [...acc, { color: chartColorsMap[color] || 'gray.6', name: key }]; diff --git a/src/pages/Dashboards/CreateTileForm.tsx b/src/pages/Dashboards/CreateTileForm.tsx index af83a8e5..d195e67b 100644 --- a/src/pages/Dashboards/CreateTileForm.tsx +++ b/src/pages/Dashboards/CreateTileForm.tsx @@ -1,7 +1,7 @@ import { Box, Button, Select, Stack, Text, TextInput } from '@mantine/core'; import classes from './styles/Form.module.css'; import { useForm } from '@mantine/form'; -import { useDashboardsStore, dashboardsStoreReducers } from './providers/DashboardsProvider'; +import { useDashboardsStore, dashboardsStoreReducers } from './providers/DashboardsProvider'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useAppStore } from '@/layouts/MainLayout/providers/AppProvider'; import _ from 'lodash'; @@ -11,8 +11,8 @@ import { Editor } from '@monaco-editor/react'; import VizEditorModal, { Viz } from './VizEditorModal'; import { SchemaList } from '../Stream/components/Querier/QueryCodeEditor'; import { IconAlertTriangle, IconArrowLeft, IconChartPie } from '@tabler/icons-react'; -import { useTileQuery } from '@/hooks/useDashboards'; -import { FormOpts, TileFormType, TileQueryResponse } from '@/@types/parseable/api/dashboards'; +import { useDashboardsQuery, useTileQuery } from '@/hooks/useDashboards'; +import { Dashboard, EditTileType, FormOpts, TileFormType, TileQueryResponse } from '@/@types/parseable/api/dashboards'; import { CodeHighlight } from '@mantine/code-highlight'; import { sanitiseSqlString } from '@/utils/sanitiseSqlString'; import { useLogsStore } from '../Stream/providers/LogsProvider'; @@ -20,7 +20,7 @@ import dayjs from 'dayjs'; import TimeRange from '@/components/Header/TimeRange'; import { isCircularChart, isGraph } from './Charts'; -const selectDashboardWarningText = 'Select a dashboard to continue' +const selectDashboardWarningText = 'Select a dashboard to continue'; const selectStreamWarningText = 'Select a stream to continue'; const validateQueryWarningText = 'Validate query to continue'; const emptyVizWarning = 'No visualization selected for the tile'; @@ -30,8 +30,8 @@ const invalidVizConfig = 'Invalid visualization config'; const { toggleVizEditorModal, toggleCreateTileModal } = dashboardsStoreReducers; const getErrorMsg = (form: TileFormType, configType: 'basic' | 'data' | 'viz'): string | null => { - const { stream, dashboardId, isQueryValidated, data, visualization} = form.values; - const hasVizConfigErrors = _.some(_.keys(form.errors), key => _.startsWith(key, 'visualization.')); + const { stream, dashboardId, isQueryValidated, data, visualization } = form.values; + const hasVizConfigErrors = _.some(_.keys(form.errors), (key) => _.startsWith(key, 'visualization.')); // form.validateField('visualization') const hasNoData = _.isEmpty(data) || _.isEmpty(data.records); @@ -48,7 +48,7 @@ const getErrorMsg = (form: TileFormType, configType: 'basic' | 'data' | 'viz'): return noDataWarning; } else if (hasVizConfigErrors || _.isEmpty(visualization)) { return invalidVizConfig; - } else if (_.isEmpty(visualization.type)) { + } else if (_.isEmpty(visualization.visualization_type)) { return emptyVizWarning; } } @@ -57,7 +57,7 @@ const getErrorMsg = (form: TileFormType, configType: 'basic' | 'data' | 'viz'): } return null; -} +}; const SectionHeader = (props: { title: string; actionBtnProps?: { label: string; onClick: () => void } }) => { const { title, actionBtnProps } = props; @@ -66,9 +66,9 @@ const SectionHeader = (props: { title: string; actionBtnProps?: { label: string; {title} {actionBtnProps && ( // - + // )} @@ -90,7 +90,7 @@ const EmptyVizView = (props: { msg: string | null }) => { setDashboardStore((store) => toggleVizEditorModal(store, true)); }, []); - if (!_.includes([emptyVizWarning, invalidVizConfig], props.msg)) return + if (!_.includes([emptyVizWarning, invalidVizConfig], props.msg)) return ; return ( @@ -145,7 +145,7 @@ const VisPreview = (props: { form: TileFormType }) => { const DataPreview = (props: { form: TileFormType }) => { const { form: { - values: { data }, + values: { data }, }, } = props; const containerRef = useRef(null); @@ -194,41 +194,49 @@ const useTileForm = (opts: FormOpts) => { isQueryValidated: (val) => (val === true ? null : 'Query not validated'), dashboardId: (val) => (_.isEmpty(val) ? 'Cannot be empty' : null), visualization: { - type: (val) => _.isEmpty(val) ? 'Cannot be empty' : null, - size: (val) => _.isEmpty(val) ? 'Cannot be empty' : null, - circularChartConfig: { - nameKey: (value, values, path) => { - const {visualization: {type, circularChartConfig}} = values; - if (isCircularChart(type)) { - const nameKey = _.get(circularChartConfig, 'nameKey', null); - return _.isEmpty(nameKey) ? 'Cannot be empty' : null + visualization_type: (val) => (_.isEmpty(val) ? 'Cannot be empty' : null), + size: (val) => (_.isEmpty(val) ? 'Cannot be empty' : null), + circular_chart_config: { + name_key: (_value, values, _path) => { + const { + visualization: { visualization_type, circular_chart_config }, + } = values; + if (isCircularChart(visualization_type)) { + const name_key = _.get(circular_chart_config, 'name_key', null); + return _.isEmpty(name_key) ? 'Cannot be empty' : null; } }, - valueKey: (value, values, path) => { - const {visualization: {type, circularChartConfig}} = values; - if (isCircularChart(type)) { - const valueKey = _.get(circularChartConfig, 'valueKey', null); - return _.isEmpty(valueKey) ? 'Cannot be empty' : null + value_key: (_value, values, _path) => { + const { + visualization: { visualization_type, circular_chart_config }, + } = values; + if (isCircularChart(visualization_type)) { + const value_key = _.get(circular_chart_config, 'value_key', null); + return _.isEmpty(value_key) ? 'Cannot be empty' : null; } }, }, - graphConfig: { - xAxis: (value, values, path) => { - const {visualization: {type, graphConfig}} = values; - if (isGraph(type)) { - const xAxis = _.get(graphConfig, 'xAxis', null); - return _.isEmpty(xAxis) ? 'Cannot be empty' : null + graph_config: { + xAxis: (_value, values, _path) => { + const { + visualization: { visualization_type, graph_config }, + } = values; + if (isGraph(visualization_type)) { + const xAxis = _.get(graph_config, 'xAxis', null); + return _.isEmpty(xAxis) ? 'Cannot be empty' : null; } }, - yAxis: (value, values, path) => { - const {visualization: {type, graphConfig}} = values; - if (isGraph(type)) { - const yAxis = _.get(graphConfig, 'yAxis', null); - return _.isEmpty(yAxis) ? 'Cannot be empty' : null + yAxis: (_value, values, _path) => { + const { + visualization: { visualization_type, graph_config }, + } = values; + if (isGraph(visualization_type)) { + const yAxis = _.get(graph_config, 'yAxis', null); + return _.isEmpty(yAxis) ? 'Cannot be empty' : null; } }, - } - } + }, + }, }, validateInputOnChange: true, validateInputOnBlur: true, @@ -274,8 +282,8 @@ const Query = (props: { form: TileFormType; onChangeValue: (key: string, value: const [fields, setFields] = useState([]); const [initialHeight, setInitialHeight] = useState(0); const isValidStream = !_.isEmpty(stream); - const [dashboards] = useDashboardsStore(store => store.dashboards); - const [timeRange] = useLogsStore(store => store.timeRange) + const [dashboards] = useDashboardsStore((store) => store.dashboards); + const [timeRange] = useLogsStore((store) => store.timeRange); useEffect(() => { setFields([]); @@ -291,9 +299,9 @@ const Query = (props: { form: TileFormType; onChangeValue: (key: string, value: }, [isValidStream]); const onFetchTileSuccess = useCallback((data: TileQueryResponse) => { - props.form.setFieldValue('isQueryValidated', true) - props.form.setFieldValue('data', data) - props.form.validate() + props.form.setFieldValue('isQueryValidated', true); + props.form.setFieldValue('data', data); + props.form.validate(); }, []); const { fetchTileData, isLoading } = useTileQuery({ onSuccess: onFetchTileSuccess }); @@ -316,7 +324,7 @@ const Query = (props: { form: TileFormType; onChangeValue: (key: string, value: // onChangeValue('data', null) }, []); - const errorMsg = getErrorMsg(props.form, 'basic') + const errorMsg = getErrorMsg(props.form, 'basic'); return ( @@ -395,7 +403,7 @@ const Query = (props: { form: TileFormType; onChangeValue: (key: string, value: const Config = (props: { form: TileFormType; onChangeValue: (key: string, value: any) => void }) => { const { form, onChangeValue } = props; const [userSpecificStreams] = useAppStore((store) => store.userSpecificStreams); - const [dashboards] = useDashboardsStore(store => store.dashboards) + const [dashboards] = useDashboardsStore((store) => store.dashboards); const allStreams = useMemo( () => _.map(userSpecificStreams, (stream) => ({ label: stream.name, value: stream.name })), [userSpecificStreams], @@ -452,37 +460,89 @@ const defaultTileOpts = { isQueryValidated: false, isVizValidated: false, query: '', - data: null, + data: { records: [], fields: [] }, dashboardId: null, visualization: { - type: 'donut-chart', + visualization_type: 'donut-chart' as 'donut-chart', size: 'sm', - colors: {}, - circularChartConfig: {}, - graphConfig: {}, - tableConfig: {}, + color_config: [], + circular_chart_config: {}, }, }; +const sanitizeFormValues = (form: TileFormType, type: 'create' | 'update', order: number): EditTileType => { + const { name, description, stream, query, visualization, tile_id } = form.values; + const { visualization_type, size, circular_chart_config } = visualization; + return { + name, + description, + stream, + query, + visualization: { + visualization_type, + size, + circular_chart_config, + color_config: [], + }, + order, + ...(type === 'create' && _.isString(tile_id) ? { tile_id } : {}), + }; +}; + +const genTileFormOpts = (opts: { activeDashboard: Dashboard | null }) => { + const { activeDashboard } = opts; + return { + name: '', + description: '', + stream: '', + isQueryValidated: false, + isVizValidated: false, + query: '', + data: { records: [], fields: [] }, + dashboardId: activeDashboard?.dashboard_id || null, + visualization: { + visualization_type: 'donut-chart' as 'donut-chart', + size: 'sm', + color_config: [], + circular_chart_config: {}, + }, + }; +}; + const CreateTileForm = () => { - const { form, onChangeValue } = useTileForm(defaultTileOpts); - const [, setDashbaordsStore] = useDashboardsStore((store) => null); + const [dashboards, setDashbaordsStore] = useDashboardsStore((store) => store.dashboards); + const [activeDashboard] = useDashboardsStore((store) => store.activeDashboard); + + const { form, onChangeValue } = useTileForm(genTileFormOpts({ activeDashboard })); - const closeEditForm = useCallback(() => { + const closeForm = useCallback(() => { setDashbaordsStore((store) => toggleCreateTileModal(store, false)); }, []); + const { updateDashboard, isUpdatingDashboard } = useDashboardsQuery(); + + const onCreate = useCallback(() => { + const { dashboardId } = form.values; + const dashboard = _.find(dashboards, (dashboard) => dashboard.dashboard_id === dashboardId); + + if (!dashboard) return; + + const existingTiles = dashboard.tiles; + const newTile = sanitizeFormValues(form, 'create', _.size(existingTiles) + 1); + updateDashboard({ dashboard: { ...dashboard, tiles: [...existingTiles, newTile] } }); + }, [form, dashboards]); + return ( - + Create Tile - + - diff --git a/src/pages/Dashboards/Dashboard.tsx b/src/pages/Dashboards/Dashboard.tsx index 58929dd9..89b73b3a 100644 --- a/src/pages/Dashboards/Dashboard.tsx +++ b/src/pages/Dashboards/Dashboard.tsx @@ -8,16 +8,17 @@ import { DASHBOARDS_SIDEBAR_WIDTH, NAVBAR_WIDTH } from '@/constants/theme'; import Tile from './Tile'; // import classes from './styles/tile.module.css'; import classes from './styles/Dashboard.module.css'; -import CreateTileForm from './CreateTileForm'; import { useDashboardsStore, dashboardsStoreReducers, genLayout } from './providers/DashboardsProvider'; import _ from 'lodash'; import { IconChartBar } from '@tabler/icons-react'; import { useCallback } from 'react'; +import { makeExportClassName } from '@/utils/exportImage'; const { toggleCreateDashboardModal, toggleCreateTileModal } = dashboardsStoreReducers; const TilesView = () => { const [activeDashboard] = useDashboardsStore((store) => store.activeDashboard); + const [allowDrag] = useDashboardsStore(store => store.allowDrag) const hasNoTiles = _.size(activeDashboard?.tiles) < 1; if (hasNoTiles || !activeDashboard) return ; @@ -26,7 +27,7 @@ const TilesView = () => { // debug - memo const layout = genLayout(tiles); - + console.log(layout, "given layout") return ( { margin={[16, 16]} containerPadding={[20, 10]} compactType="horizontal" - isDraggable={false}> + isDraggable={allowDrag} + onLayoutChange={(layout) => console.log(layout, "made layout")} + > {_.map(layout, (item) => { return (
+ className={`${classes.container} ${makeExportClassName(item.i)}`}>
); diff --git a/src/pages/Dashboards/Tile.tsx b/src/pages/Dashboards/Tile.tsx index c1521efd..136cb655 100644 --- a/src/pages/Dashboards/Tile.tsx +++ b/src/pages/Dashboards/Tile.tsx @@ -1,15 +1,17 @@ -import { Loader, px, Stack, Text } from '@mantine/core'; +import { Loader, px, Stack, Text, Menu, Button, rem, SegmentedControl, Modal, TextInput, Box } from '@mantine/core'; import classes from './styles/tile.module.css'; -import { IconAlertTriangle, IconDotsVertical } from '@tabler/icons-react'; -import charts, { getVizComponent, isCircularChart, renderCircularChart, renderGraph } from './Charts'; -import handleCapture from '@/utils/exportImage'; -import { Tile, useDashboardsStore } from './providers/DashboardsProvider'; +import { IconAlertTriangle, IconArrowsLeftRight, IconBraces, IconDotsVertical, IconFileSpreadsheet, IconGripVertical, IconJson, IconMessageCircle, IconPencil, IconPhoto, IconSearch, IconSettings, IconTable, IconTrash } from '@tabler/icons-react'; +import charts, { getVizComponent, isCircularChart, isGraph, renderCircularChart, renderGraph, renderJsonView } from './Charts'; +import handleCapture, { makeExportClassName } from '@/utils/exportImage'; +import { useDashboardsStore } from './providers/DashboardsProvider'; import _ from 'lodash'; import { useTileQuery } from '@/hooks/useDashboards'; import { useCallback, useEffect, useState } from 'react'; -import { TileQueryResponse } from '@/@types/parseable/api/dashboards'; +import { Tile, TileQueryResponse } from '@/@types/parseable/api/dashboards'; import { sanitiseSqlString } from '@/utils/sanitiseSqlString'; import Table from './Table'; +import { downloadDataAsCSV, downloadDataAsJson } from '@/utils/exportHelpers'; +import { makeExportData } from '../Stream/providers/LogsProvider'; const NoDataView = () => { return ( @@ -31,12 +33,13 @@ const LoadingView = () => { const CircularChart = (props: { tile: Tile; data: TileQueryResponse }) => { const { tile, data } = props; const { - visualization: { type, circularChartConfig }, + visualization: { visualization_type, circular_chart_config }, } = tile; - const { nameKey = '', valueKey = '' } = circularChartConfig; + const name_key = _.get(circular_chart_config, 'name_key', ''); + const value_key = _.get(circular_chart_config, 'value_key', ''); return ( - {renderCircularChart({ queryResponse: data, nameKey, valueKey, chart: type })} + {renderCircularChart({ queryResponse: data, name_key, value_key, chart: visualization_type })} ); }; @@ -44,22 +47,158 @@ const CircularChart = (props: { tile: Tile; data: TileQueryResponse }) => { const Graph = (props: { tile: Tile; data: TileQueryResponse }) => { const { tile, data } = props; const { - visualization: { type, graphConfig }, + visualization: { visualization_type, graph_config }, } = tile; - const { xKey, yKeys } = graphConfig; + const x_key = _.get(graph_config, 'x_key', ''); + const y_key = _.get(graph_config, 'y_key', []); return ( - {renderGraph({ queryResponse: data, xKey, yKeys, chart: type })} + {renderGraph({ queryResponse: data, x_key, y_key, chart: visualization_type })} ); }; +const JsonView = (props: { tile: Tile; data: TileQueryResponse }) => { + const { tile, data } = props; + return ( + {renderJsonView({ queryResponse: data })} + ); +}; + +const getViz = (vizType: string | null) => { + if (!vizType) return <>; + + if (vizType === 'table') { + return Table; + } else if (isCircularChart(vizType)) { + return CircularChart; + } else if (isGraph(vizType)) { + return Graph; + } else { + return <>; + } +}; + +const DeleteDashboardModal = () => { + const [activeDashboard, setDashbaordsStore] = useDashboardsStore((store) => store.activeDashboard); + const [deleteDashboardModalOpen] = useDashboardsStore((store) => store.deleteDashboardModalOpen); + const [confirmText, setConfirmText] = useState(''); + + const onChangeHandler = useCallback((e) => { + setConfirmText(e.target.value); + }, []); + + const onDelete = useCallback(() => {}, [activeDashboard?.dashboard_id]); + + if (!activeDashboard?.dashboard_id) return null; + + return ( + {}} + size="auto" + centered + styles={{ + body: { padding: '0 1rem 1rem 1rem', width: 400 }, + header: { padding: '1rem', paddingBottom: '0.4rem' }, + }} + title={Delete Tile}> + + + + Are you sure want to delete this dashboard and its contents ? + + + + + + + + + + + + + + ); +}; + +function TileControls(props: { tile: Tile; data: TileQueryResponse }) { + const { + tile: { name, tile_id }, + data = {}, + } = props; + const { records = [], fields = [] } = data; + const [allowDrag] = useDashboardsStore((store) => store.allowDrag); + + const exportPng = useCallback(() => { + handleCapture({ className: makeExportClassName(tile_id), fileName: name }); + }, []); + + const exportCSV = useCallback(() => { + downloadDataAsCSV(makeExportData(records, fields, 'CSV'), name); + }, [props.data]); + + const exportJson = useCallback(() => { + downloadDataAsJson(makeExportData(records, fields, 'JSON'), name); + }, [props.data]); + + if (allowDrag) return ; + + return ( + + + + + + Actions + }> + Edit + + }> + Delete + + + + Exports + }> + PNG + + }> + CSV + + }> + JSON + + + + ); +} + const Tile = (props: { id: string }) => { const [tileData, setTileData] = useState(); + const [showJson, setShowJson] = useState(false) const [activeDashboard] = useDashboardsStore((store) => store.activeDashboard); const tile = _.chain(activeDashboard) .get('tiles', []) - .find((tile) => tile.id === props.id) + .find((tile) => tile.tile_id === props.id) .value(); const onQuerySuccess = useCallback((data: TileQueryResponse) => { @@ -75,18 +214,22 @@ const Tile = (props: { id: string }) => { fetchTileData({ query: santizedQuery, startTime: new Date(now.getTime() - 24 * 3 * 60 * 60 * 1000), endTime: now }); }, []); + const toggleJsonView = useCallback(() => { + return setShowJson(prev => !prev) + }, []) const hasData = !_.isEmpty(tileData); - const Viz = tile.visualization.type === 'table' ? Table : isCircularChart(tile.visualization.type) ? CircularChart : Graph; + const vizType = _.get(tile, 'visualization.visualization_type', null); + const Viz = showJson ? JsonView : getViz(vizType); return ( - + {tile.name} {tile.description} - + {isLoading && } {!hasData && !isLoading && } diff --git a/src/pages/Dashboards/Toolbar.tsx b/src/pages/Dashboards/Toolbar.tsx index 9a73a2d0..bf584f71 100644 --- a/src/pages/Dashboards/Toolbar.tsx +++ b/src/pages/Dashboards/Toolbar.tsx @@ -1,53 +1,146 @@ import TimeRange from '@/components/Header/TimeRange'; -import { Box, Button, Stack, Text } from '@mantine/core'; -import { IconPencil } from '@tabler/icons-react'; +import { Box, Button, Modal, px, Stack, Text, TextInput } from '@mantine/core'; +import { IconCheck, IconPencil, IconPlus, IconTrash } from '@tabler/icons-react'; import classes from './styles/toolbar.module.css'; import { useDashboardsStore, dashboardsStoreReducers } from './providers/DashboardsProvider'; -import { useCallback } from 'react'; +import { useCallback, useState } from 'react'; +import IconButton from '@/components/Button/IconButton'; -const {toggleEditDashboardModal} = dashboardsStoreReducers; +const { toggleEditDashboardModal, toggleAllowDrag, toggleCreateTileModal, toggleDeleteDashboardModal } = dashboardsStoreReducers; const EditLayoutButton = () => { - const [createTileFormOpen] = useDashboardsStore(store => store.createTileFormOpen) + const [allowDrag, setDashbaordsStore] = useDashboardsStore((store) => store.allowDrag); + + const onClick = useCallback(() => { + setDashbaordsStore(toggleAllowDrag); + }, []); + + return ( + + + + ); +}; + +const AddTileButton = () => { + const [, setDashbaordsStore] = useDashboardsStore((_store) => null); + + const onClick = useCallback(() => { + setDashbaordsStore((store) => toggleCreateTileModal(store, true)); + }, []); + return ( - + - + + ); +}; + +const DeleteDashboardModal = () => { + const [activeDashboard, setDashbaordsStore] = useDashboardsStore((store) => store.activeDashboard); + const [deleteDashboardModalOpen] = useDashboardsStore((store) => store.deleteDashboardModalOpen); + const [confirmText, setConfirmText] = useState(''); + + const closeModal = useCallback(() => { + setDashbaordsStore((store) => toggleDeleteDashboardModal(store, false)); + }, []); + + const onChangeHandler = useCallback((e) => { + setConfirmText(e.target.value); + }, []); + + const onDelete = useCallback(() => {}, [activeDashboard?.dashboard_id]); + + if (!activeDashboard?.dashboard_id) return null; + + return ( + Delete Dashboard
}> + + + + Are you sure want to delete this dashboard and its contents ? + + + + + + + + + + + + +
); }; +const renderDeleteIcon = () => ; + +const DeleteDashboardButton = () => { + const [_store, setDashbaordsStore] = useDashboardsStore((_store) => null); + const onClick = useCallback(() => setDashbaordsStore(store => toggleDeleteDashboardModal(store, true)), []); + return ; +}; + const Toolbar = () => { const [activeDashboard, setDashbaordsStore] = useDashboardsStore((store) => store.activeDashboard); const openEditDashboardModal = useCallback(() => { - setDashbaordsStore((store) => toggleEditDashboardModal(store, true)) - }, []) + setDashbaordsStore((store) => toggleEditDashboardModal(store, true)); + }, []); if (!activeDashboard) return null; - const {name, description} = activeDashboard; + const { name, description } = activeDashboard; return ( + - {name} + + {name} + - {description} + + {description} + + + ); diff --git a/src/pages/Dashboards/VizEditorModal.tsx b/src/pages/Dashboards/VizEditorModal.tsx index 484f6a26..2f0f8d4c 100644 --- a/src/pages/Dashboards/VizEditorModal.tsx +++ b/src/pages/Dashboards/VizEditorModal.tsx @@ -1,13 +1,12 @@ import { Modal, MultiSelect, Select, Stack, Text, TextInput } from '@mantine/core'; import classes from './styles/VizEditor.module.css'; -import { useDashboardsStore, Visualization, dashboardsStoreReducers, tileSizes, circularChartTypes } from './providers/DashboardsProvider'; +import { useDashboardsStore, dashboardsStoreReducers } from './providers/DashboardsProvider'; import { useForm, UseFormReturnType } from '@mantine/form'; import { useCallback, useEffect, useState } from 'react'; -import { visualizations } from './providers/DashboardsProvider'; import _ from 'lodash'; import { colors as defaultColors, DonutData, isCircularChart, renderCircularChart, renderGraph, sanitizeDonutData, sanitizeDonutData2 } from './Charts'; import charts from './Charts'; -import { FormOpts, TileFormType, TileQuery, TileQueryResponse } from '@/@types/parseable/api/dashboards'; +import { FormOpts, TileFormType, TileQuery, TileQueryResponse, tileSizes, Visualization, visualizations } from '@/@types/parseable/api/dashboards'; import { IconAlertTriangle } from '@tabler/icons-react'; import TableViz from './Table'; const {toggleVizEditorModal} = dashboardsStoreReducers; @@ -25,28 +24,28 @@ const WarningView = (props: { msg: string | null }) => { const CircularChart = (props: { form: TileFormType }) => { const { - visualization: { type, circularChartConfig }, + visualization: { visualization_type, circular_chart_config }, data, } = props.form.values; - const nameKey = _.get(circularChartConfig, 'nameKey', ''); - const valueKey = _.get(circularChartConfig, 'valueKey', ''); + const name_key = _.get(circular_chart_config, 'name_key', ''); + const value_key = _.get(circular_chart_config, 'value_key', ''); return ( - {renderCircularChart({ queryResponse: data, nameKey, valueKey, chart: type })} + {renderCircularChart({ queryResponse: data, name_key, value_key, chart: visualization_type })} ); }; const Graph = (props: { form: TileFormType }) => { const { - visualization: { type, graphConfig }, + visualization: { visualization_type, graph_config }, data, } = props.form.values; - const xKey = _.get(graphConfig, 'xAxis', ''); - const yKeys = _.get(graphConfig, 'yAxis', []); + const x_key = _.get(graph_config, 'xAxis', ''); + const y_key = _.get(graph_config, 'yAxis', []); return ( - {renderGraph({ queryResponse: data, xKey, yKeys, chart: type })} + {renderGraph({ queryResponse: data, x_key, y_key, chart: visualization_type })} ); }; @@ -64,10 +63,10 @@ const Table = (props: {form: TileFormType }) => { export const Viz = (props: {form: TileFormType}) => { const {form: {values: {visualization, data}}} = props; - const {type} = visualization; - const isValidVizType = _.includes(visualizations, type); + const {visualization_type} = visualization; + const isValidVizType = _.includes(visualizations, visualization_type); const showWarning = !isValidVizType; - const Viss = type === 'table' ? Table : isCircularChart(type) ? CircularChart : Graph; + const Viss = visualization_type === 'table' ? Table : isCircularChart(visualization_type) ? CircularChart : Graph; return ( @@ -97,7 +96,7 @@ const sizeLabelMap = { const BasicConfig = (props: {form: TileFormType}) => { useEffect(() => { props.form.validate(); - }, [props.form.values.visualization.type]) + }, [props.form.values.visualization.visualization_type]); return ( @@ -110,8 +109,8 @@ const BasicConfig = (props: {form: TileFormType}) => { classNames={{ label: classes.fieldTitle }} label="Type" placeholder="Type" - key="visualization.type" - {...props.form.getInputProps('visualization.type')} + key="visualization.visualization_type" + {...props.form.getInputProps('visualization.visualization_type')} style={{ width: '50%' }} /> { classNames={{ label: classes.fieldTitle }} label="Value" placeholder="Value" - key="visualization.circularChartConfig.valueKey" - {...props.form.getInputProps('visualization.circularChartConfig.valueKey')} + key="visualization.circular_chart_config.value_key" + {...props.form.getInputProps('visualization.circular_chart_config.value_key')} style={{ width: '50%' }} /> @@ -165,8 +164,8 @@ const GraphConfig = (props: {form: TileFormType}) => { classNames={{ label: classes.fieldTitle }} label="X Axis" placeholder="X Axis" - key="visualization.graphConfig.xAxis" - {...props.form.getInputProps('visualization.graphConfig.xAxis')} + key="visualization.graph_config.xAxis" + {...props.form.getInputProps('visualization.graph_config.xAxis')} style={{ width: '50%' }} /> { classNames={{ label: classes.fieldTitle }} label="Y Axis" placeholder="Y Axis" - key="visualization.graphConfig.yAxis" - {...props.form.getInputProps('visualization.graphConfig.yAxis')} + key="visualization.graph_config.yAxis" + {...props.form.getInputProps('visualization.graph_config.yAxis')} style={{ width: '50%' }} limit={6} /> @@ -184,7 +183,7 @@ const GraphConfig = (props: {form: TileFormType}) => { } const TickConfig = (props: {form: TileFormType}) => { - if (props.form.values.visualization.type === 'table' || _.isEmpty(props.form.values.visualization.type)) return null; + if (props.form.values.visualization.visualization_type === 'table' || _.isEmpty(props.form.values.visualization.visualization_type)) return null; return ( @@ -192,7 +191,7 @@ const TickConfig = (props: {form: TileFormType}) => { Chart Config
{/* debug */} - {isCircularChart(props.form.values.visualization.type) ? ( + {isCircularChart(props.form.values.visualization.visualization_type) ? ( ) : ( diff --git a/src/pages/Dashboards/providers/DashboardsProvider.ts b/src/pages/Dashboards/providers/DashboardsProvider.ts index 6c9010c9..6f8dad1b 100644 --- a/src/pages/Dashboards/providers/DashboardsProvider.ts +++ b/src/pages/Dashboards/providers/DashboardsProvider.ts @@ -1,29 +1,15 @@ +import { Dashboard, Tile, tileSizeWidthMap } from '@/@types/parseable/api/dashboards'; import initContext from '@/utils/initContext'; -import { AxiosResponse } from 'axios'; import _ from 'lodash'; import { Layout } from 'react-grid-layout'; -export type VizType = (typeof visualizations)[number]; -export type TileSize = (typeof tileSizes)[number]; -// export type Layout = {i: string, x: number, y: number, h: number, minH: number}[] - -// viz type constants -export const visualizations = ['pie-chart', 'donut-chart', 'line-chart', 'bar-chart', 'area-chart', 'table'] as const; -export const circularChartTypes = ['pie-chart', 'donut-chart']; -export const graphTypes = ['line-chart', 'bar-chart', 'area-chart'] - -// vize size constants -export const tileSizeWidthMap = { sm: 4, md: 6, lg: 8, xl: 12 }; -export const tileSizes = ['sm', 'md', 'lg', 'xl']; - export const genLayout = (tiles: Tile[]): Layout => { - // { i: 'a', x: 0, y: 0, w: 4, h: 1, minH: 1 }, return _.reduce( tiles, (acc, tile) => { const { visualization: { size }, - id, + tile_id, } = tile; const tileWidth = _.get(tileSizeWidthMap, size, 4); @@ -32,7 +18,7 @@ export const genLayout = (tiles: Tile[]): Layout => { if (_.isEmpty(acc) || !prevItem) { return [ { - i: id, + i: tile_id, x: 0, y: 0, w: tileWidth, @@ -45,12 +31,12 @@ export const genLayout = (tiles: Tile[]): Layout => { const possibleX = prevX + prevItemWidth; if (possibleX + tileWidth <= 12) { - return [...acc, { i: id, x: possibleX, y: prevY, w: tileWidth, h: 1, minH: 1 }]; + return [...acc, { i: tile_id, x: possibleX, y: prevY, w: tileWidth, h: 1, minH: 1 }]; } else { return [ ...acc, { - i: id, + i: tile_id, x: 0, y: prevY + 1, w: tileWidth, @@ -65,47 +51,15 @@ export const genLayout = (tiles: Tile[]): Layout => { ); }; -export type Visualization = { - type: VizType; - size: TileSize; - colors: - | {} - | { - [key: string | number]: string; - }; - circularChartConfig: {} | { nameKey: string; valueKey: string }; - graphConfig: {} | {xAxis: string; yAxis: string}; - tableConfig: {} | {}; -}; - -export type Tile = { - name: string; - id: string; - description: string; - stream: string; - visualization: Visualization; - query: string; -}; - -export type Dashboard = { - name: string; - description: string; - tiles: Tile[]; - // pinned: boolean; - dashboard_id: string; - time_filter: null | { - from: string, - to: string - } -}; - type DashboardsStore = { dashboards: Dashboard[] | null; activeDashboard: Dashboard | null; createDashboardModalOpen: boolean; editDashboardModalOpen: boolean; + deleteDashboardModalOpen: boolean; createTileFormOpen: boolean; vizEditorModalOpen: boolean; + allowDrag: boolean; }; const mockDashboards = [ @@ -122,9 +76,9 @@ const mockDashboards = [ query: 'SELECT level, COUNT(*) AS level_count FROM teststream GROUP BY level;', visualization: { type: 'donut-chart' as 'donut-chart', - circularChartConfig: { - nameKey: 'level', - valueKey: 'level_count', + circular_chart_config: { + name_key: 'level', + value_key: 'level_count', }, size: 'sm', }, @@ -138,9 +92,9 @@ const mockDashboards = [ "SELECT message, SUM(CASE WHEN level = 'info' THEN 1 ELSE 0 END) AS info, SUM(CASE WHEN level = 'warn' THEN 1 ELSE 0 END) AS warn, SUM(CASE WHEN level = 'error' THEN 1 ELSE 0 END) AS error FROM teststream GROUP BY message;", visualization: { type: 'bar-chart' as 'donut-chart', - graphConfig: { - xKey: 'message', - yKeys: ['info', 'warn', 'error'], + graph_config: { + x_key: 'message', + y_key: ['info', 'warn', 'error'], }, size: 'lg', }, @@ -153,9 +107,9 @@ const mockDashboards = [ query: 'SELECT level, COUNT(*) AS level_count FROM teststream GROUP BY level;', visualization: { type: 'pie-chart' as 'donut-chart', - circularChartConfig: { - nameKey: 'level', - valueKey: 'level_count', + circular_chart_config: { + name_key: 'level', + value_key: 'level_count', }, size: 'sm', }, @@ -168,9 +122,9 @@ const mockDashboards = [ query: 'SELECT device_id, host, level, message, app_meta FROM teststream;', visualization: { type: 'table' as 'line-chart', - graphConfig: { - xKey: 'level', - yKeys: ['level_count'], + graph_config: { + x_key: 'level', + y_key: ['level_count'], }, size: 'lg', }, @@ -281,8 +235,10 @@ const initialState: DashboardsStore = { activeDashboard: null, createDashboardModalOpen: false, editDashboardModalOpen: false, + deleteDashboardModalOpen: false, createTileFormOpen: false, vizEditorModalOpen: false, + allowDrag: false, }; type ReducerOutput = Partial; @@ -294,6 +250,8 @@ type DashboardsStoreReducers = { selectDashboard: (store: DashboardsStore, dashboardId: string) => ReducerOutput; toggleCreateTileModal: (store: DashboardsStore, val: boolean) => ReducerOutput; toggleVizEditorModal: (store: DashboardsStore, val: boolean) => ReducerOutput; + toggleAllowDrag: (store: DashboardsStore) => ReducerOutput; + toggleDeleteDashboardModal: (store: DashboardsStore, val: boolean) => ReducerOutput; }; const toggleCreateDashboardModal = (_store: DashboardsStore, val: boolean) => { @@ -320,6 +278,20 @@ const toggleVizEditorModal = (_store: DashboardsStore, val: boolean) => { }; }; + +const toggleDeleteDashboardModal = (_store: DashboardsStore, val: boolean) => { + return { + deleteDashboardModalOpen: val, + }; +}; + + +const toggleAllowDrag = (store: DashboardsStore) => { + return { + allowDrag: !store.allowDrag + }; +}; + const setDashboards = (store: DashboardsStore, dashboards: Dashboard[]) => { const { activeDashboard: activeDashboardFromStore } = store; const defaultActiveDashboard = _.isArray(dashboards) && !_.isEmpty(dashboards) ? dashboards[0] : null; @@ -334,11 +306,24 @@ const setDashboards = (store: DashboardsStore, dashboards: Dashboard[]) => { })(); return { dashboards, - activeDashboard: activeDashboard - ? activeDashboard - : _.isArray(dashboards) && !_.isEmpty(dashboards) - ? dashboards[0] - : null, + activeDashboard: {...activeDashboard, tiles: [{ + "name": "asas", + "tile_id": "juiii", + "description": "asasa", + "stream": "teststream", + "query": "select * from teststream LIMIT 10", + "order": null, + "visualization": { + "visualization_type": "donut-chart", + "circular_chart_config": { + "name_key": "app_meta", + "value_key": "device_id" + }, + "graph_config": null, + "size": "sm", + "color_config": [] + } + }]}, }; }; @@ -359,7 +344,9 @@ const dashboardsStoreReducers: DashboardsStoreReducers = { selectDashboard, toggleCreateTileModal, toggleVizEditorModal, - toggleEditDashboardModal + toggleEditDashboardModal, + toggleAllowDrag, + toggleDeleteDashboardModal }; export { DashbaordsProvider, useDashboardsStore, dashboardsStoreReducers }; diff --git a/src/pages/Dashboards/styles/tile.module.css b/src/pages/Dashboards/styles/tile.module.css index a1d7ef37..62ead4c9 100644 --- a/src/pages/Dashboards/styles/tile.module.css +++ b/src/pages/Dashboards/styles/tile.module.css @@ -3,6 +3,7 @@ border: 1px solid var(--mantine-color-gray-2); height: 100%; border-radius: 0.4rem; + overflow: hidden; } .tileHeader { @@ -28,6 +29,7 @@ flex: 1; justify-content: center; align-items: center; + overflow: hidden; } .warningText { @@ -37,4 +39,25 @@ .warningIcon { color: var(--mantine-color-gray-5); +} + +.tileControlIcon { + color: var(--mantine-color-gray-7); + cursor: pointer; +} + +.tileCtrlItemIcon { + color: var(--mantine-color-gray-7); +} + +.tileCtrlItemText { + font-size: 0.75rem; + color: var(--mantine-color-gray-7); + font-weight: 500; +} + +.tileCtrlLabel { + color: var(--mantine-color-gray-5); + font-size: 0.7rem; + padding: 0.25rem 0.25rem; } \ No newline at end of file diff --git a/src/pages/Dashboards/styles/toolbar.module.css b/src/pages/Dashboards/styles/toolbar.module.css index 9744ab20..397eabfe 100644 --- a/src/pages/Dashboards/styles/toolbar.module.css +++ b/src/pages/Dashboards/styles/toolbar.module.css @@ -1,5 +1,4 @@ .editLayoutBtn { - background-color: white; color: var(--mantine-color-gray-7); border: 1px #e9ecef solid; border-radius: rem(8px); @@ -7,9 +6,34 @@ &:hover { color: black; + background-color: white; + } + + &.active { + color: white; + + &:hover { + background-color: var(--mantine-color-brandPrimary-4); + color: white; + } } } +.addTileBtn { + background-color: white; + color: var(--mantine-color-gray-7); + border: 1px #e9ecef solid; + border-radius: rem(8px); + font-size: 0.65rem; + &:hover { + color: black; + } + } + + .addTileBtn:hover { + background-color: #E0E0E0; + } + .editLayoutBtn:hover { background-color: #E0E0E0; } @@ -34,7 +58,13 @@ cursor: pointer; border-radius: 50%; margin-left: 0.2rem; + &:hover { background-color: var(--mantine-color-gray-1); } +} + +.deleteWarningText { + font-size: 0.7rem; + color: var(--mantine-color-gray-6); } \ No newline at end of file diff --git a/src/pages/Stream/providers/LogsProvider.tsx b/src/pages/Stream/providers/LogsProvider.tsx index ac508cda..97d9e0ce 100644 --- a/src/pages/Stream/providers/LogsProvider.tsx +++ b/src/pages/Stream/providers/LogsProvider.tsx @@ -742,7 +742,7 @@ const getUniqueValues = (data: Log[], key: string) => { .value(); }; -const makeExportData = (data: Log[], headers: string[], type: string): Log[] => { +export const makeExportData = (data: Log[], headers: string[], type: string): Log[] => { if (type === 'JSON') { return data; } else if (type === 'CSV') { diff --git a/src/utils/exportImage.ts b/src/utils/exportImage.ts index 2edc2736..95592e75 100644 --- a/src/utils/exportImage.ts +++ b/src/utils/exportImage.ts @@ -1,16 +1,27 @@ import html2canvas from 'html2canvas'; +import _ from 'lodash'; -const handleCapture = () => { - const element = document.querySelector('.capture-class'); - if (element) { - html2canvas(element).then(canvas => { - const imgData = canvas.toDataURL('image/png'); - const link = document.createElement('a'); - link.href = imgData; - link.download = 'capture.png'; - link.click(); - }); - } - }; +export const makeExportClassName = (name: string) => { + const sanitizedName = _.replace(name, /\./g, '-'); + return `png-capture-${sanitizedName}` +} + +const handleCapture = (opts: { className: string, fileName: string }) => { + const { className, fileName = 'png-export' } = opts; + try { + const element = document.querySelector(`.${className}`); + if (element) { + html2canvas(element).then((canvas) => { + const imgData = canvas.toDataURL('image/png'); + const link = document.createElement('a'); + link.href = imgData; + link.download = fileName; + link.click(); + }); + } + } catch (e) { + console.log('Unable to capture image', e); + } +}; export default handleCapture; \ No newline at end of file From d7d8a4bcaf328777e1e6b3903096b8649e9ff2a6 Mon Sep 17 00:00:00 2001 From: balaji-jr Date: Fri, 2 Aug 2024 13:53:21 +0530 Subject: [PATCH 10/34] restore form context for editing tile --- src/pages/Dashboards/CreateTileForm.tsx | 10 +++++-- src/pages/Dashboards/Dashboard.tsx | 2 +- src/pages/Dashboards/Tile.tsx | 12 ++++---- .../providers/DashboardsProvider.ts | 30 +++++++------------ ...rd.module.css => DashboardView.module.css} | 0 5 files changed, 24 insertions(+), 30 deletions(-) rename src/pages/Dashboards/styles/{dashboard.module.css => DashboardView.module.css} (100%) diff --git a/src/pages/Dashboards/CreateTileForm.tsx b/src/pages/Dashboards/CreateTileForm.tsx index d195e67b..0e0f7fd3 100644 --- a/src/pages/Dashboards/CreateTileForm.tsx +++ b/src/pages/Dashboards/CreateTileForm.tsx @@ -489,8 +489,11 @@ const sanitizeFormValues = (form: TileFormType, type: 'create' | 'update', order }; }; -const genTileFormOpts = (opts: { activeDashboard: Dashboard | null }) => { - const { activeDashboard } = opts; +const genTileFormOpts = (opts: { activeDashboard: Dashboard | null, editTileId: string | null }) => { + const { activeDashboard, editTileId } = opts; + + const currentTile = _.find(activeDashboard?.tiles, ) + return { name: '', description: '', @@ -512,8 +515,9 @@ const genTileFormOpts = (opts: { activeDashboard: Dashboard | null }) => { const CreateTileForm = () => { const [dashboards, setDashbaordsStore] = useDashboardsStore((store) => store.dashboards); const [activeDashboard] = useDashboardsStore((store) => store.activeDashboard); + const [editTileId] = useDashboardsStore((store) => store.editTileId); - const { form, onChangeValue } = useTileForm(genTileFormOpts({ activeDashboard })); + const { form, onChangeValue } = useTileForm(genTileFormOpts({ activeDashboard, editTileId })); const closeForm = useCallback(() => { setDashbaordsStore((store) => toggleCreateTileModal(store, false)); diff --git a/src/pages/Dashboards/Dashboard.tsx b/src/pages/Dashboards/Dashboard.tsx index 89b73b3a..ab1ed7cb 100644 --- a/src/pages/Dashboards/Dashboard.tsx +++ b/src/pages/Dashboards/Dashboard.tsx @@ -7,7 +7,7 @@ import GridLayout from 'react-grid-layout'; import { DASHBOARDS_SIDEBAR_WIDTH, NAVBAR_WIDTH } from '@/constants/theme'; import Tile from './Tile'; // import classes from './styles/tile.module.css'; -import classes from './styles/Dashboard.module.css'; +import classes from './styles/DashboardView.module.css'; import { useDashboardsStore, dashboardsStoreReducers, genLayout } from './providers/DashboardsProvider'; import _ from 'lodash'; import { IconChartBar } from '@tabler/icons-react'; diff --git a/src/pages/Dashboards/Tile.tsx b/src/pages/Dashboards/Tile.tsx index 136cb655..dc1bb7b5 100644 --- a/src/pages/Dashboards/Tile.tsx +++ b/src/pages/Dashboards/Tile.tsx @@ -11,7 +11,7 @@ import { Tile, TileQueryResponse } from '@/@types/parseable/api/dashboards'; import { sanitiseSqlString } from '@/utils/sanitiseSqlString'; import Table from './Table'; import { downloadDataAsCSV, downloadDataAsJson } from '@/utils/exportHelpers'; -import { makeExportData } from '../Stream/providers/LogsProvider'; +import { makeExportData, useLogsStore } from '../Stream/providers/LogsProvider'; const NoDataView = () => { return ( @@ -194,7 +194,8 @@ function TileControls(props: { tile: Tile; data: TileQueryResponse }) { const Tile = (props: { id: string }) => { const [tileData, setTileData] = useState(); - const [showJson, setShowJson] = useState(false) + const [showJson, setShowJson] = useState(false); + const [timeRange] = useLogsStore(store => store.timeRange) const [activeDashboard] = useDashboardsStore((store) => store.activeDashboard); const tile = _.chain(activeDashboard) .get('tiles', []) @@ -208,10 +209,9 @@ const Tile = (props: { id: string }) => { const { fetchTileData, isLoading } = useTileQuery({ onSuccess: onQuerySuccess }); useEffect(() => { - const now = new Date(); - // debug // should notify - const santizedQuery = sanitiseSqlString(tile.query, false, 100); - fetchTileData({ query: santizedQuery, startTime: new Date(now.getTime() - 24 * 3 * 60 * 60 * 1000), endTime: now }); + const shouldNotify = false; + const santizedQuery = sanitiseSqlString(tile.query, shouldNotify, 100); + fetchTileData({ query: santizedQuery, startTime: timeRange.startTime, endTime: timeRange.endTime }); }, []); const toggleJsonView = useCallback(() => { diff --git a/src/pages/Dashboards/providers/DashboardsProvider.ts b/src/pages/Dashboards/providers/DashboardsProvider.ts index 6f8dad1b..1044f06c 100644 --- a/src/pages/Dashboards/providers/DashboardsProvider.ts +++ b/src/pages/Dashboards/providers/DashboardsProvider.ts @@ -1,4 +1,4 @@ -import { Dashboard, Tile, tileSizeWidthMap } from '@/@types/parseable/api/dashboards'; +import { Dashboard, Tile, TileQueryResponse, tileSizeWidthMap } from '@/@types/parseable/api/dashboards'; import initContext from '@/utils/initContext'; import _ from 'lodash'; import { Layout } from 'react-grid-layout'; @@ -60,6 +60,10 @@ type DashboardsStore = { createTileFormOpen: boolean; vizEditorModalOpen: boolean; allowDrag: boolean; + editTileId: string | null; + tileData: { + [key: string]: TileQueryResponse; + } }; const mockDashboards = [ @@ -239,6 +243,8 @@ const initialState: DashboardsStore = { createTileFormOpen: false, vizEditorModalOpen: false, allowDrag: false, + editTileId: null, + tileData: {} }; type ReducerOutput = Partial; @@ -266,9 +272,10 @@ const toggleEditDashboardModal = (_store: DashboardsStore, val: boolean) => { }; }; -const toggleCreateTileModal = (_store: DashboardsStore, val: boolean) => { +const toggleCreateTileModal = (_store: DashboardsStore, val: boolean, tileId: string | null = null) => { return { createTileFormOpen: val, + editTileId: tileId }; }; @@ -306,24 +313,7 @@ const setDashboards = (store: DashboardsStore, dashboards: Dashboard[]) => { })(); return { dashboards, - activeDashboard: {...activeDashboard, tiles: [{ - "name": "asas", - "tile_id": "juiii", - "description": "asasa", - "stream": "teststream", - "query": "select * from teststream LIMIT 10", - "order": null, - "visualization": { - "visualization_type": "donut-chart", - "circular_chart_config": { - "name_key": "app_meta", - "value_key": "device_id" - }, - "graph_config": null, - "size": "sm", - "color_config": [] - } - }]}, + activeDashboard, }; }; diff --git a/src/pages/Dashboards/styles/dashboard.module.css b/src/pages/Dashboards/styles/DashboardView.module.css similarity index 100% rename from src/pages/Dashboards/styles/dashboard.module.css rename to src/pages/Dashboards/styles/DashboardView.module.css From b906c4de9e1547c752c67a4a38bead7e6dc61a51 Mon Sep 17 00:00:00 2001 From: balaji-jr Date: Fri, 9 Aug 2024 03:36:37 +0530 Subject: [PATCH 11/34] implemented edit & delete handlers --- src/@types/parseable/api/dashboards.ts | 3 +- src/api/constants.ts | 1 + src/api/dashboard.ts | 11 +- src/hooks/useDashboards.tsx | 39 +++- src/pages/Dashboards/CreateTileForm.tsx | 181 ++++++++++-------- src/pages/Dashboards/Dashboard.tsx | 72 ++++++- src/pages/Dashboards/Tile.tsx | 121 +++++------- src/pages/Dashboards/Toolbar.tsx | 16 +- .../providers/DashboardsProvider.ts | 47 ++++- 9 files changed, 310 insertions(+), 181 deletions(-) diff --git a/src/@types/parseable/api/dashboards.ts b/src/@types/parseable/api/dashboards.ts index 1709f5ce..14f55a5c 100644 --- a/src/@types/parseable/api/dashboards.ts +++ b/src/@types/parseable/api/dashboards.ts @@ -56,7 +56,7 @@ export type TileQueryResponse = { records: TileData } -export interface FormOpts extends Omit { +export interface FormOpts extends Omit { isQueryValidated: boolean; data: TileQueryResponse; visualization: Visualization; @@ -69,7 +69,6 @@ export type TileFormType = UseFormReturnType For export type Tile = { name: string; description: string; - stream: string; visualization: Visualization; query: string; tile_id: string; diff --git a/src/api/constants.ts b/src/api/constants.ts index 0e21c06f..0ad29c3b 100644 --- a/src/api/constants.ts +++ b/src/api/constants.ts @@ -9,6 +9,7 @@ export const LIST_SAVED_FILTERS_URL = (userId: string) => `${API_V1}/filters/${u export const LIST_DASHBOARDS = (userId: string) => `${API_V1}/dashboards/${userId}`; export const UPDATE_SAVED_FILTERS_URL = (filterId: string) => `${API_V1}/filters/filter/${filterId}`; export const UPDATE_DASHBOARDS_URL = (dashboardId: string) => `${API_V1}/dashboards/dashboard/${dashboardId}`; +export const DELETE_DASHBOARDS_URL = (dashboardId: string) => `${API_V1}/dashboards/dashboard/${dashboardId}`; export const CREATE_SAVED_FILTERS_URL = `${API_V1}/filters`; export const CREATE_DASHBOARDS_URL = `${API_V1}/dashboards`; export const DELETE_SAVED_FILTERS_URL = (filterId: string) => `${API_V1}/filters/filter/${filterId}`; diff --git a/src/api/dashboard.ts b/src/api/dashboard.ts index b7d0d66c..49d85802 100644 --- a/src/api/dashboard.ts +++ b/src/api/dashboard.ts @@ -1,9 +1,8 @@ -import { Dashboard } from "@/pages/Dashboards/providers/DashboardsProvider"; import { Axios } from "./axios"; -import { CREATE_DASHBOARDS_URL, LIST_DASHBOARDS, LOG_QUERY_URL, UPDATE_DASHBOARDS_URL } from "./constants"; +import { CREATE_DASHBOARDS_URL, DELETE_DASHBOARDS_URL, LIST_DASHBOARDS, LOG_QUERY_URL, UPDATE_DASHBOARDS_URL } from "./constants"; import timeRangeUtils from "@/utils/timeRangeUtils"; import _ from "lodash"; -import { CreateDashboardType, TileQuery, TileQueryResponse } from "@/@types/parseable/api/dashboards"; +import { CreateDashboardType, Dashboard, TileQuery, TileQueryResponse } from "@/@types/parseable/api/dashboards"; const {optimizeEndTime} = timeRangeUtils; @@ -19,9 +18,9 @@ export const postDashboard = (dashboard: CreateDashboardType, userId: string) => return Axios().post(CREATE_DASHBOARDS_URL, {...dashboard, user_id: userId}); }; -// export const deleteDashboard = (filterId: string) => { -// return Axios().delete(DELETE_SAVED_FILTERS_URL(filterId)); -// }; +export const removeDashboard = (dashboardId: string) => { + return Axios().delete(DELETE_DASHBOARDS_URL(dashboardId)); +}; // using just for the dashboard tile now // refactor once the fields are included in the /query in the response diff --git a/src/hooks/useDashboards.tsx b/src/hooks/useDashboards.tsx index 042ecc86..90b3be37 100644 --- a/src/hooks/useDashboards.tsx +++ b/src/hooks/useDashboards.tsx @@ -4,11 +4,11 @@ import { AxiosError, isAxiosError } from 'axios'; import Cookies from 'js-cookie'; import _, { isError } from 'lodash'; import { useDashboardsStore, dashboardsStoreReducers } from '@/pages/Dashboards/providers/DashboardsProvider'; -import { getDashboards, getQueryData, postDashboard, putDashboard } from '@/api/dashboard'; +import { getDashboards, getQueryData, postDashboard, putDashboard, removeDashboard } from '@/api/dashboard'; import { useCallback, useState } from 'react'; import { CreateDashboardType, TileQuery, TileQueryResponse, UpdateDashboardType } from '@/@types/parseable/api/dashboards'; -const { setDashboards } = dashboardsStoreReducers; +const { setDashboards, setTileData } = dashboardsStoreReducers; export const useDashboardsQuery = () => { const [, setDashbaordsStore] = useDashboardsStore((_store) => null); @@ -56,8 +56,8 @@ export const useDashboardsQuery = () => { putDashboard(data.dashboard.dashboard_id, data.dashboard), { onSuccess: (_data, variables) => { - variables.onSuccess && variables.onSuccess(); fetchDashboards(); + variables.onSuccess && variables.onSuccess(); notifySuccess({ message: 'Updated Successfully' }); }, onError: (data: AxiosError) => { @@ -71,6 +71,25 @@ export const useDashboardsQuery = () => { }, ); + const { mutate: deleteDashboard, isLoading: isDeleting } = useMutation( + (data: { dashboardId: string; onSuccess?: () => void }) => removeDashboard(data.dashboardId), + { + onSuccess: (_data, variables) => { + fetchDashboards(); + variables.onSuccess && variables.onSuccess(); + notifySuccess({ message: 'Deleted Successfully' }); + }, + onError: (data: AxiosError) => { + if (isAxiosError(data) && data.response) { + const error = data.response?.data as string; + typeof error === 'string' && notifyError({ message: error }); + } else if (data.message && typeof data.message === 'string') { + notifyError({ message: data.message }); + } + }, + }, + ); + return { fetchDashaboardsError, fetchDashboardsSuccess, @@ -81,11 +100,15 @@ export const useDashboardsQuery = () => { isCreatingDashboard, updateDashboard, isUpdatingDashboard, + + deleteDashboard, + isDeleting }; }; -export const useTileQuery = (opts: { onSuccess: (data: TileQueryResponse) => void }) => { - const { onSuccess } = opts; +export const useTileQuery = (opts?: { tileId?: string, onSuccess?: (data: TileQueryResponse) => void }) => { + const [, setDashbaordsStore] = useDashboardsStore((_store) => null); + const { onSuccess } = opts || {}; const [fetchState, setFetchState] = useState<{ isLoading: boolean; isError: null | boolean; @@ -97,9 +120,9 @@ export const useTileQuery = (opts: { onSuccess: (data: TileQueryResponse) => voi try { setFetchState({ isLoading: true, isError: null, isSuccess: null }); const res = await getQueryData(queryOpts); - // debug - // if no result, fill {records: [], fields: []} - onSuccess(res.data); + const tileData = _.isEmpty(res) ? { records: [], fields: [] } : res.data; + opts?.tileId && setDashbaordsStore((store) => setTileData(store, opts.tileId || '', tileData)); + opts?.onSuccess && opts.onSuccess(tileData); setFetchState({ isLoading: false, isError: false, isSuccess: true }); } catch (e) { setFetchState({ isLoading: false, isError: true, isSuccess: false }); diff --git a/src/pages/Dashboards/CreateTileForm.tsx b/src/pages/Dashboards/CreateTileForm.tsx index 0e0f7fd3..49831d8f 100644 --- a/src/pages/Dashboards/CreateTileForm.tsx +++ b/src/pages/Dashboards/CreateTileForm.tsx @@ -1,4 +1,4 @@ -import { Box, Button, Select, Stack, Text, TextInput } from '@mantine/core'; +import { Box, Button, Divider, Select, Stack, Text, TextInput } from '@mantine/core'; import classes from './styles/Form.module.css'; import { useForm } from '@mantine/form'; import { useDashboardsStore, dashboardsStoreReducers } from './providers/DashboardsProvider'; @@ -12,7 +12,7 @@ import VizEditorModal, { Viz } from './VizEditorModal'; import { SchemaList } from '../Stream/components/Querier/QueryCodeEditor'; import { IconAlertTriangle, IconArrowLeft, IconChartPie } from '@tabler/icons-react'; import { useDashboardsQuery, useTileQuery } from '@/hooks/useDashboards'; -import { Dashboard, EditTileType, FormOpts, TileFormType, TileQueryResponse } from '@/@types/parseable/api/dashboards'; +import { Dashboard, EditTileType, FormOpts, Tile, TileFormType, TileQueryResponse } from '@/@types/parseable/api/dashboards'; import { CodeHighlight } from '@mantine/code-highlight'; import { sanitiseSqlString } from '@/utils/sanitiseSqlString'; import { useLogsStore } from '../Stream/providers/LogsProvider'; @@ -30,15 +30,13 @@ const invalidVizConfig = 'Invalid visualization config'; const { toggleVizEditorModal, toggleCreateTileModal } = dashboardsStoreReducers; const getErrorMsg = (form: TileFormType, configType: 'basic' | 'data' | 'viz'): string | null => { - const { stream, dashboardId, isQueryValidated, data, visualization } = form.values; + const { dashboardId, isQueryValidated, data, visualization } = form.values; const hasVizConfigErrors = _.some(_.keys(form.errors), (key) => _.startsWith(key, 'visualization.')); // form.validateField('visualization') const hasNoData = _.isEmpty(data) || _.isEmpty(data.records); if (_.isEmpty(dashboardId)) { return selectDashboardWarningText; - } else if (_.isEmpty(stream)) { - return selectStreamWarningText; } else if (configType === 'data' || configType === 'viz') { if (!isQueryValidated) { return validateQueryWarningText; @@ -125,8 +123,6 @@ const VisPreview = (props: { form: TileFormType }) => { ...(errorMsg ? {} : { actionBtnProps: { label: 'Edit', onClick: openVizModal } }), }; - console.log(sectionHeaderProps); - return ( @@ -182,13 +178,18 @@ const DataPreview = (props: { form: TileFormType }) => { ); }; -const useTileForm = (opts: FormOpts) => { +const useTileForm = (opts: { + activeDashboard: Dashboard | null; + editTileId: string | null; + tileData: TileQueryResponse; +}) => { + const { activeDashboard, editTileId, tileData } = opts; + const formOpts = genTileFormOpts({ activeDashboard, editTileId, tileData }); const form = useForm({ mode: 'controlled', - initialValues: opts, + initialValues: formOpts, validate: { name: (val) => (_.isEmpty(val) ? 'Cannot be empty' : null), - stream: (val) => (_.isEmpty(val) ? 'Cannot be empty' : null), description: (val) => (_.isEmpty(val) ? 'Cannot be empty' : null), query: (val) => (_.isEmpty(val) ? 'Cannot be empty' : null), isQueryValidated: (val) => (val === true ? null : 'Query not validated'), @@ -242,6 +243,12 @@ const useTileForm = (opts: FormOpts) => { validateInputOnBlur: true, }); + // useEffect(() => { + // if (_.isFunction(form.setValues)) { + // form.setValues(genTileFormOpts({ activeDashboard, editTileId, tileData })); + // } + // }, [activeDashboard?.dashboard_id, editTileId]); + const colors = form.values.visualization?.colors; const onChangeValue = useCallback((key: string, value: any) => { @@ -273,30 +280,31 @@ const fetchStreamFields = async (stream: string, setFields: (fields: Field[]) => const Query = (props: { form: TileFormType; onChangeValue: (key: string, value: any) => void }) => { const { form: { - values: { stream, query, isQueryValidated, dashboardId }, + values: { query, isQueryValidated, dashboardId }, }, onChangeValue, } = props; const [llmActive] = useAppStore((store) => store.instanceConfig?.llmActive); const containerRef = useRef(null); + const [localStream, setLocalStream] = useState(''); const [fields, setFields] = useState([]); const [initialHeight, setInitialHeight] = useState(0); - const isValidStream = !_.isEmpty(stream); const [dashboards] = useDashboardsStore((store) => store.dashboards); const [timeRange] = useLogsStore((store) => store.timeRange); - - useEffect(() => { - setFields([]); - if (_.size(stream) > 0) { - fetchStreamFields(stream, (fields: Field[]) => setFields(fields)); - } - }, [stream]); - + const [userSpecificStreams] = useAppStore((store) => store.userSpecificStreams); + const allStreams = useMemo( + () => _.map(userSpecificStreams, (stream) => ({ label: stream.name, value: stream.name })), + [userSpecificStreams], + ); useEffect(() => { if (containerRef.current) { setInitialHeight(containerRef.current.offsetHeight); } - }, [isValidStream]); + setFields([]); + if (_.size(localStream) > 0) { + fetchStreamFields(localStream, (fields: Field[]) => setFields(fields)); + } + }, [localStream]); const onFetchTileSuccess = useCallback((data: TileQueryResponse) => { props.form.setFieldValue('isQueryValidated', true); @@ -324,13 +332,23 @@ const Query = (props: { form: TileFormType; onChangeValue: (key: string, value: // onChangeValue('data', null) }, []); + const onStreamSelect = useCallback((val: string | null) => { + setLocalStream(val || ''); + }, []) + const errorMsg = getErrorMsg(props.form, 'basic'); return ( - Query + + Query + + + - + @@ -155,7 +153,7 @@ const CreateDashboardModal = () => { diff --git a/src/pages/Dashboards/CreateTileForm.tsx b/src/pages/Dashboards/CreateTileForm.tsx index d34cb07f..45bf2cba 100644 --- a/src/pages/Dashboards/CreateTileForm.tsx +++ b/src/pages/Dashboards/CreateTileForm.tsx @@ -2,7 +2,7 @@ import { Box, Button, Divider, Select, Stack, Text, TextInput } from '@mantine/c import classes from './styles/Form.module.css'; import { useForm } from '@mantine/form'; import { useDashboardsStore, dashboardsStoreReducers } from './providers/DashboardsProvider'; -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { MutableRefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useAppStore } from '@/layouts/MainLayout/providers/AppProvider'; import _ from 'lodash'; import { getLogStreamSchema } from '@/api/logStream'; @@ -33,8 +33,6 @@ const { toggleVizEditorModal, toggleCreateTileModal } = dashboardsStoreReducers; const getErrorMsg = (form: TileFormType, configType: 'basic' | 'data' | 'viz'): string | null => { const { dashboardId, isQueryValidated, data, visualization } = form.values; const hasVizConfigErrors = _.some(_.keys(form.errors), (key) => _.startsWith(key, 'visualization.')); - // form.validateField('visualization') - const hasNoData = _.isEmpty(data) || _.isEmpty(data.records); if (_.isEmpty(dashboardId)) { return selectDashboardWarningText; @@ -114,7 +112,7 @@ const VisPreview = (props: { form: TileFormType }) => { props.form.validateField('visualization'); }, [visualization]); - const [, setDashbaordsStore] = useDashboardsStore((store) => null); + const [, setDashbaordsStore] = useDashboardsStore((_store) => null); const openVizModal = useCallback(() => setDashbaordsStore((store) => toggleVizEditorModal(store, true)), []); const sectionHeaderProps = { @@ -143,7 +141,7 @@ const DataPreview = (props: { form: TileFormType }) => { values: { data }, }, } = props; - const containerRef = useRef(null); + const containerRef: MutableRefObject = useRef(null); const [containerSize, setContainerSize] = useState({ height: 0, width: 0 }); useEffect(() => { if (containerRef.current) { @@ -242,13 +240,7 @@ const useTileForm = (opts: { validateInputOnBlur: true, }); - // useEffect(() => { - // if (_.isFunction(form.setValues)) { - // form.setValues(genTileFormOpts({ activeDashboard, editTileId, tileData })); - // } - // }, [activeDashboard?.dashboard_id, editTileId]); - - const colors = form.values.visualization?.colors; + const colors = form.values.visualization?.color_config; const onChangeValue = useCallback((key: string, value: any) => { form.setFieldValue(key, value); @@ -284,7 +276,7 @@ const Query = (props: { form: TileFormType; onChangeValue: (key: string, value: onChangeValue, } = props; const [llmActive] = useAppStore((store) => store.instanceConfig?.llmActive); - const containerRef = useRef(null); + const containerRef:MutableRefObject = useRef(null); const [localStream, setLocalStream] = useState(''); const [fields, setFields] = useState([]); const [initialHeight, setInitialHeight] = useState(0); @@ -442,12 +434,7 @@ const Query = (props: { form: TileFormType; onChangeValue: (key: string, value: const Config = (props: { form: TileFormType; onChangeValue: (key: string, value: any) => void }) => { const { form, onChangeValue } = props; - const [userSpecificStreams] = useAppStore((store) => store.userSpecificStreams); const [dashboards] = useDashboardsStore((store) => store.dashboards); - const allStreams = useMemo( - () => _.map(userSpecificStreams, (stream) => ({ label: stream.name, value: stream.name })), - [userSpecificStreams], - ); const allDashboards = useMemo( () => _.map(dashboards, (dashboard) => ({ label: dashboard.name, value: dashboard.dashboard_id })), [dashboards], @@ -534,13 +521,13 @@ const genTileFormOpts = (opts: { tileData: TileQueryResponse; }) => { const { activeDashboard, editTileId } = opts; - if (!editTileId) return {...defaultFormOpts, dashboardId: activeDashboard?.dashboard_id, order: _.size(activeDashboard?.tiles) + 1}; + if (!editTileId) return {...defaultFormOpts, dashboardId: activeDashboard?.dashboard_id || null, order: _.size(activeDashboard?.tiles) + 1}; const currentTile = _.find(activeDashboard?.tiles, (tile) => tile.tile_id === editTileId); if (!currentTile) return { ...defaultFormOpts, - dashboardId: activeDashboard?.dashboard_id, + dashboardId: activeDashboard?.dashboard_id || null, order: _.size(activeDashboard?.tiles) + 1, }; const { diff --git a/src/pages/Dashboards/Dashboard.tsx b/src/pages/Dashboards/Dashboard.tsx index 54fb18bc..14855690 100644 --- a/src/pages/Dashboards/Dashboard.tsx +++ b/src/pages/Dashboards/Dashboard.tsx @@ -1,37 +1,29 @@ -import { Box, Button, Modal, Stack, Text, ThemeIcon } from '@mantine/core'; +import { Box, Button, Modal, Stack, Text } from '@mantine/core'; import Toolbar from './Toolbar'; import 'react-grid-layout/css/styles.css'; import 'react-resizable/css/styles.css'; import './styles/ReactGridLayout.css'; import GridLayout from 'react-grid-layout'; import { DASHBOARDS_SIDEBAR_WIDTH, NAVBAR_WIDTH } from '@/constants/theme'; -// import classes from './styles/tile.module.css'; import classes from './styles/DashboardView.module.css'; -import { useDashboardsStore, dashboardsStoreReducers, genLayout, assignOrderToTiles } from './providers/DashboardsProvider'; +import { useDashboardsStore, dashboardsStoreReducers, assignOrderToTiles } from './providers/DashboardsProvider'; import _ from 'lodash'; import { IconChartBar } from '@tabler/icons-react'; -import { useCallback, useEffect, useRef } from 'react'; +import { useCallback, useRef } from 'react'; import { makeExportClassName } from '@/utils/exportImage'; import { useDashboardsQuery } from '@/hooks/useDashboards'; import Tile from './Tile'; import { Dashboard as DashboardType } from '@/@types/parseable/api/dashboards'; -import { useLogsStore } from '../Stream/providers/LogsProvider'; +import { Layout } from 'react-grid-layout'; -const { toggleCreateDashboardModal, toggleCreateTileModal, toggleDeleteTileModal, resetTilesData } = dashboardsStoreReducers; +const { toggleCreateDashboardModal, toggleCreateTileModal, toggleDeleteTileModal } = dashboardsStoreReducers; -const TilesView = (props) => { - const [activeDashboard, setDashbaordsStore] = useDashboardsStore((store) => store.activeDashboard); - const [allowDrag] = useDashboardsStore(store => store.allowDrag) - const [layout] = useDashboardsStore(store => store.layout) +const TilesView = (props: { onLayoutChange: (layout: Layout[]) => void }) => { + const [activeDashboard] = useDashboardsStore((store) => store.activeDashboard); + const [allowDrag] = useDashboardsStore((store) => store.allowDrag); + const [layout] = useDashboardsStore((store) => store.layout); const hasNoTiles = _.size(activeDashboard?.tiles) < 1; - const showNoTilesView = hasNoTiles || !activeDashboard - const [timeRange] = useLogsStore(store => store.timeRange) - - // useEffect(() => { - // if (!showNoTilesView) { - // setDashbaordsStore(resetTilesData); - // } - // }, [timeRange]); + const showNoTilesView = hasNoTiles || !activeDashboard; if (showNoTilesView) return ; @@ -48,7 +40,7 @@ const TilesView = (props) => { containerPadding={[20, 10]} compactType="horizontal" isDraggable={allowDrag} - onLayoutChange={(layout) => props.onLayoutChang(layout)}> + onLayoutChange={(layout) => props.onLayoutChange(layout)}> {_.map(layout, (item) => { return (
{
); })} - - {/*
- Item B -
-
- Item C -
-
- Item D -
*/}
); @@ -87,7 +69,7 @@ const DeleteTileModal = () => { }, []); const onConfirm = useCallback(() => { - const allTiles = activeDashboard.tiles.filter(tile => tile.tile_id !== selectedTile?.tile_id); + const allTiles = activeDashboard.tiles.filter((tile) => tile.tile_id !== selectedTile?.tile_id); const tilesWithUpdatedOrder = assignOrderToTiles(allTiles); updateDashboard({ dashboard: { ...activeDashboard, tiles: tilesWithUpdatedOrder }, onSuccess: onClose }); @@ -108,16 +90,18 @@ const DeleteTileModal = () => { title={Delete Tile}> - - Are you sure want to delete this tile ? - + Are you sure want to delete this tile ? - + - + @@ -126,15 +110,14 @@ const DeleteTileModal = () => { }; const NoDashboardsView = () => { - const [, setDashbaordsStore] = useDashboardsStore((store) => null); + const [, setDashboardsStore] = useDashboardsStore((_store) => null); const openCreateDashboardModal = useCallback(() => { - setDashbaordsStore((store) => toggleCreateDashboardModal(store, true)); + setDashboardsStore((store) => toggleCreateDashboardModal(store, true)); }, []); return ( - {/* */} @@ -146,13 +129,12 @@ const NoDashboardsView = () => { - {/* */} ); }; const NoTilesView = () => { - const [, setDashbaordsStore] = useDashboardsStore((store) => null); + const [, setDashbaordsStore] = useDashboardsStore((_store) => null); const openCreateTileModal = useCallback(() => { setDashbaordsStore((store) => toggleCreateTileModal(store, true)); @@ -177,20 +159,21 @@ const NoTilesView = () => { const Dashboard = () => { const [dashboards] = useDashboardsStore((store) => store.dashboards); - const layoutRef = useRef(null); + const layoutRef = useRef([]); if (_.isEmpty(dashboards)) return ; - const onLayoutChange = useCallback((layout) => { - const tileIdsWithUpdatedOrder = _.map(layout, (layoutItem) => layoutItem.i); - layoutRef.current = layout - console.log(layout, "onchange") - }, [layoutRef.current]) + const onLayoutChange = useCallback( + (layout: Layout[]) => { + layoutRef.current = layout; + }, + [layoutRef.current], + ); return ( - - + + ); }; diff --git a/src/pages/Dashboards/SideBar.tsx b/src/pages/Dashboards/SideBar.tsx index 9f56504f..af052de4 100644 --- a/src/pages/Dashboards/SideBar.tsx +++ b/src/pages/Dashboards/SideBar.tsx @@ -2,9 +2,10 @@ import { DASHBOARDS_SIDEBAR_WIDTH } from '@/constants/theme'; import { Button, Stack, Text } from '@mantine/core'; import classes from './styles/sidebar.module.css'; import { IconPlus } from '@tabler/icons-react'; -import { Dashboard, useDashboardsStore, dashboardsStoreReducers } from './providers/DashboardsProvider'; -import { useCallback, useEffect, useState } from 'react'; +import { useDashboardsStore, dashboardsStoreReducers } from './providers/DashboardsProvider'; +import { useCallback } from 'react'; import _ from 'lodash'; +import { Dashboard } from '@/@types/parseable/api/dashboards'; const {selectDashboard, toggleCreateDashboardModal} = dashboardsStoreReducers; diff --git a/src/pages/Dashboards/Table.tsx b/src/pages/Dashboards/Table.tsx index bd4bebcf..2fd5a0a1 100644 --- a/src/pages/Dashboards/Table.tsx +++ b/src/pages/Dashboards/Table.tsx @@ -1,16 +1,19 @@ -import { Stack, Table, Text } from "@mantine/core" -import { TileQueryResponse } from "@/@types/parseable/api/dashboards"; -import classes from './styles/Table.module.css' -import _ from "lodash"; -import { useEffect, useMemo, useRef, useState } from "react"; -import { IconAlertTriangle } from "@tabler/icons-react"; +import { Stack, Table, Text } from '@mantine/core'; +import { TileQueryResponse } from '@/@types/parseable/api/dashboards'; +import classes from './styles/Table.module.css'; +import _ from 'lodash'; +import { MutableRefObject, useEffect, useMemo, useRef, useState } from 'react'; +import { IconAlertTriangle } from '@tabler/icons-react'; const makeRowData = (data: TileQueryResponse) => { - const {fields, records} = data; - return _.map(records, rec => { - return _.at(rec, fields); - }) -} + const { fields, records } = data; + return _.map(records, (rec) => { + return _.chain(rec) + .at(fields) + .map((i) => _.toString(i)) + .value(); + }); +}; const NoDataView = () => { return ( @@ -25,10 +28,9 @@ const TableViz = (props: { data: TileQueryResponse }) => { const { data: { fields, records }, } = props; - // debug const rowData = useMemo(() => makeRowData({ fields, records }), []); - const containerRef = useRef(null); + const containerRef: MutableRefObject = useRef(null); const [initialHeight, setInitialHeight] = useState(0); useEffect(() => { @@ -36,7 +38,7 @@ const TableViz = (props: { data: TileQueryResponse }) => { setInitialHeight(containerRef.current.offsetHeight); } }, []); - const hasNoData = _.isEmpty(records) || _.isEmpty(fields) + const hasNoData = _.isEmpty(records) || _.isEmpty(fields); return ( @@ -60,4 +62,4 @@ const TableViz = (props: { data: TileQueryResponse }) => { ); }; -export default TableViz; \ No newline at end of file +export default TableViz; diff --git a/src/pages/Dashboards/Tile.tsx b/src/pages/Dashboards/Tile.tsx index 6a9e8374..029c6686 100644 --- a/src/pages/Dashboards/Tile.tsx +++ b/src/pages/Dashboards/Tile.tsx @@ -1,4 +1,4 @@ -import { Loader, px, Stack, Text, Menu, Button, rem, SegmentedControl, Modal, TextInput, Box } from '@mantine/core'; +import { Loader, Stack, Text, Menu } from '@mantine/core'; import classes from './styles/tile.module.css'; import { IconAlertTriangle, @@ -7,22 +7,21 @@ import { IconGripVertical, IconPencil, IconPhoto, - IconTable, + IconTable, IconTrash, } from '@tabler/icons-react'; -import charts, { - getVizComponent, +import { isCircularChart, isGraph, renderCircularChart, renderGraph, - renderJsonView, + // renderJsonView, } from './Charts'; import handleCapture, { makeExportClassName } from '@/utils/exportImage'; import { useDashboardsStore, dashboardsStoreReducers } from './providers/DashboardsProvider'; import _ from 'lodash'; import { useTileQuery } from '@/hooks/useDashboards'; -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect } from 'react'; import { Tile as TileType, TileQueryResponse } from '@/@types/parseable/api/dashboards'; import { sanitiseSqlString } from '@/utils/sanitiseSqlString'; import Table from './Table'; @@ -76,15 +75,15 @@ const Graph = (props: { tile: TileType; data: TileQueryResponse }) => { ); }; -const JsonView = (props: { tile: TileType; data: TileQueryResponse }) => { - const { tile, data } = props; - return ( - {renderJsonView({ queryResponse: data })} - ); -}; +// const JsonView = (props: { tile: TileType; data: TileQueryResponse }) => { +// const { data } = props; +// return ( +// {renderJsonView({ queryResponse: data })} +// ); +// }; const getViz = (vizType: string | null) => { - if (!vizType) return <>; + if (!vizType) return null; if (vizType === 'table') { return Table; @@ -93,14 +92,14 @@ const getViz = (vizType: string | null) => { } else if (isGraph(vizType)) { return Graph; } else { - return <>; + return null; } }; function TileControls(props: { tile: TileType; data: TileQueryResponse }) { const { tile: { name, tile_id }, - data = {}, + data, } = props; const { records = [], fields = [] } = data; const [allowDrag] = useDashboardsStore((store) => store.allowDrag); @@ -174,7 +173,7 @@ function TileControls(props: { tile: TileType; data: TileQueryResponse }) { } const Tile = (props: { id: string }) => { - const [showJson, setShowJson] = useState(false); + // const [showJson, setShowJson] = useState(false); const [timeRange] = useLogsStore((store) => store.timeRange); const [activeDashboard] = useDashboardsStore((store) => store.activeDashboard); const [tilesData] = useDashboardsStore((store) => store.tilesData); @@ -192,12 +191,12 @@ const Tile = (props: { id: string }) => { fetchTileData({ query: santizedQuery, startTime: timeRange.startTime, endTime: timeRange.endTime }); }, [timeRange]); - const toggleJsonView = useCallback(() => { - return setShowJson((prev) => !prev); - }, []); + // const toggleJsonView = useCallback(() => { + // return setShowJson((prev) => !prev); + // }, []); const hasData = !_.isEmpty(tileData); const vizType = _.get(tile, 'visualization.visualization_type', null); - const Viz = showJson ? JsonView : getViz(vizType); + const Viz = getViz(vizType); return ( @@ -212,7 +211,7 @@ const Tile = (props: { id: string }) => { {!hasData && !isLoading && } {!isLoading && hasData && ( - + {Viz && } )} diff --git a/src/pages/Dashboards/Toolbar.tsx b/src/pages/Dashboards/Toolbar.tsx index 5aba18fb..a7eb1941 100644 --- a/src/pages/Dashboards/Toolbar.tsx +++ b/src/pages/Dashboards/Toolbar.tsx @@ -3,14 +3,15 @@ import { Box, Button, Modal, px, Stack, Text, TextInput } from '@mantine/core'; import { IconCheck, IconPencil, IconPlus, IconTrash } from '@tabler/icons-react'; import classes from './styles/toolbar.module.css'; import { useDashboardsStore, dashboardsStoreReducers, sortTilesByOrder } from './providers/DashboardsProvider'; -import { useCallback, useState } from 'react'; +import { ChangeEvent, useCallback, useState } from 'react'; import IconButton from '@/components/Button/IconButton'; import { useDashboardsQuery } from '@/hooks/useDashboards'; import _ from 'lodash'; +import ReactGridLayout, { Layout } from 'react-grid-layout'; const { toggleEditDashboardModal, toggleAllowDrag, toggleCreateTileModal, toggleDeleteDashboardModal } = dashboardsStoreReducers; -const tileIdsbyOrder = (layout) => { +const tileIdsbyOrder = (layout: Layout[]) => { return layout .slice() .sort((a, b) => { @@ -22,7 +23,7 @@ const tileIdsbyOrder = (layout) => { .map((item) => item.i); }; -const EditLayoutButton = (props) => { +const EditLayoutButton = (props: {layoutRef: React.MutableRefObject}) => { const [allowDrag, setDashbaordsStore] = useDashboardsStore((store) => store.allowDrag); const [activeDashboard] = useDashboardsStore(store => store.activeDashboard) @@ -85,7 +86,7 @@ const DeleteDashboardModal = () => { setDashbaordsStore((store) => toggleDeleteDashboardModal(store, false)); }, []); - const onChangeHandler = useCallback((e) => { + const onChangeHandler = useCallback((e: ChangeEvent) => { setConfirmText(e.target.value); }, []); @@ -145,7 +146,7 @@ const DeleteDashboardButton = () => { return ; }; -const Toolbar = (props) => { +const Toolbar = (props: {layoutRef: React.MutableRefObject}) => { const [activeDashboard, setDashbaordsStore] = useDashboardsStore((store) => store.activeDashboard); const openEditDashboardModal = useCallback(() => { setDashbaordsStore((store) => toggleEditDashboardModal(store, true)); diff --git a/src/pages/Dashboards/VizEditorModal.tsx b/src/pages/Dashboards/VizEditorModal.tsx index 894cce44..63ed5ae0 100644 --- a/src/pages/Dashboards/VizEditorModal.tsx +++ b/src/pages/Dashboards/VizEditorModal.tsx @@ -1,24 +1,10 @@ -import { Box, Button, Modal, MultiSelect, Select, Stack, Text, TextInput } from '@mantine/core'; +import { Button, Modal, MultiSelect, Select, Stack, Text } from '@mantine/core'; import classes from './styles/VizEditor.module.css'; import { useDashboardsStore, dashboardsStoreReducers } from './providers/DashboardsProvider'; -import { useForm, UseFormReturnType } from '@mantine/form'; import { useCallback, useEffect } from 'react'; import _ from 'lodash'; -import { - isCircularChart, - renderCircularChart, - renderGraph, -} from './Charts'; -import charts from './Charts'; -import { - FormOpts, - TileFormType, - TileQuery, - TileQueryResponse, - tileSizes, - Visualization, - visualizations, -} from '@/@types/parseable/api/dashboards'; +import { isCircularChart, renderCircularChart, renderGraph } from './Charts'; +import { TileFormType, tileSizes, visualizations } from '@/@types/parseable/api/dashboards'; import { IconAlertTriangle } from '@tabler/icons-react'; import TableViz from './Table'; const { toggleVizEditorModal } = dashboardsStoreReducers; @@ -77,7 +63,7 @@ const Table = (props: { form: TileFormType }) => { export const Viz = (props: { form: TileFormType }) => { const { form: { - values: { visualization, data }, + values: { visualization }, }, } = props; @@ -89,13 +75,7 @@ export const Viz = (props: { form: TileFormType }) => { return ( - {showWarning ? ( - - ) : ( - // - - // - )} + {showWarning ? : } ); @@ -155,7 +135,6 @@ const CircularChartConfig = (props: { form: TileFormType }) => { const { form: { values: { data }, - errors, }, } = props; @@ -237,7 +216,7 @@ const TickConfig = (props: { form: TileFormType }) => { ); }; -const Config = (props: { form: TileFormType; updateColors: (key: string, value: string) => void }) => { +const Config = (props: { form: TileFormType }) => { return ( @@ -268,34 +247,6 @@ const Config = (props: { form: TileFormType; updateColors: (key: string, value: ); }; -const useVizForm = (opts: Visualization) => { - const form = useForm({ - mode: 'controlled', - initialValues: opts, - validate: {}, - validateInputOnChange: true, - validateInputOnBlur: true, - }); - - const onChangeValue = useCallback((key: string, value: any) => { - form.setFieldValue(key, value); - }, []); - - const updateColors = useCallback( - (key: string, value: string) => { - form.setFieldValue('colors', { - ...form.values.colors, - [key]: value, - }); - }, - [form], - ); - - return { form, onChangeValue, updateColors }; -}; - -type VizFormReturnType = UseFormReturnType Visualization>; - const VizEditorModal = (props: { form: TileFormType }) => { const { form } = props; const [vizEditorModalOpen] = useDashboardsStore((store) => store.vizEditorModalOpen); @@ -303,7 +254,7 @@ const VizEditorModal = (props: { form: TileFormType }) => { const closeVizModal = useCallback(() => { setDashboardStore((store) => toggleVizEditorModal(store, false)); }, []); - const isTableViz = form.values.visualization.visualization_type === 'table' + const isTableViz = form.values.visualization.visualization_type === 'table'; return ( { classNames={{ title: classes.modalTitle }}> - + diff --git a/src/pages/Dashboards/index.tsx b/src/pages/Dashboards/index.tsx index 5599b4b9..b8f19ab3 100644 --- a/src/pages/Dashboards/index.tsx +++ b/src/pages/Dashboards/index.tsx @@ -1,4 +1,4 @@ -import { Box, Loader, px, Stack } from '@mantine/core'; +import { Box, Loader, Stack } from '@mantine/core'; import 'react-grid-layout/css/styles.css'; import 'react-resizable/css/styles.css'; import SideBar from './SideBar'; @@ -11,11 +11,11 @@ import CreateTileForm from './CreateTileForm'; const LoadingView = () => { return ( - - + + - ) -} + ); +}; const Dashboards = () => { const [dashboards] = useDashboardsStore((store) => store.dashboards); diff --git a/src/pages/Dashboards/providers/DashboardsProvider.ts b/src/pages/Dashboards/providers/DashboardsProvider.ts index e1131e49..ed319951 100644 --- a/src/pages/Dashboards/providers/DashboardsProvider.ts +++ b/src/pages/Dashboards/providers/DashboardsProvider.ts @@ -22,7 +22,7 @@ export const assignOrderToTiles = (tiles: Tile[]) => { export const genLayout = (tiles: Tile[]): Layout[] => { return _.reduce( tiles, - (acc, tile) => { + (acc: Layout[], tile) => { const { visualization: { size }, tile_id, @@ -85,174 +85,6 @@ type DashboardsStore = { deleteTileId: string | null; }; -const mockDashboards = [ - { - name: 'Backend dashboard', - description: 'This is a description for the dashboard', - id: 'dashboard-1', - tiles: [ - { - name: 'Donut Tile', - id: 'tile-1', - description: 'Description for the tile', - stream: 'backend', - query: 'SELECT level, COUNT(*) AS level_count FROM teststream GROUP BY level;', - visualization: { - type: 'donut-chart' as 'donut-chart', - circular_chart_config: { - name_key: 'level', - value_key: 'level_count', - }, - size: 'sm', - }, - }, - { - name: 'Bar Tile', - id: 'tile-4', - description: 'Description for the tile', - stream: 'backend', - query: - "SELECT message, SUM(CASE WHEN level = 'info' THEN 1 ELSE 0 END) AS info, SUM(CASE WHEN level = 'warn' THEN 1 ELSE 0 END) AS warn, SUM(CASE WHEN level = 'error' THEN 1 ELSE 0 END) AS error FROM teststream GROUP BY message;", - visualization: { - type: 'bar-chart' as 'donut-chart', - graph_config: { - x_key: 'message', - y_key: ['info', 'warn', 'error'], - }, - size: 'lg', - }, - }, - { - name: 'Donut Tile', - id: 'tile-3', - description: 'Description for the tile', - stream: 'backend', - query: 'SELECT level, COUNT(*) AS level_count FROM teststream GROUP BY level;', - visualization: { - type: 'pie-chart' as 'donut-chart', - circular_chart_config: { - name_key: 'level', - value_key: 'level_count', - }, - size: 'sm', - }, - }, - { - name: 'Line Tile', - id: 'tile-2', - description: 'Description for the tile', - stream: 'backend', - query: 'SELECT device_id, host, level, message, app_meta FROM teststream;', - visualization: { - type: 'table' as 'line-chart', - graph_config: { - x_key: 'level', - y_key: ['level_count'], - }, - size: 'lg', - }, - }, - // { - // name: 'Donut Tile', - // id: 'tile-2', - // description: 'Description for the tile', - // stream: 'backend', - // visualization: { - // type: 'donut-chart' as 'donut-chart', - // } - // }, - // { - // name: 'Donut Tile', - // id: 'tile-3', - // description: 'Description for the tile', - // stream: 'backend', - // visualization: { - // type: 'donut-chart' as 'donut-chart', - // } - // }, - ], - }, - { - name: 'Frontend Dashboard', - description: 'This is a description for the dashboard', - id: 'dashboard-2', - pinned: true, - tiles: [ - { - name: 'Donut Tile', - id: 'tile-1', - description: 'Description for the tile', - stream: 'backend', - visualization: { - type: 'donut-chart' as 'donut-chart', - }, - }, - { - name: 'Donut Tile', - id: 'tile-1', - description: 'Description for the tile', - stream: 'backend', - visualization: { - type: 'donut-chart' as 'donut-chart', - }, - }, - ], - }, - { - name: 'Api Dashboard', - description: 'This is a description for the dashboard', - id: 'dashboard-3', - pinned: true, - tiles: [ - { - name: 'Donut Tile', - id: 'tile-1', - description: 'Description for the tile', - stream: 'backend', - visualization: { - type: 'donut-chart' as 'donut-chart', - }, - }, - { - name: 'Donut Tile', - id: 'tile-1', - description: 'Description for the tile', - stream: 'backend', - visualization: { - type: 'donut-chart' as 'donut-chart', - }, - }, - { - name: 'Donut Tile', - id: 'tile-1', - description: 'Description for the tile', - stream: 'backend', - visualization: { - type: 'donut-chart' as 'donut-chart', - }, - }, - { - name: 'Donut Tile', - id: 'tile-1', - description: 'Description for the tile', - stream: 'backend', - visualization: { - type: 'donut-chart' as 'donut-chart', - }, - }, - { - name: 'Donut Tile', - id: 'tile-1', - description: 'Description for the tile', - stream: 'backend', - visualization: { - type: 'donut-chart' as 'donut-chart', - }, - }, - ], - }, -]; - const initialState: DashboardsStore = { dashboards: null, activeDashboard: null, diff --git a/src/utils/exportImage.ts b/src/utils/exportImage.ts index 95592e75..2240c822 100644 --- a/src/utils/exportImage.ts +++ b/src/utils/exportImage.ts @@ -9,7 +9,7 @@ export const makeExportClassName = (name: string) => { const handleCapture = (opts: { className: string, fileName: string }) => { const { className, fileName = 'png-export' } = opts; try { - const element = document.querySelector(`.${className}`); + const element = document.querySelector(`.${className}`) as HTMLElement; if (element) { html2canvas(element).then((canvas) => { const imgData = canvas.toDataURL('image/png'); From a604b5283dfbebba2f45a28cd6f68cc5b0fee928 Mon Sep 17 00:00:00 2001 From: balaji-jr Date: Tue, 13 Aug 2024 10:50:30 +0530 Subject: [PATCH 15/34] fixed hook position --- src/pages/Dashboards/Dashboard.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/Dashboards/Dashboard.tsx b/src/pages/Dashboards/Dashboard.tsx index 14855690..cdd689ad 100644 --- a/src/pages/Dashboards/Dashboard.tsx +++ b/src/pages/Dashboards/Dashboard.tsx @@ -160,14 +160,13 @@ const NoTilesView = () => { const Dashboard = () => { const [dashboards] = useDashboardsStore((store) => store.dashboards); const layoutRef = useRef([]); - if (_.isEmpty(dashboards)) return ; - const onLayoutChange = useCallback( (layout: Layout[]) => { layoutRef.current = layout; }, [layoutRef.current], ); + if (_.isEmpty(dashboards)) return ; return ( From e90b19776e6eb4eecc86de3ad0ec1e04fdae6578 Mon Sep 17 00:00:00 2001 From: balaji-jr Date: Tue, 13 Aug 2024 10:57:03 +0530 Subject: [PATCH 16/34] fixed ts errors --- src/pages/Dashboards/Dashboard.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pages/Dashboards/Dashboard.tsx b/src/pages/Dashboards/Dashboard.tsx index cdd689ad..850b14f0 100644 --- a/src/pages/Dashboards/Dashboard.tsx +++ b/src/pages/Dashboards/Dashboard.tsx @@ -13,7 +13,6 @@ import { useCallback, useRef } from 'react'; import { makeExportClassName } from '@/utils/exportImage'; import { useDashboardsQuery } from '@/hooks/useDashboards'; import Tile from './Tile'; -import { Dashboard as DashboardType } from '@/@types/parseable/api/dashboards'; import { Layout } from 'react-grid-layout'; const { toggleCreateDashboardModal, toggleCreateTileModal, toggleDeleteTileModal } = dashboardsStoreReducers; @@ -57,7 +56,7 @@ const TilesView = (props: { onLayoutChange: (layout: Layout[]) => void }) => { }; const DeleteTileModal = () => { - const [activeDashboard, setDashboardsStore] = useDashboardsStore((store) => store.activeDashboard as DashboardType); + const [activeDashboard, setDashboardsStore] = useDashboardsStore((store) => store.activeDashboard); const [deleteTileModalOpen] = useDashboardsStore((store) => store.deleteTileModalOpen); const [deleteTileId] = useDashboardsStore((store) => store.deleteTileId); const selectedTile = _.find(activeDashboard?.tiles, (tile) => tile.tile_id === deleteTileId); @@ -69,11 +68,12 @@ const DeleteTileModal = () => { }, []); const onConfirm = useCallback(() => { - const allTiles = activeDashboard.tiles.filter((tile) => tile.tile_id !== selectedTile?.tile_id); - const tilesWithUpdatedOrder = assignOrderToTiles(allTiles); + const allTiles = activeDashboard?.tiles.filter((tile) => tile.tile_id !== selectedTile?.tile_id); + if (_.isEmpty(allTiles) || _.isUndefined(allTiles) || !activeDashboard) return; + const tilesWithUpdatedOrder = assignOrderToTiles(allTiles); updateDashboard({ dashboard: { ...activeDashboard, tiles: tilesWithUpdatedOrder }, onSuccess: onClose }); - }, [selectedTile?.tile_id, activeDashboard.tiles]); + }, [selectedTile?.tile_id, activeDashboard?.tiles]); if (!activeDashboard?.dashboard_id || !deleteTileId || !selectedTile) return null; From 4790971b6ec06c491888669fcef2901cd9d5a786 Mon Sep 17 00:00:00 2001 From: balaji-jr Date: Tue, 13 Aug 2024 10:59:53 +0530 Subject: [PATCH 17/34] hide timerange option in create dashboard when there is no toolbar --- src/pages/Dashboards/CreateDashboardModal.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/Dashboards/CreateDashboardModal.tsx b/src/pages/Dashboards/CreateDashboardModal.tsx index bad8806d..eeccbe25 100644 --- a/src/pages/Dashboards/CreateDashboardModal.tsx +++ b/src/pages/Dashboards/CreateDashboardModal.tsx @@ -128,7 +128,8 @@ const CreateDashboardModal = () => { key="description" {...form.getInputProps('description')} /> - + Time Range From 0c303b35ee3af9474f60e92d367ab69835f64b35 Mon Sep 17 00:00:00 2001 From: balaji-jr Date: Tue, 13 Aug 2024 14:36:55 +0530 Subject: [PATCH 19/34] close create dashboard modal on success --- src/pages/Dashboards/CreateDashboardModal.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/pages/Dashboards/CreateDashboardModal.tsx b/src/pages/Dashboards/CreateDashboardModal.tsx index 0835a0d8..0a8d3b29 100644 --- a/src/pages/Dashboards/CreateDashboardModal.tsx +++ b/src/pages/Dashboards/CreateDashboardModal.tsx @@ -84,10 +84,6 @@ const CreateDashboardModal = () => { } }, [createMode, editMode]); - const onCreateSuccess = useCallback(() => { - closeModal(); - }, []); - const onSubmit = useCallback(() => { const dashboard = form.values; const timeFilter = @@ -97,11 +93,11 @@ const CreateDashboardModal = () => { if (dashboardId) { updateDashboard({ dashboard: { ...dashboard, dashboard_id: dashboardId, time_filter: timeFilter }, - onSuccess: onCreateSuccess, + onSuccess: closeModal, }); } } else { - createDashboard({ dashboard: { ...dashboard, time_filter: timeFilter }, onSuccess: onCreateSuccess }); + createDashboard({ dashboard: { ...dashboard, time_filter: timeFilter }, onSuccess: closeModal }); } }, [form.values, editMode, createMode, timeRangeOptions]); From 0ea3f6fa1a2d9c6c9e65f8f7d21cf76c6e9a08b3 Mon Sep 17 00:00:00 2001 From: balaji-jr Date: Tue, 13 Aug 2024 15:11:55 +0530 Subject: [PATCH 20/34] tone down chart color scheme --- src/pages/Dashboards/Charts.tsx | 34 ++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/pages/Dashboards/Charts.tsx b/src/pages/Dashboards/Charts.tsx index 4bc0784a..bbd249ff 100644 --- a/src/pages/Dashboards/Charts.tsx +++ b/src/pages/Dashboards/Charts.tsx @@ -9,20 +9,20 @@ import { CodeHighlight } from '@mantine/code-highlight'; import { Log } from '@/@types/parseable/api/query'; export const chartColorsMap = { - 'black': 'dark.6', - 'gray': 'gray.6', - 'red': 'red.6', - 'pink': 'pink.6', - 'grape': 'grape.6', - 'violet': 'violet.6', - 'indigo': 'indigo.6', - 'cyan': 'cyan.6', - 'blue': 'blue.6', - 'teal': 'teal.6', - 'green': 'green.6', - 'lime': 'lime.6', - 'yellow': 'yellow.6', - 'orange': 'orange.6', + 'black': 'dark.5', + 'gray': 'gray.5', + 'red': 'red.5', + 'pink': 'pink.5', + 'grape': 'grape.5', + 'violet': 'violet.5', + 'indigo': 'indigo.5', + 'cyan': 'cyan.5', + 'blue': 'blue.5', + 'teal': 'teal.5', + 'green': 'green.5', + 'lime': 'lime.5', + 'yellow': 'yellow.5', + 'orange': 'orange.5', } export const colors = [ @@ -175,13 +175,13 @@ export const makeCircularChartData = (data: Log[], name_key: string, value_key: usedColors = [...usedColors, colorkey]; const colorKey = _.difference(colors, usedColors)[index] || nullColor; const color = colorKey in chartColorsMap ? chartColorsMap[colorKey as keyof typeof chartColorsMap] : nullColor; - return { topNArcs: [...topNArcs, { name: key, value, color: color || 'gray.6' }], index: index + 1 }; + return { topNArcs: [...topNArcs, { name: key, value, color: color || 'gray.4' }], index: index + 1 }; }, { topNArcs: [], index: 0 }, ); const restArcValue = _.sum(_.values(restObject)); - return [...topNArcs, ...(restArcValue !== 0 ? [{ name: 'Others', value: restArcValue, color: 'gray.6' }] : [])]; + return [...topNArcs, ...(restArcValue !== 0 ? [{ name: 'Others', value: restArcValue, color: 'gray.4' }] : [])]; } const makeSeriesData = (data: Log[], y_key: string[]) => { @@ -194,7 +194,7 @@ const makeSeriesData = (data: Log[], y_key: string[]) => { (acc, key: string, index: number) => { const colorKey = _.difference(colors, usedColors)[index] || nullColor; const color = colorKey in chartColorsMap ? chartColorsMap[colorKey as keyof typeof chartColorsMap] : nullColor; - return [...acc, { color: color || 'gray.6', name: key }]; + return [...acc, { color: color || 'gray.4', name: key }]; }, [], ); From 917f5dc2d1ff5d89e4faa288c3989716ad514906 Mon Sep 17 00:00:00 2001 From: balaji-jr Date: Tue, 13 Aug 2024 15:19:16 +0530 Subject: [PATCH 21/34] fix: unable to switch dashboards --- src/pages/Dashboards/SideBar.tsx | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/src/pages/Dashboards/SideBar.tsx b/src/pages/Dashboards/SideBar.tsx index af052de4..bac81fe2 100644 --- a/src/pages/Dashboards/SideBar.tsx +++ b/src/pages/Dashboards/SideBar.tsx @@ -7,8 +7,7 @@ import { useCallback } from 'react'; import _ from 'lodash'; import { Dashboard } from '@/@types/parseable/api/dashboards'; -const {selectDashboard, toggleCreateDashboardModal} = dashboardsStoreReducers; - +const { selectDashboard, toggleCreateDashboardModal } = dashboardsStoreReducers; interface DashboardItemProps extends Dashboard { activeDashboardId: undefined | string; onSelect: (id: string) => void; @@ -19,10 +18,14 @@ const DashboardListItem = (props: DashboardItemProps) => { const totalTiles = _.size(tiles); const isActive = dashboard_id === activeDashboardId; - const selectDashboard = useCallback(() => onSelect(dashboard_id), []) + const selectDashboard = useCallback(() => { + !isActive && onSelect(dashboard_id); + }, [isActive]); return ( - {name} + + {name} + {`${totalTiles} Tile${totalTiles === 1 ? '' : 's'}`} ); @@ -32,17 +35,25 @@ const DashboardList = () => { const [dashboards, setDashbaordsStore] = useDashboardsStore((store) => store.dashboards); const [activeDashboardId] = useDashboardsStore((store) => store.activeDashboard?.dashboard_id); - const onSelectDashboardId = useCallback((dashboardId: string) => { - if (activeDashboardId === dashboardId) return; + const onSelectDashboardId = useCallback( + (dashboardId: string) => { + if (activeDashboardId === dashboardId) return; - setDashbaordsStore((store) => selectDashboard(store, dashboardId)); - }, [activeDashboardId]); + setDashbaordsStore((store) => selectDashboard(store, dashboardId)); + }, + [activeDashboardId], + ); return ( {_.map(dashboards, (dashboard) => { return ( - + ); })} From 2c8c247390fa1ece2e7b49b63f20bc83d57a38cd Mon Sep 17 00:00:00 2001 From: balaji-jr Date: Wed, 14 Aug 2024 01:26:18 +0530 Subject: [PATCH 22/34] minor feedbacks --- src/components/Navbar/index.tsx | 6 +++++- src/hooks/useDashboards.tsx | 16 ++++++++++------ src/pages/Dashboards/CreateDashboardModal.tsx | 2 +- src/pages/Dashboards/CreateTileForm.tsx | 2 +- src/pages/Dashboards/Dashboard.tsx | 10 ++++++---- src/pages/Dashboards/SideBar.tsx | 2 +- src/pages/Dashboards/Toolbar.tsx | 8 +++++++- src/pages/Dashboards/index.tsx | 1 + .../Dashboards/providers/DashboardsProvider.ts | 16 ++++++++++++---- .../Dashboards/styles/DashboardView.module.css | 2 +- 10 files changed, 45 insertions(+), 20 deletions(-) diff --git a/src/components/Navbar/index.tsx b/src/components/Navbar/index.tsx index ed237fa6..c94ea54e 100644 --- a/src/components/Navbar/index.tsx +++ b/src/components/Navbar/index.tsx @@ -98,10 +98,11 @@ const Navbar: FC = () => { const [infoModalOpened, { toggle: toggleInfoModal, close: closeInfoModal }] = useDisclosure(false); const { getLogStreamListData } = useLogStream(); const { getUserRolesData, getUserRolesMutation } = useUser(); + const shouldRedirectToHome = _.isEmpty(userSpecificStreams) || _.isNil(userSpecificStreams); const navigateToPage = useCallback( (route: string) => { if (route === STREAM_ROUTE) { - if (_.isEmpty(userSpecificStreams) || _.isNil(userSpecificStreams)) return navigate('/'); + if (shouldRedirectToHome) return navigate('/'); const defaultStream = currentStream && currentStream.length !== 0 ? currentStream : userSpecificStreams[0].name; const stream = !streamName || streamName.length === 0 ? defaultStream : streamName; @@ -112,6 +113,9 @@ const Navbar: FC = () => { navigate(path); } } else { + if (shouldRedirectToHome && route === DASHBOARDS_ROUTE) { + return navigate('/'); + } return navigate(route); } }, diff --git a/src/hooks/useDashboards.tsx b/src/hooks/useDashboards.tsx index f94232b9..feb2c950 100644 --- a/src/hooks/useDashboards.tsx +++ b/src/hooks/useDashboards.tsx @@ -8,7 +8,7 @@ import { getDashboards, getQueryData, postDashboard, putDashboard, removeDashboa import { useCallback, useState } from 'react'; import { CreateDashboardType, TileQuery, TileQueryResponse, UpdateDashboardType } from '@/@types/parseable/api/dashboards'; -const { setDashboards, setTileData } = dashboardsStoreReducers; +const { setDashboards, setTileData, selectDashboard } = dashboardsStoreReducers; export const useDashboardsQuery = () => { const [, setDashbaordsStore] = useDashboardsStore((_store) => null); @@ -26,7 +26,6 @@ export const useDashboardsQuery = () => { onSuccess: (data) => { setDashbaordsStore((store) => setDashboards(store, data.data || [])); }, - // remove debug onError: () => { setDashbaordsStore((store) => setDashboards(store, [])); }, @@ -35,9 +34,13 @@ export const useDashboardsQuery = () => { const { mutate: createDashboard, isLoading: isCreatingDashboard } = useMutation( (data: { dashboard: CreateDashboardType; onSuccess?: () => void }) => postDashboard(data.dashboard, username || ''), { - onSuccess: (_data, variables) => { - variables.onSuccess && variables.onSuccess(); + onSuccess: (response, variables) => { + const { dashboard_id } = response.data; + if (_.isString(dashboard_id) && !_.isEmpty(dashboard_id)) { + setDashbaordsStore((store) => selectDashboard(store, null, response.data)); + } fetchDashboards(); + variables.onSuccess && variables.onSuccess(); notifySuccess({ message: 'Created Successfully' }); }, onError: (data: AxiosError) => { @@ -76,8 +79,9 @@ export const useDashboardsQuery = () => { (data: { dashboardId: string; onSuccess?: () => void }) => removeDashboard(data.dashboardId), { onSuccess: (_data, variables) => { - fetchDashboards(); - variables.onSuccess && variables.onSuccess(); + fetchDashboards().then(() => { + variables.onSuccess && variables.onSuccess(); + }); notifySuccess({ message: 'Deleted Successfully' }); }, onError: (data: AxiosError) => { diff --git a/src/pages/Dashboards/CreateDashboardModal.tsx b/src/pages/Dashboards/CreateDashboardModal.tsx index 0a8d3b29..40a606e0 100644 --- a/src/pages/Dashboards/CreateDashboardModal.tsx +++ b/src/pages/Dashboards/CreateDashboardModal.tsx @@ -151,7 +151,7 @@ const CreateDashboardModal = () => { diff --git a/src/pages/Dashboards/CreateTileForm.tsx b/src/pages/Dashboards/CreateTileForm.tsx index 45bf2cba..04ceffab 100644 --- a/src/pages/Dashboards/CreateTileForm.tsx +++ b/src/pages/Dashboards/CreateTileForm.tsx @@ -590,7 +590,7 @@ const CreateTileForm = () => { - diff --git a/src/pages/Dashboards/Dashboard.tsx b/src/pages/Dashboards/Dashboard.tsx index 850b14f0..0b215da3 100644 --- a/src/pages/Dashboards/Dashboard.tsx +++ b/src/pages/Dashboards/Dashboard.tsx @@ -23,11 +23,10 @@ const TilesView = (props: { onLayoutChange: (layout: Layout[]) => void }) => { const [layout] = useDashboardsStore((store) => store.layout); const hasNoTiles = _.size(activeDashboard?.tiles) < 1; const showNoTilesView = hasNoTiles || !activeDashboard; - if (showNoTilesView) return ; return ( - + void }) => { width={window.innerWidth - NAVBAR_WIDTH - DASHBOARDS_SIDEBAR_WIDTH} isResizable={false} margin={[16, 16]} - containerPadding={[20, 10]} + containerPadding={[16, 16]} compactType="horizontal" isDraggable={allowDrag} onLayoutChange={(layout) => props.onLayoutChange(layout)}> @@ -44,7 +43,10 @@ const TilesView = (props: { onLayoutChange: (layout: Layout[]) => void }) => { return (
diff --git a/src/pages/Dashboards/SideBar.tsx b/src/pages/Dashboards/SideBar.tsx index bac81fe2..83832316 100644 --- a/src/pages/Dashboards/SideBar.tsx +++ b/src/pages/Dashboards/SideBar.tsx @@ -45,7 +45,7 @@ const DashboardList = () => { ); return ( - + {_.map(dashboards, (dashboard) => { return ( { const onDelete = useCallback(() => { if (activeDashboard?.dashboard_id) { - deleteDashboard({ dashboardId: activeDashboard?.dashboard_id }); + deleteDashboard({ + dashboardId: activeDashboard?.dashboard_id, + onSuccess: () => { + closeModal(); + setConfirmText(''); + }, + }); } }, [activeDashboard?.dashboard_id]); diff --git a/src/pages/Dashboards/index.tsx b/src/pages/Dashboards/index.tsx index b8f19ab3..eb320f86 100644 --- a/src/pages/Dashboards/index.tsx +++ b/src/pages/Dashboards/index.tsx @@ -33,6 +33,7 @@ const Dashboards = () => { position: 'relative', flexDirection: 'row', width: '100%', + overflow: 'hidden', }}> {dashboards === null ? ( diff --git a/src/pages/Dashboards/providers/DashboardsProvider.ts b/src/pages/Dashboards/providers/DashboardsProvider.ts index ed319951..bc975649 100644 --- a/src/pages/Dashboards/providers/DashboardsProvider.ts +++ b/src/pages/Dashboards/providers/DashboardsProvider.ts @@ -107,7 +107,7 @@ type DashboardsStoreReducers = { setDashboards: (store: DashboardsStore, dashboards: Dashboard[]) => ReducerOutput; toggleCreateDashboardModal: (store: DashboardsStore, val: boolean) => ReducerOutput; toggleEditDashboardModal: (store: DashboardsStore, val: boolean) => ReducerOutput; - selectDashboard: (store: DashboardsStore, dashboardId: string) => ReducerOutput; + selectDashboard: (store: DashboardsStore, dashboardId?: string | null, dashboard?: Dashboard) => ReducerOutput; toggleCreateTileModal: (store: DashboardsStore, val: boolean, tileId?: string | null) => ReducerOutput; toggleVizEditorModal: (store: DashboardsStore, val: boolean) => ReducerOutput; toggleAllowDrag: (store: DashboardsStore) => ReducerOutput; @@ -167,6 +167,9 @@ const setDashboards = (store: DashboardsStore, dashboards: Dashboard[]) => { return defaultActiveDashboard; } })(); + + console.log({activeDashboardFromStore}) + return { dashboards, activeDashboard, @@ -174,13 +177,18 @@ const setDashboards = (store: DashboardsStore, dashboards: Dashboard[]) => { }; }; -const selectDashboard = (store: DashboardsStore, dashboardId: string) => { - const activeDashboard = _.find(store.dashboards, (dashboard) => dashboard.dashboard_id === dashboardId); +const selectDashboard = (store: DashboardsStore, dashboardId?: string | null, dashboard?: Dashboard) => { + const activeDashboard = + dashboard && _.isObject(dashboard) && !_.isEmpty(dashboard.dashboard_id) + ? dashboard + : _.find(store.dashboards, (dashboard) => dashboard.dashboard_id === dashboardId); + if (!activeDashboard) return {} + return { ...initialState, dashboards: store.dashboards, activeDashboard: activeDashboard || null, - layout: activeDashboard ? genLayout(activeDashboard.tiles) : [] + layout: activeDashboard ? genLayout(activeDashboard.tiles) : [], }; }; diff --git a/src/pages/Dashboards/styles/DashboardView.module.css b/src/pages/Dashboards/styles/DashboardView.module.css index 7524217c..94eb829d 100644 --- a/src/pages/Dashboards/styles/DashboardView.module.css +++ b/src/pages/Dashboards/styles/DashboardView.module.css @@ -32,5 +32,5 @@ } .tilesViewConatiner { - margin-top: 0.5rem; + } \ No newline at end of file From 39351f4a84cee2f182095be54b58701517e7ccbb Mon Sep 17 00:00:00 2001 From: balaji-jr Date: Wed, 14 Aug 2024 15:07:13 +0530 Subject: [PATCH 23/34] Placeholder text updates --- src/pages/Dashboards/Dashboard.tsx | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/pages/Dashboards/Dashboard.tsx b/src/pages/Dashboards/Dashboard.tsx index 0b215da3..7a871c3f 100644 --- a/src/pages/Dashboards/Dashboard.tsx +++ b/src/pages/Dashboards/Dashboard.tsx @@ -119,16 +119,15 @@ const NoDashboardsView = () => { }, []); return ( - + - Create Dashboard Title Placeholder + Create dashboard - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore - magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco + Create your first dashboard to visualize log events from various streams. - + @@ -147,10 +146,10 @@ const NoTilesView = () => { - Create Tile Title Placeholder + Add tiles to the dashboard - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore - magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco + A tile is single unit of visualization. It is a visualization window based on a SQL query. A dashboard is made + up of tiles. Create your first tile for this dashboard. From 5fdabae9f35183487bc18cd6f0e2a5902544b444 Mon Sep 17 00:00:00 2001 From: balaji-jr Date: Mon, 19 Aug 2024 23:20:07 +0530 Subject: [PATCH 24/34] added support for formatting tooltips and axis labels --- src/@types/parseable/api/dashboards.ts | 53 ++-- src/components/Navbar/index.tsx | 4 +- src/pages/Dashboards/Charts.tsx | 231 ++++++++++++++---- src/pages/Dashboards/CreateTileForm.tsx | 4 +- src/pages/Dashboards/Table.tsx | 30 ++- src/pages/Dashboards/Tile.tsx | 14 +- src/pages/Dashboards/VizEditorModal.tsx | 131 +++++++--- .../Dashboards/styles/VizEditor.module.css | 4 +- src/pages/Dashboards/utils.ts | 26 ++ src/pages/Home/index.tsx | 4 +- src/utils/formatBytes.ts | 8 +- 11 files changed, 375 insertions(+), 134 deletions(-) create mode 100644 src/pages/Dashboards/utils.ts diff --git a/src/@types/parseable/api/dashboards.ts b/src/@types/parseable/api/dashboards.ts index 42c252f6..3b1a3b84 100644 --- a/src/@types/parseable/api/dashboards.ts +++ b/src/@types/parseable/api/dashboards.ts @@ -1,13 +1,15 @@ -import { UseFormReturnType } from "@mantine/form"; -import { Log } from "./query"; +import { UseFormReturnType } from '@mantine/form'; +import { Log } from './query'; export type VizType = (typeof visualizations)[number]; export type TileSize = (typeof tileSizes)[number]; +export type UnitType = (typeof tickUnits)[number] | null; // viz type constants export const visualizations = ['pie-chart', 'donut-chart', 'line-chart', 'bar-chart', 'area-chart', 'table'] as const; -export const circularChartTypes = ['pie-chart', 'donut-chart']; -export const graphTypes = ['line-chart', 'bar-chart', 'area-chart'] +export const circularChartTypes = ['pie-chart', 'donut-chart'] as const; +export const tickUnits = ['bytes'] as const; +export const graphTypes = ['line-chart', 'bar-chart', 'area-chart'] as const; // vize size constants export const tileSizeWidthMap = { sm: 4, md: 6, lg: 8, xl: 12 }; @@ -16,7 +18,12 @@ export const tileSizes = ['sm', 'md', 'lg', 'xl']; export type ColorConfig = { field_name: string; color_palette: string; -} +}; + +export type TickConfig = { + key: string; + unit: string; +}; export type Visualization = { visualization_type: VizType; @@ -24,39 +31,39 @@ export type Visualization = { circular_chart_config?: null | { name_key: string; value_key: string } | {}; graph_config?: null | { x_key: string; y_keys: string[] } | {}; color_config: ColorConfig[]; + tick_config: TickConfig[]; }; export type Dashboard = { - name: string; - description: string; - refresh_interval: number; - tiles: Tile[]; - dashboard_id: string; - time_filter: null | { - from: string; - to: string; - }; + name: string; + description: string; + refresh_interval: number; + tiles: Tile[]; + dashboard_id: string; + time_filter: null | { + from: string; + to: string; + }; }; export type CreateDashboardType = Omit; export type UpdateDashboardType = Omit & { - tiles: EditTileType[]; + tiles: EditTileType[]; }; -export type TileQuery = {query: string, startTime: Date, endTime: Date} +export type TileQuery = { query: string; startTime: Date; endTime: Date }; export type TileData = Log[]; export type TileQueryResponse = { - fields: string[]; - records: TileData; -} + fields: string[]; + records: TileData; +}; -export interface FormOpts extends Omit { +export interface FormOpts extends Omit { isQueryValidated: boolean; data: TileQueryResponse; - visualization: Visualization; dashboardId: string | null; tile_id?: string; } @@ -73,5 +80,5 @@ export type Tile = { }; export type EditTileType = Omit & { - tile_id?: string; -}; \ No newline at end of file + tile_id?: string; +}; diff --git a/src/components/Navbar/index.tsx b/src/components/Navbar/index.tsx index c94ea54e..71ae63bf 100644 --- a/src/components/Navbar/index.tsx +++ b/src/components/Navbar/index.tsx @@ -7,7 +7,7 @@ import { IconServerCog, IconHomeStats, IconListDetails, - IconChartBar + IconChartInfographic } from '@tabler/icons-react'; import { FC, useCallback, useEffect } from 'react'; import { useLocation, useParams } from 'react-router-dom'; @@ -37,7 +37,7 @@ const navItems = [ route: HOME_ROUTE, }, { - icon: IconChartBar, + icon: IconChartInfographic, label: 'Dashboards', path: '/dashboards', route: DASHBOARDS_ROUTE, diff --git a/src/pages/Dashboards/Charts.tsx b/src/pages/Dashboards/Charts.tsx index bbd249ff..88617945 100644 --- a/src/pages/Dashboards/Charts.tsx +++ b/src/pages/Dashboards/Charts.tsx @@ -1,29 +1,30 @@ -import { TileData, TileQueryResponse } from '@/@types/parseable/api/dashboards'; -import { AreaChart, BarChart, DonutChart, LineChart, PieChart } from '@mantine/charts'; -import { Stack, Text } from '@mantine/core'; +import { TileData, TileQueryResponse, UnitType } from '@/@types/parseable/api/dashboards'; +import { AreaChart, BarChart, DonutChart, LineChart, PieChart, getFilteredChartTooltipPayload } from '@mantine/charts'; +import { Paper, Stack, Text } from '@mantine/core'; import _ from 'lodash'; import { circularChartTypes, graphTypes } from '@/@types/parseable/api/dashboards'; import { IconAlertTriangle } from '@tabler/icons-react'; -import classes from './styles/Charts.module.css' +import classes from './styles/Charts.module.css'; import { CodeHighlight } from '@mantine/code-highlight'; import { Log } from '@/@types/parseable/api/query'; +import { tickFormatter } from './utils'; export const chartColorsMap = { - 'black': 'dark.5', - 'gray': 'gray.5', - 'red': 'red.5', - 'pink': 'pink.5', - 'grape': 'grape.5', - 'violet': 'violet.5', - 'indigo': 'indigo.5', - 'cyan': 'cyan.5', - 'blue': 'blue.5', - 'teal': 'teal.5', - 'green': 'green.5', - 'lime': 'lime.5', - 'yellow': 'yellow.5', - 'orange': 'orange.5', -} + black: 'dark.5', + gray: 'gray.5', + red: 'red.5', + pink: 'pink.5', + grape: 'grape.5', + violet: 'violet.5', + indigo: 'indigo.5', + cyan: 'cyan.5', + blue: 'blue.5', + teal: 'teal.5', + green: 'green.5', + lime: 'lime.5', + yellow: 'yellow.5', + orange: 'orange.5', +}; export const colors = [ 'indigo', @@ -39,7 +40,7 @@ export const colors = [ 'red', 'green', ]; -export const nullColor = 'gray' +export const nullColor = 'gray'; export const getGraphVizComponent = (viz: string) => { if (viz === 'line-chart') { @@ -77,25 +78,33 @@ export type SeriesType = { export const isCircularChart = (viz: string) => _.includes(circularChartTypes, viz); export const isGraph = (viz: string) => _.includes(graphTypes, viz); -const invalidConfigMsg = "Invalid chart config" -const noDataMsg = "No data available" -const invalidDataMsg = "Invalid chart data" +const invalidConfigMsg = 'Invalid chart config'; +const noDataMsg = 'No data available'; +const invalidDataMsg = 'Invalid chart data'; const WarningView = (props: { msg: string | null }) => { return ( - - - {props.msg} + + + {props.msg} ); }; const validateCircularChartData = (data: CircularChartData) => { - return _.every(data, d => _.isNumber(d.value)) -} + return _.every(data, (d) => _.isNumber(d.value)); +}; -export const renderCircularChart = (opts: {queryResponse: TileQueryResponse | null, name_key: string, value_key: string, chart: string }) => { - const { queryResponse, name_key, value_key, chart } = opts; +export const renderCircularChart = (opts: { + queryResponse: TileQueryResponse | null; + name_key: string; + value_key: string; + chart: string; + unit: UnitType; +}) => { + const { queryResponse, name_key, value_key, chart, unit } = opts; const VizComponent = getCircularVizComponent(chart); const data = makeCircularChartData(queryResponse?.records || [], name_key, value_key); @@ -106,13 +115,13 @@ export const renderCircularChart = (opts: {queryResponse: TileQueryResponse | nu const warningMsg = isInvalidKey ? invalidConfigMsg : hasNoData ? noDataMsg : !isValidData ? invalidDataMsg : null; return ( - - {warningMsg ? : VizComponent ? : null} + + {warningMsg ? : VizComponent ? : null} ); -} +}; -export const renderJsonView = (opts: {queryResponse: TileQueryResponse | null}) => { +export const renderJsonView = (opts: { queryResponse: TileQueryResponse | null }) => { return ( ); -} +}; -export const renderGraph = (opts: {queryResponse: TileQueryResponse | null, x_key: string, y_keys: string[], chart: string}) => { - const { queryResponse, x_key, y_keys, chart } = opts; +export const renderGraph = (opts: { + queryResponse: TileQueryResponse | null; + x_key: string; + y_keys: string[]; + chart: string; + unit: UnitType; +}) => { + const { queryResponse, x_key, y_keys, chart, unit } = opts; const VizComponent = getGraphVizComponent(chart); const seriesData = makeSeriesData(queryResponse?.records || [], y_keys); - const data = queryResponse?.records || [] + const data = queryResponse?.records || []; const isInvalidKey = _.isEmpty(x_key) || _.isEmpty(y_keys); const hasNoData = _.isEmpty(seriesData) || _.isEmpty(data); const warningMsg = isInvalidKey ? invalidConfigMsg : hasNoData ? noDataMsg : null; return ( - {warningMsg ? : VizComponent ? : null} + {warningMsg ? ( + + ) : VizComponent ? ( + + ) : null} ); -} +}; export const makeCircularChartData = (data: Log[], name_key: string, value_key: string): CircularChartData => { if (!_.isArray(data)) return []; - const topN = 5; + const topN = 2; const chartData = _.reduce( data, (acc, rec: Log) => { @@ -182,7 +201,7 @@ export const makeCircularChartData = (data: Log[], name_key: string, value_key: const restArcValue = _.sum(_.values(restObject)); return [...topNArcs, ...(restArcValue !== 0 ? [{ name: 'Others', value: restArcValue, color: 'gray.4' }] : [])]; -} +}; const makeSeriesData = (data: Log[], y_key: string[]) => { if (!_.isArray(data)) return []; @@ -192,37 +211,141 @@ const makeSeriesData = (data: Log[], y_key: string[]) => { return _.reduce( y_key, (acc, key: string, index: number) => { - const colorKey = _.difference(colors, usedColors)[index] || nullColor; + const colorKey = _.difference(colors, usedColors)[index] || nullColor; const color = colorKey in chartColorsMap ? chartColorsMap[colorKey as keyof typeof chartColorsMap] : nullColor; return [...acc, { color: color || 'gray.4', name: key }]; }, [], ); -} +}; -const Donut = (props: { data: CircularChartData }) => { - return ; +const Donut = (props: { data: CircularChartData; unit: UnitType }) => { + return ( + ( + + ), + }} + /> + ); }; -const Pie = (props: { data: CircularChartData }) => { - return ; +const Pie = (props: { data: CircularChartData; unit: UnitType }) => { + return ( + ( + + ), + }} + /> + ); }; -const Line = (props: { data: TileData; dataKey: string; series: SeriesType }) => { +interface ChartTooltipProps { + label: string; + payload: Record[] | undefined; + unit: UnitType; + chartType: 'line' | 'bar' | 'area' | 'donut' | 'pie'; +} + +function ChartTooltip({ label, payload, unit, chartType }: ChartTooltipProps) { + if (!payload) return null; + + const sanitizedPayload = chartType === 'area' ? getFilteredChartTooltipPayload(payload) : payload; + return ( + + + {label} + + {sanitizedPayload.map((item: any) => { + const { name = '', value = null } = item; + return ( + + {name}: {tickFormatter(value, unit)} + + ); + })} + + ); +} + +const Line = (props: { data: TileData; dataKey: string; series: SeriesType; unit: UnitType }) => { return ( - + tickFormatter(value, props.unit), + }} + tooltipProps={{ + content: ({ label, payload }) => ( + + ), + }} + /> ); }; -const Bar = (props: { data: TileData; dataKey: string; series: SeriesType }) => { +const Bar = (props: { data: TileData; dataKey: string; series: SeriesType; unit: UnitType }) => { return ( - + tickFormatter(value, props.unit), + }} + tooltipProps={{ + content: ({ label, payload }) => ( + + ), + }} + /> ); }; -const Area = (props: { data: TileData; dataKey: string; series: SeriesType }) => { +const Area = (props: { data: TileData; dataKey: string; series: SeriesType; unit: UnitType }) => { return ( - + tickFormatter(value, props.unit), + }} + tooltipProps={{ + content: ({ label, payload }) => ( + + ), + }} + /> ); }; diff --git a/src/pages/Dashboards/CreateTileForm.tsx b/src/pages/Dashboards/CreateTileForm.tsx index 04ceffab..c220ad62 100644 --- a/src/pages/Dashboards/CreateTileForm.tsx +++ b/src/pages/Dashboards/CreateTileForm.tsx @@ -475,7 +475,7 @@ const Config = (props: { form: TileFormType; onChangeValue: (key: string, value: const sanitizeFormValues = (form: TileFormType, type: 'create' | 'update'): EditTileType => { const { name, description, query, visualization, tile_id, order } = form.values; - const { visualization_type, size, circular_chart_config, graph_config, color_config } = visualization; + const { visualization_type, size, circular_chart_config, graph_config, color_config, tick_config } = visualization; const vizElementConfig = isCircularChart(visualization_type) ? { circular_chart_config } : isGraph(visualization_type) @@ -490,6 +490,7 @@ const sanitizeFormValues = (form: TileFormType, type: 'create' | 'update'): Edit size, ...vizElementConfig, color_config, + tick_config }, order, ...(type === 'update' && _.isString(tile_id) ? { tile_id } : {}), @@ -500,6 +501,7 @@ const defaultVizOpts = { visualization_type: 'donut-chart' as 'donut-chart', size: 'sm', color_config: [], + tick_config: [], circular_chart_config: {}, graph_config: {}, }; diff --git a/src/pages/Dashboards/Table.tsx b/src/pages/Dashboards/Table.tsx index 2fd5a0a1..5a135e90 100644 --- a/src/pages/Dashboards/Table.tsx +++ b/src/pages/Dashboards/Table.tsx @@ -1,16 +1,21 @@ import { Stack, Table, Text } from '@mantine/core'; -import { TileQueryResponse } from '@/@types/parseable/api/dashboards'; +import { TickConfig, TileQueryResponse, UnitType } from '@/@types/parseable/api/dashboards'; import classes from './styles/Table.module.css'; import _ from 'lodash'; import { MutableRefObject, useEffect, useMemo, useRef, useState } from 'react'; import { IconAlertTriangle } from '@tabler/icons-react'; +import { tickFormatter } from './utils'; -const makeRowData = (data: TileQueryResponse) => { +const makeRowData = (data: TileQueryResponse, fieldUnitMap: Record) => { const { fields, records } = data; return _.map(records, (rec) => { return _.chain(rec) - .at(fields) - .map((i) => _.toString(i)) + .thru(rec => { + return _.map(fields, field => { + const colValue = _.get(rec, field, '-'); + return tickFormatter(colValue, fieldUnitMap[field]) + }) + }) .value(); }); }; @@ -24,11 +29,24 @@ const NoDataView = () => { ); }; -const TableViz = (props: { data: TileQueryResponse }) => { +const makeFieldUnitMap = (tick_config: TickConfig[]) => { + return _.reduce( + tick_config, + (acc, config) => { + return { ...acc, [config.key]: config.unit}; + }, + {}, + ); +}; + + +const TableViz = (props: { data: TileQueryResponse, tick_config: TickConfig[] }) => { const { data: { fields, records }, + tick_config } = props; - const rowData = useMemo(() => makeRowData({ fields, records }), []); + const fieldUnitMap = useMemo(() => makeFieldUnitMap(tick_config), [fields]) + const rowData = useMemo(() => makeRowData({ fields, records }, fieldUnitMap), [records]); const containerRef: MutableRefObject = useRef(null); const [initialHeight, setInitialHeight] = useState(0); diff --git a/src/pages/Dashboards/Tile.tsx b/src/pages/Dashboards/Tile.tsx index 029c6686..b20ae164 100644 --- a/src/pages/Dashboards/Tile.tsx +++ b/src/pages/Dashboards/Tile.tsx @@ -27,6 +27,7 @@ import { sanitiseSqlString } from '@/utils/sanitiseSqlString'; import Table from './Table'; import { downloadDataAsCSV, downloadDataAsJson } from '@/utils/exportHelpers'; import { makeExportData, useLogsStore } from '../Stream/providers/LogsProvider'; +import { getRandomUnitTypeForChart } from './utils'; const { toggleCreateTileModal, toggleDeleteTileModal } = dashboardsStoreReducers; @@ -50,13 +51,14 @@ const LoadingView = () => { const CircularChart = (props: { tile: TileType; data: TileQueryResponse }) => { const { tile, data } = props; const { - visualization: { visualization_type, circular_chart_config }, + visualization: { visualization_type, circular_chart_config, tick_config }, } = tile; const name_key = _.get(circular_chart_config, 'name_key', ''); const value_key = _.get(circular_chart_config, 'value_key', ''); + const unit = getRandomUnitTypeForChart(tick_config); return ( - {renderCircularChart({ queryResponse: data, name_key, value_key, chart: visualization_type })} + {renderCircularChart({ queryResponse: data, name_key, value_key, chart: visualization_type, unit })} ); }; @@ -64,13 +66,14 @@ const CircularChart = (props: { tile: TileType; data: TileQueryResponse }) => { const Graph = (props: { tile: TileType; data: TileQueryResponse }) => { const { tile, data } = props; const { - visualization: { visualization_type, graph_config }, + visualization: { visualization_type, graph_config, tick_config }, } = tile; const x_key = _.get(graph_config, 'x_key', ''); const y_keys = _.get(graph_config, 'y_keys', []); + const unit = getRandomUnitTypeForChart(tick_config); return ( - {renderGraph({ queryResponse: data, x_key, y_keys, chart: visualization_type })} + {renderGraph({ queryResponse: data, x_key, y_keys, chart: visualization_type, unit })} ); }; @@ -196,6 +199,7 @@ const Tile = (props: { id: string }) => { // }, []); const hasData = !_.isEmpty(tileData); const vizType = _.get(tile, 'visualization.visualization_type', null); + const tick_config = _.get(tile, 'visualization.tick_config', []); const Viz = getViz(vizType); return ( @@ -211,7 +215,7 @@ const Tile = (props: { id: string }) => { {!hasData && !isLoading && } {!isLoading && hasData && ( - {Viz && } + {Viz && } )} diff --git a/src/pages/Dashboards/VizEditorModal.tsx b/src/pages/Dashboards/VizEditorModal.tsx index 63ed5ae0..a2d60be1 100644 --- a/src/pages/Dashboards/VizEditorModal.tsx +++ b/src/pages/Dashboards/VizEditorModal.tsx @@ -1,12 +1,13 @@ -import { Button, Modal, MultiSelect, Select, Stack, Text } from '@mantine/core'; +import { ActionIcon, Button, CloseIcon, Modal, MultiSelect, Select, Stack, Text, Box } from '@mantine/core'; import classes from './styles/VizEditor.module.css'; import { useDashboardsStore, dashboardsStoreReducers } from './providers/DashboardsProvider'; import { useCallback, useEffect } from 'react'; import _ from 'lodash'; import { isCircularChart, renderCircularChart, renderGraph } from './Charts'; -import { TileFormType, tileSizes, visualizations } from '@/@types/parseable/api/dashboards'; -import { IconAlertTriangle } from '@tabler/icons-react'; +import { tickUnits, TileFormType, tileSizes, visualizations } from '@/@types/parseable/api/dashboards'; +import { IconAlertTriangle, IconPlus } from '@tabler/icons-react'; import TableViz from './Table'; +import { getRandomUnitTypeForChart } from './utils'; const { toggleVizEditorModal } = dashboardsStoreReducers; const inValidVizType = 'Select a visualization type'; @@ -22,39 +23,42 @@ const WarningView = (props: { msg: string | null }) => { const CircularChart = (props: { form: TileFormType }) => { const { - visualization: { visualization_type, circular_chart_config }, + visualization: { visualization_type, circular_chart_config, tick_config }, data, } = props.form.values; const name_key = _.get(circular_chart_config, 'name_key', ''); const value_key = _.get(circular_chart_config, 'value_key', ''); + const unit = getRandomUnitTypeForChart(tick_config) return ( - {renderCircularChart({ queryResponse: data, name_key, value_key, chart: visualization_type })} + {renderCircularChart({ queryResponse: data, name_key, value_key, chart: visualization_type, unit })} ); }; const Graph = (props: { form: TileFormType }) => { const { - visualization: { visualization_type, graph_config }, + visualization: { visualization_type, graph_config, tick_config }, data, } = props.form.values; const x_key = _.get(graph_config, 'x_key', ''); const y_keys = _.get(graph_config, 'y_keys', []); + const unit = getRandomUnitTypeForChart(tick_config); return ( - {renderGraph({ queryResponse: data, x_key, y_keys, chart: visualization_type })} + {renderGraph({ queryResponse: data, x_key, y_keys, chart: visualization_type, unit })} ); }; const Table = (props: { form: TileFormType }) => { - const data = props.form.values.data; + const {visualization: {tick_config}, data} = props.form.values; + return ( - + ); @@ -194,7 +198,7 @@ const GraphConfig = (props: { form: TileFormType }) => { ); }; -const TickConfig = (props: { form: TileFormType }) => { +const ChartConfig = (props: { form: TileFormType }) => { if ( props.form.values.visualization.visualization_type === 'table' || _.isEmpty(props.form.values.visualization.visualization_type) @@ -216,32 +220,91 @@ const TickConfig = (props: { form: TileFormType }) => { ); }; +const AddTickConfigButton = (props: {onClick: () => void;}) => { + return ( + + + + ); +} + +const TickUnitTypeConfig = (props: { form: TileFormType }) => { + const { + form: { + values: { + data, + visualization: { tick_config }, + }, + }, + } = props; + + const fieldPath = 'visualization.tick_config' + const onAddConfig = useCallback(() => { + props.form.insertListItem(fieldPath, { key: '', unit: tickUnits[0] }) + }, []) + + return ( + + {_.map(tick_config, (_tick, index) => { + const keyFieldPath = fieldPath + '.' + index + '.key'; + const unitFieldPath = fieldPath + '.' + index + '.unit'; + return ( + + + + props.form.removeListItem(fieldPath, index)} variant="light"> + + + + + ); + })} + + + ); +}; + +const TickConfig = (props: { form: TileFormType }) => { + if ( + props.form.values.visualization.visualization_type === 'table' || + _.isEmpty(props.form.values.visualization.visualization_type) + ) + return null; + + return ( + + + Tick Config + + + + ); +}; + const Config = (props: { form: TileFormType }) => { return ( - - + + + - {/* - Colors - - {_.map(dataKeys, (dataKey) => { - return ( - - - store.instanceConfig?.llmActive); - const containerRef:MutableRefObject = useRef(null); + const containerRef: MutableRefObject = useRef(null); const [localStream, setLocalStream] = useState(''); const [fields, setFields] = useState([]); const [initialHeight, setInitialHeight] = useState(0); @@ -333,10 +340,9 @@ const Query = (props: { form: TileFormType; onChangeValue: (key: string, value: fetchTileData({ query: santizedQuery, startTime: dayjs(from).toDate(), endTime: dayjs(to).toDate() }); }, [query, dashboardId, dashboards, timeRange]); - const onStreamSelect = useCallback((val: string | null) => { setLocalStream(val || ''); - }, []) + }, []); const isValidStream = !_.isEmpty(localStream); const handleAIGenerate = useCallback(() => { @@ -352,15 +358,23 @@ const Query = (props: { form: TileFormType; onChangeValue: (key: string, value: Query - + - {errorMsg && {errorMsg}} - + - {llmActive? ( + {llmActive ? ( setAiQuery(e.target.value)} - placeholder={isValidStream ? "Enter plain text to generate SQL query using OpenAI" : "Choose a schema to generate AI query"} + placeholder={ + isValidStream + ? 'Enter plain text to generate SQL query using OpenAI' + : 'Choose a schema to generate AI query' + } style={{ flex: 1 }} disabled={!isValidStream} /> - @@ -413,7 +436,7 @@ const Query = (props: { form: TileFormType; onChangeValue: (key: string, value: alignItems: 'flex-end', justifyContent: 'flex-end', padding: '1rem 0', - ...(errorMsg ? {display: 'none'} : {}) + ...(errorMsg ? { display: 'none' } : {}), }}> @@ -467,7 +488,7 @@ const Config = (props: { form: TileFormType; onChangeValue: (key: string, value: {...form.getInputProps('description')} /> - + ); @@ -490,7 +511,7 @@ const sanitizeFormValues = (form: TileFormType, type: 'create' | 'update'): Edit size, ...vizElementConfig, color_config, - tick_config + tick_config, }, order, ...(type === 'update' && _.isString(tile_id) ? { tile_id } : {}), @@ -523,7 +544,12 @@ const genTileFormOpts = (opts: { tileData: TileQueryResponse; }) => { const { activeDashboard, editTileId } = opts; - if (!editTileId) return {...defaultFormOpts, dashboardId: activeDashboard?.dashboard_id || null, order: _.size(activeDashboard?.tiles) + 1}; + if (!editTileId) + return { + ...defaultFormOpts, + dashboardId: activeDashboard?.dashboard_id || null, + order: _.size(activeDashboard?.tiles) + 1, + }; const currentTile = _.find(activeDashboard?.tiles, (tile) => tile.tile_id === editTileId); if (!currentTile) diff --git a/src/pages/Dashboards/Table.tsx b/src/pages/Dashboards/Table.tsx index 5a135e90..a173a5ba 100644 --- a/src/pages/Dashboards/Table.tsx +++ b/src/pages/Dashboards/Table.tsx @@ -10,11 +10,11 @@ const makeRowData = (data: TileQueryResponse, fieldUnitMap: Record { return _.chain(rec) - .thru(rec => { - return _.map(fields, field => { - const colValue = _.get(rec, field, '-'); - return tickFormatter(colValue, fieldUnitMap[field]) - }) + .thru((rec) => { + return _.map(fields, (field) => { + const colValue = _.get(rec, field, '-'); + return tickFormatter(colValue, fieldUnitMap[field]); + }); }) .value(); }); @@ -33,19 +33,18 @@ const makeFieldUnitMap = (tick_config: TickConfig[]) => { return _.reduce( tick_config, (acc, config) => { - return { ...acc, [config.key]: config.unit}; + return { ...acc, [config.key]: config.unit }; }, {}, ); }; - -const TableViz = (props: { data: TileQueryResponse, tick_config: TickConfig[] }) => { +const TableViz = (props: { data: TileQueryResponse; tick_config: TickConfig[] }) => { const { data: { fields, records }, - tick_config + tick_config, } = props; - const fieldUnitMap = useMemo(() => makeFieldUnitMap(tick_config), [fields]) + const fieldUnitMap = useMemo(() => makeFieldUnitMap(tick_config), [fields]); const rowData = useMemo(() => makeRowData({ fields, records }, fieldUnitMap), [records]); const containerRef: MutableRefObject = useRef(null); diff --git a/src/pages/Dashboards/Toolbar.tsx b/src/pages/Dashboards/Toolbar.tsx index d8db9a6d..03f5000f 100644 --- a/src/pages/Dashboards/Toolbar.tsx +++ b/src/pages/Dashboards/Toolbar.tsx @@ -9,7 +9,8 @@ import { useDashboardsQuery } from '@/hooks/useDashboards'; import _ from 'lodash'; import ReactGridLayout, { Layout } from 'react-grid-layout'; -const { toggleEditDashboardModal, toggleAllowDrag, toggleCreateTileModal, toggleDeleteDashboardModal } = dashboardsStoreReducers; +const { toggleEditDashboardModal, toggleAllowDrag, toggleCreateTileModal, toggleDeleteDashboardModal } = + dashboardsStoreReducers; const tileIdsbyOrder = (layout: Layout[]) => { return layout @@ -23,15 +24,15 @@ const tileIdsbyOrder = (layout: Layout[]) => { .map((item) => item.i); }; -const EditLayoutButton = (props: {layoutRef: React.MutableRefObject}) => { +const EditLayoutButton = (props: { layoutRef: React.MutableRefObject }) => { const [allowDrag, setDashbaordsStore] = useDashboardsStore((store) => store.allowDrag); - const [activeDashboard] = useDashboardsStore(store => store.activeDashboard) + const [activeDashboard] = useDashboardsStore((store) => store.activeDashboard); const onToggle = useCallback(() => { setDashbaordsStore(toggleAllowDrag); }, []); - const {updateDashboard, isUpdatingDashboard} = useDashboardsQuery(); + const { updateDashboard, isUpdatingDashboard } = useDashboardsQuery(); const onClick = useCallback(() => { if (allowDrag && activeDashboard) { const allTiles = activeDashboard.tiles; @@ -40,7 +41,6 @@ const EditLayoutButton = (props: {layoutRef: React.MutableRefObject { const [activeDashboard, setDashbaordsStore] = useDashboardsStore((store) => store.activeDashboard); const [deleteDashboardModalOpen] = useDashboardsStore((store) => store.deleteDashboardModalOpen); const [confirmText, setConfirmText] = useState(''); - const {isDeleting, deleteDashboard} = useDashboardsQuery() + const { isDeleting, deleteDashboard } = useDashboardsQuery(); const closeModal = useCallback(() => { setDashbaordsStore((store) => toggleDeleteDashboardModal(store, false)); }, []); @@ -148,11 +148,11 @@ const renderDeleteIcon = () => ; const DeleteDashboardButton = () => { const [_store, setDashbaordsStore] = useDashboardsStore((_store) => null); - const onClick = useCallback(() => setDashbaordsStore(store => toggleDeleteDashboardModal(store, true)), []); + const onClick = useCallback(() => setDashbaordsStore((store) => toggleDeleteDashboardModal(store, true)), []); return ; }; -const Toolbar = (props: {layoutRef: React.MutableRefObject}) => { +const Toolbar = (props: { layoutRef: React.MutableRefObject }) => { const [activeDashboard, setDashbaordsStore] = useDashboardsStore((store) => store.activeDashboard); const openEditDashboardModal = useCallback(() => { setDashbaordsStore((store) => toggleEditDashboardModal(store, true)); @@ -167,7 +167,7 @@ const Toolbar = (props: {layoutRef: React.MutableRefObject - + @@ -183,9 +183,9 @@ const Toolbar = (props: {layoutRef: React.MutableRefObject - - - + + + ); diff --git a/src/pages/Dashboards/VizEditorModal.tsx b/src/pages/Dashboards/VizEditorModal.tsx index a2d60be1..f935345c 100644 --- a/src/pages/Dashboards/VizEditorModal.tsx +++ b/src/pages/Dashboards/VizEditorModal.tsx @@ -28,7 +28,7 @@ const CircularChart = (props: { form: TileFormType }) => { } = props.form.values; const name_key = _.get(circular_chart_config, 'name_key', ''); const value_key = _.get(circular_chart_config, 'value_key', ''); - const unit = getRandomUnitTypeForChart(tick_config) + const unit = getRandomUnitTypeForChart(tick_config); return ( @@ -53,12 +53,15 @@ const Graph = (props: { form: TileFormType }) => { }; const Table = (props: { form: TileFormType }) => { - const {visualization: {tick_config}, data} = props.form.values; + const { + visualization: { tick_config }, + data, + } = props.form.values; return ( - + ); @@ -220,15 +223,19 @@ const ChartConfig = (props: { form: TileFormType }) => { ); }; -const AddTickConfigButton = (props: {onClick: () => void;}) => { +const AddTickConfigButton = (props: { onClick: () => void }) => { return ( - ); -} +}; const TickUnitTypeConfig = (props: { form: TileFormType }) => { const { @@ -240,10 +247,10 @@ const TickUnitTypeConfig = (props: { form: TileFormType }) => { }, } = props; - const fieldPath = 'visualization.tick_config' + const fieldPath = 'visualization.tick_config'; const onAddConfig = useCallback(() => { - props.form.insertListItem(fieldPath, { key: '', unit: tickUnits[0] }) - }, []) + props.form.insertListItem(fieldPath, { key: '', unit: tickUnits[0] }); + }, []); return ( @@ -293,15 +300,15 @@ const TickConfig = (props: { form: TileFormType }) => { Tick Config - + ); }; const Config = (props: { form: TileFormType }) => { return ( - - + + @@ -325,7 +332,7 @@ const VizEditorModal = (props: { form: TileFormType }) => { centered size="90rem" title={'Edit Visualization'} - styles={{ body: { padding: '0 1rem' }, header: { padding: '1rem', paddingBottom: '0' }}} + styles={{ body: { padding: '0 1rem' }, header: { padding: '1rem', paddingBottom: '0' } }} classNames={{ title: classes.modalTitle }}> diff --git a/src/pages/Dashboards/providers/DashboardsProvider.ts b/src/pages/Dashboards/providers/DashboardsProvider.ts index bc975649..5877b2c0 100644 --- a/src/pages/Dashboards/providers/DashboardsProvider.ts +++ b/src/pages/Dashboards/providers/DashboardsProvider.ts @@ -11,13 +11,13 @@ export const sortTilesByOrder = (tiles: Tile[], idsByOrder: string[]): Tile[] => }) .compact() .value(); -} +}; export const assignOrderToTiles = (tiles: Tile[]) => { return _.map(tiles, (tile, index) => { return { ...tile, order: index + 1 }; }); -} +}; export const genLayout = (tiles: Tile[]): Layout[] => { return _.reduce( @@ -79,8 +79,8 @@ type DashboardsStore = { editTileId: string | null; tilesData: { [key: string]: TileQueryResponse; - }, - layout: Layout[], + }; + layout: Layout[]; deleteTileModalOpen: boolean; deleteTileId: string | null; }; @@ -95,10 +95,10 @@ const initialState: DashboardsStore = { vizEditorModalOpen: false, allowDrag: false, editTileId: null, - tilesData: {}, + tilesData: {}, layout: [], deleteTileModalOpen: false, - deleteTileId: null + deleteTileId: null, }; type ReducerOutput = Partial; @@ -132,7 +132,7 @@ const toggleEditDashboardModal = (_store: DashboardsStore, val: boolean) => { const toggleCreateTileModal = (_store: DashboardsStore, val: boolean, tileId: string | null = null) => { return { createTileFormOpen: val, - editTileId: tileId + editTileId: tileId, }; }; @@ -142,7 +142,6 @@ const toggleVizEditorModal = (_store: DashboardsStore, val: boolean) => { }; }; - const toggleDeleteDashboardModal = (_store: DashboardsStore, val: boolean) => { return { deleteDashboardModalOpen: val, @@ -151,7 +150,7 @@ const toggleDeleteDashboardModal = (_store: DashboardsStore, val: boolean) => { const toggleAllowDrag = (store: DashboardsStore) => { return { - allowDrag: !store.allowDrag + allowDrag: !store.allowDrag, }; }; @@ -168,12 +167,10 @@ const setDashboards = (store: DashboardsStore, dashboards: Dashboard[]) => { } })(); - console.log({activeDashboardFromStore}) - return { dashboards, activeDashboard, - layout: activeDashboard ? genLayout(activeDashboard.tiles) : [] + layout: activeDashboard ? genLayout(activeDashboard.tiles) : [], }; }; @@ -182,7 +179,7 @@ const selectDashboard = (store: DashboardsStore, dashboardId?: string | null, da dashboard && _.isObject(dashboard) && !_.isEmpty(dashboard.dashboard_id) ? dashboard : _.find(store.dashboards, (dashboard) => dashboard.dashboard_id === dashboardId); - if (!activeDashboard) return {} + if (!activeDashboard) return {}; return { ...initialState, @@ -196,17 +193,17 @@ const setTileData = (store: DashboardsStore, tileId: string, data: TileQueryResp return { tilesData: { ...store.tilesData, - [tileId]: data - } - } -} + [tileId]: data, + }, + }; +}; const toggleDeleteTileModal = (_store: DashboardsStore, val: boolean, tileId: string | null) => { return { deleteTileModalOpen: val, - deleteTileId: tileId - } -} + deleteTileId: tileId, + }; +}; const resetTilesData = (_store: DashboardsStore) => { return { @@ -227,7 +224,7 @@ const dashboardsStoreReducers: DashboardsStoreReducers = { toggleDeleteDashboardModal, setTileData, toggleDeleteTileModal, - resetTilesData + resetTilesData, }; export { DashbaordsProvider, useDashboardsStore, dashboardsStoreReducers }; diff --git a/src/pages/Dashboards/styles/Charts.module.css b/src/pages/Dashboards/styles/Charts.module.css index 8d9b7016..5316cc28 100644 --- a/src/pages/Dashboards/styles/Charts.module.css +++ b/src/pages/Dashboards/styles/Charts.module.css @@ -6,8 +6,3 @@ .warningIcon { color: var(--mantine-color-gray-5); } - -.warningViewContainer { - /* border: 1px solid var(--mantine-color-gray-2); */ - /* border-radius: 0.2rem; */ -} \ No newline at end of file diff --git a/src/pages/Dashboards/styles/CreateDashboardModal.module.css b/src/pages/Dashboards/styles/CreateDashboardModal.module.css index 949ef166..cf0671b1 100644 --- a/src/pages/Dashboards/styles/CreateDashboardModal.module.css +++ b/src/pages/Dashboards/styles/CreateDashboardModal.module.css @@ -1,9 +1,9 @@ .modalTitle { - font-size: 0.9rem; - font-weight: 600; + font-size: 0.9rem; + font-weight: 600; } .fieldTitle { font-weight: 500; font-size: 0.7rem; -} +} \ No newline at end of file diff --git a/src/pages/Dashboards/styles/DashboardView.module.css b/src/pages/Dashboards/styles/DashboardView.module.css index 94eb829d..2cb77ecf 100644 --- a/src/pages/Dashboards/styles/DashboardView.module.css +++ b/src/pages/Dashboards/styles/DashboardView.module.css @@ -9,11 +9,11 @@ } .dashboardIconContainer { - width: fit-content; - background-color: var(--mantine-color-brandPrimary-0); - border-radius: 50%; - padding: 0.5rem; - align-self: center; + width: fit-content; + background-color: var(--mantine-color-brandPrimary-0); + border-radius: 50%; + padding: 0.5rem; + align-self: center; } .dashboardIcon { @@ -30,7 +30,3 @@ font-size: 0.8rem; text-align: center; } - -.tilesViewConatiner { - -} \ No newline at end of file diff --git a/src/pages/Dashboards/styles/ReactGridLayout.css b/src/pages/Dashboards/styles/ReactGridLayout.css index 2b3fd985..05e09f37 100644 --- a/src/pages/Dashboards/styles/ReactGridLayout.css +++ b/src/pages/Dashboards/styles/ReactGridLayout.css @@ -1,4 +1,4 @@ .react-grid-item.react-grid-placeholder { background-color: var(--mantine-color-gray-3); border-radius: 0.4rem; -} \ No newline at end of file +} diff --git a/src/pages/Dashboards/styles/toolbar.module.css b/src/pages/Dashboards/styles/toolbar.module.css index 397eabfe..b1139485 100644 --- a/src/pages/Dashboards/styles/toolbar.module.css +++ b/src/pages/Dashboards/styles/toolbar.module.css @@ -20,19 +20,20 @@ } .addTileBtn { - background-color: white; - color: var(--mantine-color-gray-7); - border: 1px #e9ecef solid; + background-color: white; + color: var(--mantine-color-gray-7); + border: 1px #e9ecef solid; border-radius: rem(8px); - font-size: 0.65rem; - &:hover { - color: black; - } - } - - .addTileBtn:hover { + font-size: 0.65rem; + + &:hover { + color: black; + } +} + +.addTileBtn:hover { background-color: #E0E0E0; - } +} .editLayoutBtn:hover { background-color: #E0E0E0; diff --git a/src/pages/Dashboards/utils.ts b/src/pages/Dashboards/utils.ts index 2e01d221..bb978f42 100644 --- a/src/pages/Dashboards/utils.ts +++ b/src/pages/Dashboards/utils.ts @@ -15,7 +15,7 @@ const formatTickValue = (value: any, unit: (typeof tickUnits)[number] | null) => if (unit === null) { return HumanizeNumber(value); } else if (unit === 'bytes') { - return formatBytes(value) + return formatBytes(value); } else { return value; } diff --git a/src/pages/Stream/components/Querier/SaveFilterModal.tsx b/src/pages/Stream/components/Querier/SaveFilterModal.tsx index 9d807d4f..c12b7708 100644 --- a/src/pages/Stream/components/Querier/SaveFilterModal.tsx +++ b/src/pages/Stream/components/Querier/SaveFilterModal.tsx @@ -10,7 +10,7 @@ import useSavedFiltersQuery from '@/hooks/useSavedFilters'; import Cookies from 'js-cookie'; import timeRangeUtils from '@/utils/timeRangeUtils'; -const {defaultTimeRangeOption, makeTimeRangeOptions, getDefaultTimeRangeOption} = timeRangeUtils; +const { defaultTimeRangeOption, makeTimeRangeOptions, getDefaultTimeRangeOption } = timeRangeUtils; const { toggleSaveFiltersModal } = filterStoreReducers; interface FormObjectType extends Omit { diff --git a/src/routes/elements.tsx b/src/routes/elements.tsx index 3b5697b5..573281bb 100644 --- a/src/routes/elements.tsx +++ b/src/routes/elements.tsx @@ -85,4 +85,4 @@ export const SystemsElement: FC = () => { ); -}; \ No newline at end of file +}; diff --git a/src/routes/index.tsx b/src/routes/index.tsx index ad5611db..69b89259 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -6,14 +6,22 @@ import { USERS_MANAGEMENT_ROUTE, CLUSTER_ROUTE, STREAM_ROUTE, - DASHBOARDS_ROUTE + DASHBOARDS_ROUTE, } from '@/constants/routes'; import FullPageLayout from '@/layouts/FullPageLayout'; import NotFound from '@/pages/Errors/NotFound'; import type { FC } from 'react'; import { Route, Routes } from 'react-router-dom'; import PrivateRoute from './PrivateRoute'; -import { HomeElement, LoginElement, StreamElement, MainLayoutElement, SystemsElement, UsersElement, DashboardsElement } from './elements'; +import { + HomeElement, + LoginElement, + StreamElement, + MainLayoutElement, + SystemsElement, + UsersElement, + DashboardsElement, +} from './elements'; import AccessSpecificRoute from './AccessSpecificRoute'; import OIDCNotConFigured from '@/pages/Errors/OIDC'; @@ -44,4 +52,4 @@ const AppRouter: FC = () => { ); }; -export default AppRouter; \ No newline at end of file +export default AppRouter; diff --git a/src/utils/exportImage.ts b/src/utils/exportImage.ts index 2240c822..e3d512ff 100644 --- a/src/utils/exportImage.ts +++ b/src/utils/exportImage.ts @@ -2,11 +2,11 @@ import html2canvas from 'html2canvas'; import _ from 'lodash'; export const makeExportClassName = (name: string) => { - const sanitizedName = _.replace(name, /\./g, '-'); - return `png-capture-${sanitizedName}` -} + const sanitizedName = _.replace(name, /\./g, '-'); + return `png-capture-${sanitizedName}`; +}; -const handleCapture = (opts: { className: string, fileName: string }) => { +const handleCapture = (opts: { className: string; fileName: string }) => { const { className, fileName = 'png-export' } = opts; try { const element = document.querySelector(`.${className}`) as HTMLElement; @@ -24,4 +24,4 @@ const handleCapture = (opts: { className: string, fileName: string }) => { } }; -export default handleCapture; \ No newline at end of file +export default handleCapture; diff --git a/src/utils/sanitiseSqlString.ts b/src/utils/sanitiseSqlString.ts index 220b1447..7cb92f25 100644 --- a/src/utils/sanitiseSqlString.ts +++ b/src/utils/sanitiseSqlString.ts @@ -1,23 +1,28 @@ import { notify } from './notification'; import { LOAD_LIMIT } from '@/pages/Stream/providers/LogsProvider'; -export const sanitiseSqlString = (sqlString: string, shouldNotify:boolean = true, limit: number = LOAD_LIMIT): string => { - const withoutComments = sqlString.replace(/--.*$/gm, ''); - const withoutNewLines = withoutComments.replace(/\n/g, ' '); - const withoutTrailingSemicolon = withoutNewLines.replace(/;/g, '').trim(); - const limitRegex = /limit\s+(\d+)/i; +export const sanitiseSqlString = ( + sqlString: string, + shouldNotify: boolean = true, + limit: number = LOAD_LIMIT, +): string => { + const withoutComments = sqlString.replace(/--.*$/gm, ''); + const withoutNewLines = withoutComments.replace(/\n/g, ' '); + const withoutTrailingSemicolon = withoutNewLines.replace(/;/g, '').trim(); + const limitRegex = /limit\s+(\d+)/i; - if (limitRegex.test(withoutTrailingSemicolon)) { - return withoutTrailingSemicolon.replace(limitRegex, (match, p1) => { - const currentLimit = parseInt(p1, 10); - if (currentLimit > limit) { - shouldNotify && notify({ message: `Limit exceeds the default load limit. Replacing with default limit - ${limit}` }); - return `LIMIT ${limit}`; - } - return match; - }); - } + if (limitRegex.test(withoutTrailingSemicolon)) { + return withoutTrailingSemicolon.replace(limitRegex, (match, p1) => { + const currentLimit = parseInt(p1, 10); + if (currentLimit > limit) { + shouldNotify && + notify({ message: `Limit exceeds the default load limit. Replacing with default limit - ${limit}` }); + return `LIMIT ${limit}`; + } + return match; + }); + } - shouldNotify && notify({ message: `Default limit used i.e - ${limit}` }); - return `${withoutTrailingSemicolon} LIMIT ${limit}`; + shouldNotify && notify({ message: `Default limit used i.e - ${limit}` }); + return `${withoutTrailingSemicolon} LIMIT ${limit}`; }; diff --git a/src/utils/timeRangeUtils.ts b/src/utils/timeRangeUtils.ts index b48e5cd0..284cca7b 100644 --- a/src/utils/timeRangeUtils.ts +++ b/src/utils/timeRangeUtils.ts @@ -1,5 +1,5 @@ -import dayjs from "dayjs"; -import _ from "lodash"; +import dayjs from 'dayjs'; +import _ from 'lodash'; const defaultTimeRangeOption = { value: 'none', @@ -40,8 +40,8 @@ const makeTimeRangeOptions = ({ }; const makeTimeRangeLabel = (startTime: Date | string, endTime: Date | string) => { - return `${dayjs(startTime).format('hh:mm A DD MMM YY')} to ${dayjs(endTime).format('hh:mm A DD MMM YY')}` -} + return `${dayjs(startTime).format('hh:mm A DD MMM YY')} to ${dayjs(endTime).format('hh:mm A DD MMM YY')}`; +}; // to optimize performace, it has been decided to round off the time at the given level // so making the end-time inclusive @@ -57,12 +57,12 @@ const getDefaultTimeRangeOption = ( }; const timeRangeUtils = { - defaultTimeRangeOption, + defaultTimeRangeOption, - makeTimeRangeOptions, - makeTimeRangeLabel, + makeTimeRangeOptions, + makeTimeRangeLabel, optimizeEndTime, - getDefaultTimeRangeOption -} + getDefaultTimeRangeOption, +}; -export default timeRangeUtils; \ No newline at end of file +export default timeRangeUtils; From 37b523cb724e3d6791fb80a7aed269558cb9dfdc Mon Sep 17 00:00:00 2001 From: balaji-jr Date: Tue, 20 Aug 2024 00:35:03 +0530 Subject: [PATCH 27/34] added line clamp for tile title and description --- src/pages/Dashboards/Tile.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/Dashboards/Tile.tsx b/src/pages/Dashboards/Tile.tsx index b20ae164..e41033f2 100644 --- a/src/pages/Dashboards/Tile.tsx +++ b/src/pages/Dashboards/Tile.tsx @@ -206,8 +206,8 @@ const Tile = (props: { id: string }) => { - {tile.name} - {tile.description} + {tile.name} + {tile.description} From 6ab8e5ff9569a4be76fa32398e6151bcbdc3b45d Mon Sep 17 00:00:00 2001 From: balaji-jr Date: Tue, 20 Aug 2024 00:37:05 +0530 Subject: [PATCH 28/34] update top N arcs count for circular charts --- src/pages/Dashboards/Charts.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Dashboards/Charts.tsx b/src/pages/Dashboards/Charts.tsx index 88617945..9b4bafed 100644 --- a/src/pages/Dashboards/Charts.tsx +++ b/src/pages/Dashboards/Charts.tsx @@ -165,7 +165,7 @@ export const renderGraph = (opts: { export const makeCircularChartData = (data: Log[], name_key: string, value_key: string): CircularChartData => { if (!_.isArray(data)) return []; - const topN = 2; + const topN = 5; const chartData = _.reduce( data, (acc, rec: Log) => { From c637934949705ae91d713155da44123667095191 Mon Sep 17 00:00:00 2001 From: balaji-jr Date: Tue, 20 Aug 2024 16:53:06 +0530 Subject: [PATCH 29/34] fix for stored time range and dashbboad icon update --- src/components/Navbar/index.tsx | 4 +-- src/hooks/useDashboards.tsx | 19 +++++++----- src/pages/Dashboards/Charts.tsx | 1 + src/pages/Dashboards/CreateDashboardModal.tsx | 2 +- src/pages/Dashboards/CreateTileForm.tsx | 2 +- src/pages/Dashboards/Dashboard.tsx | 2 +- src/pages/Dashboards/SideBar.tsx | 10 ++++--- src/pages/Dashboards/Toolbar.tsx | 4 +-- src/pages/Dashboards/hooks.ts | 29 +++++++++++++++++++ src/pages/Dashboards/index.tsx | 6 ++-- 10 files changed, 59 insertions(+), 20 deletions(-) create mode 100644 src/pages/Dashboards/hooks.ts diff --git a/src/components/Navbar/index.tsx b/src/components/Navbar/index.tsx index bbc86cb9..2c63ca9d 100644 --- a/src/components/Navbar/index.tsx +++ b/src/components/Navbar/index.tsx @@ -7,7 +7,7 @@ import { IconServerCog, IconHomeStats, IconListDetails, - IconChartInfographic, + IconLayoutDashboard, } from '@tabler/icons-react'; import { FC, useCallback, useEffect } from 'react'; import { useLocation, useParams } from 'react-router-dom'; @@ -37,7 +37,7 @@ const navItems = [ route: HOME_ROUTE, }, { - icon: IconChartInfographic, + icon: IconLayoutDashboard, label: 'Dashboards', path: '/dashboards', route: DASHBOARDS_ROUTE, diff --git a/src/hooks/useDashboards.tsx b/src/hooks/useDashboards.tsx index 5070728d..eeb3c2ea 100644 --- a/src/hooks/useDashboards.tsx +++ b/src/hooks/useDashboards.tsx @@ -8,6 +8,7 @@ import { getDashboards, getQueryData, postDashboard, putDashboard, removeDashboa import { useCallback, useState } from 'react'; import { CreateDashboardType, + Dashboard, TileQuery, TileQueryResponse, UpdateDashboardType, @@ -15,8 +16,8 @@ import { const { setDashboards, setTileData, selectDashboard } = dashboardsStoreReducers; -export const useDashboardsQuery = () => { - const [, setDashbaordsStore] = useDashboardsStore((_store) => null); +export const useDashboardsQuery = (opts: { updateTimeRange?: (dashboard: Dashboard) => void }) => { + const [activeDashboard, setDashboardsStore] = useDashboardsStore((store) => store.activeDashboard); const username = Cookies.get('username'); const { @@ -29,10 +30,14 @@ export const useDashboardsQuery = () => { enabled: false, // not on mount refetchOnWindowFocus: false, onSuccess: (data) => { - setDashbaordsStore((store) => setDashboards(store, data.data || [])); + const firstDashboard = _.head(data.data); + if (!activeDashboard && firstDashboard && opts.updateTimeRange) { + opts.updateTimeRange(firstDashboard); + } + setDashboardsStore((store) => setDashboards(store, data.data || [])); }, onError: () => { - setDashbaordsStore((store) => setDashboards(store, [])); + setDashboardsStore((store) => setDashboards(store, [])); }, }); @@ -42,7 +47,7 @@ export const useDashboardsQuery = () => { onSuccess: (response, variables) => { const { dashboard_id } = response.data; if (_.isString(dashboard_id) && !_.isEmpty(dashboard_id)) { - setDashbaordsStore((store) => selectDashboard(store, null, response.data)); + setDashboardsStore((store) => selectDashboard(store, null, response.data)); } fetchDashboards(); variables.onSuccess && variables.onSuccess(); @@ -117,7 +122,7 @@ export const useDashboardsQuery = () => { }; export const useTileQuery = (opts?: { tileId?: string; onSuccess?: (data: TileQueryResponse) => void }) => { - const [, setDashbaordsStore] = useDashboardsStore((_store) => null); + const [, setDashboardsStore] = useDashboardsStore((_store) => null); const { onSuccess } = opts || {}; const [fetchState, setFetchState] = useState<{ isLoading: boolean; @@ -131,7 +136,7 @@ export const useTileQuery = (opts?: { tileId?: string; onSuccess?: (data: TileQu setFetchState({ isLoading: true, isError: null, isSuccess: null }); const res = await getQueryData(queryOpts); const tileData = _.isEmpty(res) ? { records: [], fields: [] } : res.data; - opts?.tileId && setDashbaordsStore((store) => setTileData(store, opts.tileId || '', tileData)); + opts?.tileId && setDashboardsStore((store) => setTileData(store, opts.tileId || '', tileData)); opts?.onSuccess && opts.onSuccess(tileData); setFetchState({ isLoading: false, isError: false, isSuccess: true }); } catch (e: any) { diff --git a/src/pages/Dashboards/Charts.tsx b/src/pages/Dashboards/Charts.tsx index 9b4bafed..6a1d8648 100644 --- a/src/pages/Dashboards/Charts.tsx +++ b/src/pages/Dashboards/Charts.tsx @@ -247,6 +247,7 @@ const Pie = (props: { data: CircularChartData; unit: UnitType }) => { w="100%" withTooltip tooltipDataSource="all" + labelsType="percent" tooltipProps={{ content: ({ label, payload }) => ( diff --git a/src/pages/Dashboards/CreateDashboardModal.tsx b/src/pages/Dashboards/CreateDashboardModal.tsx index 045a8ee2..1bd72a69 100644 --- a/src/pages/Dashboards/CreateDashboardModal.tsx +++ b/src/pages/Dashboards/CreateDashboardModal.tsx @@ -61,7 +61,7 @@ const CreateDashboardModal = () => { }); const selectedTimeRangeOption = getDefaultTimeRangeOption(timeRangeOptions); const { form } = useDashboardForm(defaultOpts); - const { createDashboard, updateDashboard, isCreatingDashboard, isUpdatingDashboard } = useDashboardsQuery(); + const { createDashboard, updateDashboard, isCreatingDashboard, isUpdatingDashboard } = useDashboardsQuery({}); const showLoader = isCreatingDashboard || isUpdatingDashboard; useEffect(() => { diff --git a/src/pages/Dashboards/CreateTileForm.tsx b/src/pages/Dashboards/CreateTileForm.tsx index dd2bc01b..0642fb42 100644 --- a/src/pages/Dashboards/CreateTileForm.tsx +++ b/src/pages/Dashboards/CreateTileForm.tsx @@ -589,7 +589,7 @@ const CreateTileForm = () => { setDashbaordsStore((store) => toggleCreateTileModal(store, false)); }, []); - const { updateDashboard, isUpdatingDashboard } = useDashboardsQuery(); + const { updateDashboard, isUpdatingDashboard } = useDashboardsQuery({}); const onCreate = useCallback(() => { const { dashboardId } = form.values; diff --git a/src/pages/Dashboards/Dashboard.tsx b/src/pages/Dashboards/Dashboard.tsx index 7a871c3f..abf63169 100644 --- a/src/pages/Dashboards/Dashboard.tsx +++ b/src/pages/Dashboards/Dashboard.tsx @@ -63,7 +63,7 @@ const DeleteTileModal = () => { const [deleteTileId] = useDashboardsStore((store) => store.deleteTileId); const selectedTile = _.find(activeDashboard?.tiles, (tile) => tile.tile_id === deleteTileId); - const { updateDashboard, isUpdatingDashboard } = useDashboardsQuery(); + const { updateDashboard, isUpdatingDashboard } = useDashboardsQuery({}); const onClose = useCallback(() => { setDashboardsStore((store) => toggleDeleteTileModal(store, false, null)); diff --git a/src/pages/Dashboards/SideBar.tsx b/src/pages/Dashboards/SideBar.tsx index 83832316..eeff8081 100644 --- a/src/pages/Dashboards/SideBar.tsx +++ b/src/pages/Dashboards/SideBar.tsx @@ -31,7 +31,7 @@ const DashboardListItem = (props: DashboardItemProps) => { ); }; -const DashboardList = () => { +const DashboardList = (props: { updateTimeRange: (dashboard: Dashboard) => void }) => { const [dashboards, setDashbaordsStore] = useDashboardsStore((store) => store.dashboards); const [activeDashboardId] = useDashboardsStore((store) => store.activeDashboard?.dashboard_id); @@ -39,9 +39,11 @@ const DashboardList = () => { (dashboardId: string) => { if (activeDashboardId === dashboardId) return; + const dashboard = _.find(dashboards, (dashboard) => dashboard.dashboard_id === dashboardId); + dashboard && props.updateTimeRange(dashboard); setDashbaordsStore((store) => selectDashboard(store, dashboardId)); }, - [activeDashboardId], + [activeDashboardId, dashboards], ); return ( @@ -60,7 +62,7 @@ const DashboardList = () => { ); }; -const SideBar = () => { +const SideBar = (props: { updateTimeRange: (dashboard: Dashboard) => void }) => { const [dashboards, setDashbaordsStore] = useDashboardsStore((store) => store.dashboards); const openCreateStreamModal = useCallback(() => { @@ -80,7 +82,7 @@ const SideBar = () => { New Dashboard - + ); }; diff --git a/src/pages/Dashboards/Toolbar.tsx b/src/pages/Dashboards/Toolbar.tsx index 03f5000f..ea3ba467 100644 --- a/src/pages/Dashboards/Toolbar.tsx +++ b/src/pages/Dashboards/Toolbar.tsx @@ -32,7 +32,7 @@ const EditLayoutButton = (props: { layoutRef: React.MutableRefObject { if (allowDrag && activeDashboard) { const allTiles = activeDashboard.tiles; @@ -81,7 +81,7 @@ const DeleteDashboardModal = () => { const [activeDashboard, setDashbaordsStore] = useDashboardsStore((store) => store.activeDashboard); const [deleteDashboardModalOpen] = useDashboardsStore((store) => store.deleteDashboardModalOpen); const [confirmText, setConfirmText] = useState(''); - const { isDeleting, deleteDashboard } = useDashboardsQuery(); + const { isDeleting, deleteDashboard } = useDashboardsQuery({}); const closeModal = useCallback(() => { setDashbaordsStore((store) => toggleDeleteDashboardModal(store, false)); }, []); diff --git a/src/pages/Dashboards/hooks.ts b/src/pages/Dashboards/hooks.ts new file mode 100644 index 00000000..9134eafb --- /dev/null +++ b/src/pages/Dashboards/hooks.ts @@ -0,0 +1,29 @@ +import { useLogsStore, logsStoreReducers } from "../Stream/providers/LogsProvider" +import { useCallback } from "react" +import _ from "lodash" +import dayjs from "dayjs" +import { useDashboardsStore } from "./providers/DashboardsProvider" +import { Dashboard } from "@/@types/parseable/api/dashboards" + +const { setTimeRange } = logsStoreReducers; + +export const useSyncTimeRange = () => { + const [dashboards] = useDashboardsStore(store => store.dashboards); + const [, setLogsStore] = useLogsStore((_store) => null); + + const updateTimeRange = useCallback( + (dashboard: Dashboard) => { + if (!dashboard) return; + + const { time_filter } = dashboard; + const hasTimeFilter = !_.isEmpty(time_filter); + hasTimeFilter && + setLogsStore((store) => + setTimeRange(store, { startTime: dayjs(time_filter.from), endTime: dayjs(time_filter.to), type: 'custom' }), + ); + }, + [dashboards], + ); + + return { updateTimeRange }; +}; diff --git a/src/pages/Dashboards/index.tsx b/src/pages/Dashboards/index.tsx index eb320f86..f1570ef1 100644 --- a/src/pages/Dashboards/index.tsx +++ b/src/pages/Dashboards/index.tsx @@ -8,6 +8,7 @@ import CreateDashboardModal from './CreateDashboardModal'; import { useEffect } from 'react'; import { useDashboardsQuery } from '@/hooks/useDashboards'; import CreateTileForm from './CreateTileForm'; +import { useSyncTimeRange } from './hooks'; const LoadingView = () => { return ( @@ -20,7 +21,8 @@ const LoadingView = () => { const Dashboards = () => { const [dashboards] = useDashboardsStore((store) => store.dashboards); const [createTileFormOpen] = useDashboardsStore((store) => store.createTileFormOpen); - const { fetchDashboards } = useDashboardsQuery(); + const { updateTimeRange } = useSyncTimeRange(); + const { fetchDashboards } = useDashboardsQuery({ updateTimeRange }); useEffect(() => { fetchDashboards(); }, []); @@ -41,7 +43,7 @@ const Dashboards = () => { ) : ( <> - + From 00821d9f682d90e2af5f9e17888f33689be41bdf Mon Sep 17 00:00:00 2001 From: balaji-jr Date: Tue, 20 Aug 2024 23:05:22 +0530 Subject: [PATCH 30/34] merge chart config and tick config in viz form --- src/@types/parseable/api/dashboards.ts | 2 +- src/pages/Dashboards/Charts.tsx | 83 +++++---- src/pages/Dashboards/Tile.tsx | 15 +- src/pages/Dashboards/VizEditorModal.tsx | 220 +++++++++++++++--------- src/pages/Dashboards/utils.ts | 39 ++++- src/utils/timeRangeUtils.ts | 12 +- 6 files changed, 233 insertions(+), 138 deletions(-) diff --git a/src/@types/parseable/api/dashboards.ts b/src/@types/parseable/api/dashboards.ts index 3b1a3b84..095f1eeb 100644 --- a/src/@types/parseable/api/dashboards.ts +++ b/src/@types/parseable/api/dashboards.ts @@ -8,7 +8,7 @@ export type UnitType = (typeof tickUnits)[number] | null; // viz type constants export const visualizations = ['pie-chart', 'donut-chart', 'line-chart', 'bar-chart', 'area-chart', 'table'] as const; export const circularChartTypes = ['pie-chart', 'donut-chart'] as const; -export const tickUnits = ['bytes'] as const; +export const tickUnits = ['bytes', 'utc-timestamp'] as const; export const graphTypes = ['line-chart', 'bar-chart', 'area-chart'] as const; // vize size constants diff --git a/src/pages/Dashboards/Charts.tsx b/src/pages/Dashboards/Charts.tsx index 6a1d8648..f2324776 100644 --- a/src/pages/Dashboards/Charts.tsx +++ b/src/pages/Dashboards/Charts.tsx @@ -140,9 +140,10 @@ export const renderGraph = (opts: { x_key: string; y_keys: string[]; chart: string; - unit: UnitType; + xUnit: UnitType; + yUnit: UnitType; }) => { - const { queryResponse, x_key, y_keys, chart, unit } = opts; + const { queryResponse, x_key, y_keys, chart, xUnit, yUnit } = opts; const VizComponent = getGraphVizComponent(chart); const seriesData = makeSeriesData(queryResponse?.records || [], y_keys); @@ -156,7 +157,7 @@ export const renderGraph = (opts: { {warningMsg ? ( ) : VizComponent ? ( - + ) : null} ); @@ -220,21 +221,7 @@ const makeSeriesData = (data: Log[], y_key: string[]) => { }; const Donut = (props: { data: CircularChartData; unit: UnitType }) => { - return ( - ( - - ), - }} - /> - ); + return ; }; const Pie = (props: { data: CircularChartData; unit: UnitType }) => { @@ -248,11 +235,6 @@ const Pie = (props: { data: CircularChartData; unit: UnitType }) => { withTooltip tooltipDataSource="all" labelsType="percent" - tooltipProps={{ - content: ({ label, payload }) => ( - - ), - }} /> ); }; @@ -260,32 +242,36 @@ const Pie = (props: { data: CircularChartData; unit: UnitType }) => { interface ChartTooltipProps { label: string; payload: Record[] | undefined; - unit: UnitType; + xUnit: UnitType; + yUnit: UnitType; chartType: 'line' | 'bar' | 'area' | 'donut' | 'pie'; } -function ChartTooltip({ label, payload, unit, chartType }: ChartTooltipProps) { +function ChartTooltip({ label, payload, xUnit, yUnit, chartType }: ChartTooltipProps) { if (!payload) return null; const sanitizedPayload = chartType === 'area' ? getFilteredChartTooltipPayload(payload) : payload; return ( - - {label} + + {tickFormatter(label, xUnit)} - {sanitizedPayload.map((item: any) => { - const { name = '', value = null } = item; - return ( - - {name}: {tickFormatter(value, unit)} - - ); - })} + + {sanitizedPayload.map((item: any) => { + const { name = '', value = null } = item; + return ( + + {name} + {tickFormatter(value, yUnit)} + + ); + })} + ); } -const Line = (props: { data: TileData; dataKey: string; series: SeriesType; unit: UnitType }) => { +const Line = (props: { data: TileData; dataKey: string; series: SeriesType; xUnit: UnitType; yUnit: UnitType }) => { return ( tickFormatter(value, props.unit), + tickFormatter: (value) => tickFormatter(value, props.yUnit), + }} + xAxisProps={{ + tickFormatter: (value) => tickFormatter(value, props.xUnit), }} tooltipProps={{ content: ({ label, payload }) => ( - + ), }} /> ); }; -const Bar = (props: { data: TileData; dataKey: string; series: SeriesType; unit: UnitType }) => { +const Bar = (props: { data: TileData; dataKey: string; series: SeriesType; xUnit: UnitType; yUnit: UnitType }) => { return ( tickFormatter(value, props.unit), + tickFormatter: (value) => tickFormatter(value, props.yUnit), + }} + xAxisProps={{ + tickFormatter: (value) => tickFormatter(value, props.xUnit), }} tooltipProps={{ content: ({ label, payload }) => ( - + ), }} /> ); }; -const Area = (props: { data: TileData; dataKey: string; series: SeriesType; unit: UnitType }) => { +const Area = (props: { data: TileData; dataKey: string; series: SeriesType; xUnit: UnitType; yUnit: UnitType }) => { return ( tickFormatter(value, props.unit), + tickFormatter: (value) => tickFormatter(value, props.yUnit), + }} + xAxisProps={{ + tickFormatter: (value) => tickFormatter(value, props.xUnit), }} tooltipProps={{ content: ({ label, payload }) => ( - + ), }} /> diff --git a/src/pages/Dashboards/Tile.tsx b/src/pages/Dashboards/Tile.tsx index e41033f2..c31636a4 100644 --- a/src/pages/Dashboards/Tile.tsx +++ b/src/pages/Dashboards/Tile.tsx @@ -27,7 +27,7 @@ import { sanitiseSqlString } from '@/utils/sanitiseSqlString'; import Table from './Table'; import { downloadDataAsCSV, downloadDataAsJson } from '@/utils/exportHelpers'; import { makeExportData, useLogsStore } from '../Stream/providers/LogsProvider'; -import { getRandomUnitTypeForChart } from './utils'; +import { getRandomUnitTypeForChart, getUnitTypeByKey } from './utils'; const { toggleCreateTileModal, toggleDeleteTileModal } = dashboardsStoreReducers; @@ -70,10 +70,11 @@ const Graph = (props: { tile: TileType; data: TileQueryResponse }) => { } = tile; const x_key = _.get(graph_config, 'x_key', ''); const y_keys = _.get(graph_config, 'y_keys', []); - const unit = getRandomUnitTypeForChart(tick_config); + const yUnit = getRandomUnitTypeForChart(tick_config); + const xUnit = getUnitTypeByKey(x_key, tick_config); return ( - {renderGraph({ queryResponse: data, x_key, y_keys, chart: visualization_type, unit })} + {renderGraph({ queryResponse: data, x_key, y_keys, chart: visualization_type, yUnit, xUnit })} ); }; @@ -206,8 +207,12 @@ const Tile = (props: { id: string }) => { - {tile.name} - {tile.description} + + {tile.name} + + + {tile.description} + diff --git a/src/pages/Dashboards/VizEditorModal.tsx b/src/pages/Dashboards/VizEditorModal.tsx index f935345c..8441f770 100644 --- a/src/pages/Dashboards/VizEditorModal.tsx +++ b/src/pages/Dashboards/VizEditorModal.tsx @@ -1,4 +1,4 @@ -import { ActionIcon, Button, CloseIcon, Modal, MultiSelect, Select, Stack, Text, Box } from '@mantine/core'; +import { ActionIcon, Box, Button, CloseIcon, Modal, Select, Stack, Text } from '@mantine/core'; import classes from './styles/VizEditor.module.css'; import { useDashboardsStore, dashboardsStoreReducers } from './providers/DashboardsProvider'; import { useCallback, useEffect } from 'react'; @@ -7,10 +7,11 @@ import { isCircularChart, renderCircularChart, renderGraph } from './Charts'; import { tickUnits, TileFormType, tileSizes, visualizations } from '@/@types/parseable/api/dashboards'; import { IconAlertTriangle, IconPlus } from '@tabler/icons-react'; import TableViz from './Table'; -import { getRandomUnitTypeForChart } from './utils'; +import { getRandomUnitTypeForChart, getUnitTypeByKey } from './utils'; const { toggleVizEditorModal } = dashboardsStoreReducers; const inValidVizType = 'Select a visualization type'; +const defaultTickUnit = 'default'; const WarningView = (props: { msg: string | null }) => { return ( @@ -44,10 +45,12 @@ const Graph = (props: { form: TileFormType }) => { } = props.form.values; const x_key = _.get(graph_config, 'x_key', ''); const y_keys = _.get(graph_config, 'y_keys', []); - const unit = getRandomUnitTypeForChart(tick_config); + const yUnit = getRandomUnitTypeForChart(tick_config); + const xUnit = getUnitTypeByKey(x_key, tick_config); + return ( - {renderGraph({ queryResponse: data, x_key, y_keys, chart: visualization_type, unit })} + {renderGraph({ queryResponse: data, x_key, y_keys, chart: visualization_type, xUnit, yUnit })} ); }; @@ -169,17 +172,45 @@ const CircularChartConfig = (props: { form: TileFormType }) => { ); }; -const GraphConfig = (props: { form: TileFormType }) => { +const XAxisConfig = (props: { form: TileFormType }) => { const { form: { - values: { data }, + values: { + data: { fields = [] }, + visualization: { graph_config, tick_config }, + }, }, } = props; + const x_key = _.get(graph_config, 'x_key', ''); + const tickConfigIndex = x_key === '' ? -1 : _.findIndex(tick_config, (e) => e.key === x_key); + const unit = tickConfigIndex !== -1 ? tick_config[tickConfigIndex].unit : defaultTickUnit; + const tickConfigPath = 'visualization.tick_config'; + + const onChangeUnit = useCallback( + (unit: string | null) => { + if (unit === null || unit === defaultTickUnit) { + if (tickConfigIndex === -1) return; + + props.form.removeListItem(tickConfigPath, tickConfigIndex); + } else { + if (tickConfigIndex === -1) { + props.form.insertListItem(tickConfigPath, { unit, key: x_key }); + } else { + const unitFieldPath = tickConfigPath + '.' + tickConfigIndex + '.unit'; + const keyFieldPath = tickConfigPath + '.' + tickConfigIndex + '.key'; + props.form.setFieldValue(unitFieldPath, unit); + props.form.setFieldValue(keyFieldPath, x_key); + } + } + }, + [tickConfigIndex, x_key, props.form], + ); + return ( - + ({ label: field, value: field }))} + classNames={{ label: classes.fieldTitle }} + placeholder="Y Axis" + key={currentKeyPath} + {...props.form.getInputProps(currentKeyPath)} + style={{ width: '50%' }} + /> + ({ label: field, value: field }))} - classNames={{ label: classes.fieldTitle }} - placeholder="Tick" - key={keyFieldPath} - {...props.form.getInputProps(keyFieldPath)} - style={{ width: '50%' }} - /> -
+ +
+ )} diff --git a/src/pages/Dashboards/VizEditorModal.tsx b/src/pages/Dashboards/VizEditorModal.tsx index 94b22460..57106304 100644 --- a/src/pages/Dashboards/VizEditorModal.tsx +++ b/src/pages/Dashboards/VizEditorModal.tsx @@ -1,4 +1,4 @@ -import { ActionIcon, Box, Button, CloseIcon, Modal, Select, Stack, Text } from '@mantine/core'; +import { ActionIcon, Box, Button, CloseIcon, Modal, ScrollArea, Select, Stack, Text } from '@mantine/core'; import classes from './styles/VizEditor.module.css'; import { useDashboardsStore, dashboardsStoreReducers } from './providers/DashboardsProvider'; import { useCallback, useEffect } from 'react'; @@ -331,8 +331,14 @@ const GraphConfig = (props: { form: TileFormType }) => { const y_keys = _.get(graph_config, 'y_keys', []); const addAxes = useCallback(() => { - _.head(fields) && props.form.insertListItem(yKeysPath, _.head(fields)); - }, [fields, props.form]); + if (!_.head(fields)) return; + + if (_.isEmpty(y_keys)) { + props.form.setFieldValue(yKeysPath, [_.head(fields)]); + } else { + props.form.insertListItem(yKeysPath, _.head(fields)); + } + }, [fields, props.form, y_keys]); useEffect(() => { if (_.isEmpty(y_keys)) { @@ -388,7 +394,7 @@ const ChartConfig = (props: { form: TileFormType }) => { const Config = (props: { form: TileFormType }) => { return ( - + @@ -420,8 +426,10 @@ const VizEditorModal = (props: { form: TileFormType }) => { - - + + + + diff --git a/src/pages/Dashboards/styles/Table.module.css b/src/pages/Dashboards/styles/Table.module.css index 0c35cfa0..19cda6e2 100644 --- a/src/pages/Dashboards/styles/Table.module.css +++ b/src/pages/Dashboards/styles/Table.module.css @@ -1,8 +1,8 @@ .thead { th { - font-weight: 600; + font-weight: 500; font-size: 0.65rem; - padding: 0.6rem; + padding: 0.5rem; min-width: 5rem; max-width: 10rem; white-space: nowrap; @@ -28,6 +28,7 @@ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + font-size: 0.62rem; } } diff --git a/src/pages/Stream/Views/Explore/StaticLogTable.tsx b/src/pages/Stream/Views/Explore/StaticLogTable.tsx index 965ce9b5..6b9a098e 100644 --- a/src/pages/Stream/Views/Explore/StaticLogTable.tsx +++ b/src/pages/Stream/Views/Explore/StaticLogTable.tsx @@ -98,6 +98,7 @@ const Columns = (props: { containerRefs: SectionRefs }) => { viewportRef={rightSectionRef} onScrollPositionChange={({ y }) => { if (activeSectionRef.current === 'left') return; + leftSectionRef.current!.scrollTop = y; }}> From bd1511dbc86650fcba982a6e82ef2cfc950b8849 Mon Sep 17 00:00:00 2001 From: balaji-jr Date: Wed, 21 Aug 2024 14:05:20 +0530 Subject: [PATCH 34/34] disable dashboard field in form and some style changes --- src/pages/Dashboards/CreateTileForm.tsx | 3 ++- src/pages/Dashboards/Dashboard.tsx | 6 +++--- src/pages/Dashboards/styles/Form.module.css | 1 - 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pages/Dashboards/CreateTileForm.tsx b/src/pages/Dashboards/CreateTileForm.tsx index 05f9b01b..6914f2eb 100644 --- a/src/pages/Dashboards/CreateTileForm.tsx +++ b/src/pages/Dashboards/CreateTileForm.tsx @@ -367,7 +367,7 @@ const Query = (props: { form: TileFormType; onChangeValue: (key: string, value: onChange={onStreamSelect} classNames={{ label: classes.fieldTitle }} key="stream" - placeholder="Select Schema" + placeholder="Select Stream" /> @@ -479,6 +479,7 @@ const Config = (props: { form: TileFormType; onChangeValue: (key: string, value: key="dashboardId" {...form.getInputProps('dashboardId')} style={{ width: '50%' }} + disabled /> diff --git a/src/pages/Dashboards/Dashboard.tsx b/src/pages/Dashboards/Dashboard.tsx index 00da8b0b..3ae1367d 100644 --- a/src/pages/Dashboards/Dashboard.tsx +++ b/src/pages/Dashboards/Dashboard.tsx @@ -8,7 +8,7 @@ import { DASHBOARDS_SIDEBAR_WIDTH, NAVBAR_WIDTH } from '@/constants/theme'; import classes from './styles/DashboardView.module.css'; import { useDashboardsStore, dashboardsStoreReducers, assignOrderToTiles } from './providers/DashboardsProvider'; import _ from 'lodash'; -import { IconChartBar } from '@tabler/icons-react'; +import { IconLayoutDashboard } from '@tabler/icons-react'; import { useCallback, useRef } from 'react'; import { makeExportClassName } from '@/utils/exportImage'; import { useDashboardsQuery } from '@/hooks/useDashboards'; @@ -121,7 +121,7 @@ const NoDashboardsView = () => { return ( - + Create dashboard @@ -144,7 +144,7 @@ const NoTilesView = () => { return ( - + Add tiles to the dashboard diff --git a/src/pages/Dashboards/styles/Form.module.css b/src/pages/Dashboards/styles/Form.module.css index 6c9a023c..2e1e7370 100644 --- a/src/pages/Dashboards/styles/Form.module.css +++ b/src/pages/Dashboards/styles/Form.module.css @@ -27,7 +27,6 @@ border: 1px solid var(--mantine-color-gray-2); border-radius: 0.2rem; padding: 1rem; - overflow-y: scroll; } .schemaListContainer {