From 7b70cfb0c497a05a605bf04ab886eb4dd4afd089 Mon Sep 17 00:00:00 2001 From: Palash <88981777+palash-signoz@users.noreply.github.com> Date: Thu, 23 Sep 2021 15:43:43 +0530 Subject: [PATCH] feat: Metrics (#281) * refactor: store is updated * temp * fix: eslint error is fixed * fix:eslint linting error is updated * chore: react-grid-layout is added * chore: linting changes are updated * chore: linting changes are updated * chore: @types/node is moved to devDependecies and @types/react-grid-layout is added * chore: tsconfig is updated * chore: updateUrl function is updated * feat: All Dashboard is updated * feat: All Dashboard page is updated * feat: New Dashboard is added * feat: App Layout is updated * feat: Add Tags is updated * chore: uuid package is added * chore: AppRoutes is updated * chore: UI components are updated * chore: baseUrl is added in the apiUrl and removed from other api request * chore: commonApi Response is updated * chore: ErrorResponse handler is updated * chore: useFetch hook is made * chore: axios instance is updated * chore:some of the changes are updated * chore: list of all dashboard types is updated * chore: logic is updated to the global state * chore: all dashboard data is fetched from the global state * chore: unnessary prop is removed * chore: changes are updated * chore: getAll and create is updated * chore: getDashboard is updated * chore: isEditMode is moved to the global state * chore: get,getAll is updated * chore: update title,tags,description is now fixed * chore: new widget is updated * chore: graph is updated * chore: input component accept input props * chore: name of the dashboard is updated * chore: Widgets page in WIP * chore: types for the error api is updated * chore: getQuery data is updated * chore: widget types is updated * default widget is updated * chore: getQuery is updated * chore: Add Query is updated * fix: creating new widget bug is resolved * chore: widget type is updated * chore: Query error is updated * chore: query error and success state is handled * chore: label of graph in WIP * chore: legend input placeholder is updated * chore: changes are updated * chore: no data component is updated and error component is rendered along with the data * chore: data fetching over the initial render is fixed over the initial mount * chore: convertDateToAndPm is updated * chore: x-axis label is now fixed * chore: label is updated * chore: labels name is updated * chore: labels name is updated * chore: labels color is updated * chore: values are parsed in float * chore: tags is updated * chore: datasets type is updated * chore: graph is updated * chore: more eslint rules are updated * chore: some of the linting changes and data is updated * chore: chart.js version is updated * chore: gitignore is updated * chore: graph component is updated * chore: apply functionality is updated * chore: dashboard is now saved * chore: getChartData is updated * feat: Dashboard graph is reflected * chore: some of the bugs is resolved * fix: aspect ratio is made false * chore: some small css are fixed * chore: widgetId and graphType is preAdded if present in the search params * chore: user is now able to change the time via global time and reflect new graph values * chore: query is updated * chore: onBlurHandler is updated * fix: usage explorer is now fixed * chore: bar element is updated * chore: chartjs adapter is added * chore: old instance for the charts are removed via re-chart * chore: re-chart is removed * chore: get chart data is updated * chore: added the counter in the useEffect * chore: history is added * chore: some of the features are updated * chore: history package is updated * chore: AppRoutes is updated * fix: some are components breaking while moving from BrowserRouter to Router * chore: Dashboard icon is updated * chore: Full screen component is updated * stepSize (optional) is added in the widgets type * fix: fetching query result is fixed * update: start and end time function is updated * fix: Alert color is updated * update: Query fetching is updated * fix: start and end time is fixed * fix: chartjs data is compatable for larger data set and no ajax call for empty query is fixed * fix: last 1 week selection is fixed * fix: legends is added * update: antd version is updated * feat: value graph is updated * feat: Title is added for the value graph * fix: Full Screen view is updated with refresh functionality and alignment is updated to flex-end * fix: Graph component is updated * fix: metric graph are fixed * feature: Delete widget functionality is updated * fix: empty value bug is resolved * fix: delete widget position is fixed * fix: resize functionality is fixed * fix: sumation of the query is fixed * update: default legend is removed * update: resize handlers is removed and service metric component is updated * fix: legends is updated * update: querySuccess reducer is updated * Modal component is updated * fix: ant-d tab css is updated of the tabs * update: stringToHTML is made * update: graph component is updated * fix: several component in the metric and traces are updated * wip: build error is fixed * fix: metric section is fixed * update: console.log are commented * fix: onClick graph re-render is stopped * fix: trace graph is updated * fix: updated the min,max time for the value type graph * getQueryMaxMin Time is updated * fix: trace chart is updated * fix: re-render is fixed * fix: localstorage persistance is there * update: if label is not present legend is not displayed * fix: graph is changed while updated the global time * fix: default title is updated while creation of the dashboard * update: external database call tabs are made of same size * fix: query graph max-min time is updated in the full screen mode * fix: Request per sec graph is fixed * fix: ErrorChart is fixed Co-authored-by: Palash gupta --- .gitignore | 1 + frontend/.eslintrc.js | 29 +- frontend/.prettierrc.json | 3 +- frontend/cypress/CustomFunctions/Login.ts | 18 +- .../integration/appLayout/index.spec.ts | 16 +- .../cypress/integration/metrics/index.spec.ts | 23 +- frontend/cypress/support/commands.ts | 1 + frontend/package.json | 17 +- frontend/src/AppRoutes/index.tsx | 19 +- .../{pages.ts => AppRoutes/pageComponents.ts} | 15 +- frontend/src/AppRoutes/routes.ts | 23 +- frontend/src/api/ErrorResponseHandler.ts | 57 +++ frontend/src/api/dashboard/create.ts | 26 + frontend/src/api/dashboard/delete.ts | 24 + frontend/src/api/dashboard/get.ts | 24 + frontend/src/api/dashboard/getAll.ts | 24 + frontend/src/api/dashboard/update.ts | 26 + frontend/src/api/index.ts | 4 +- frontend/src/api/widgets/getQuery.ts | 26 + frontend/src/assets/Dashboard/TimeSeries.tsx | 35 ++ frontend/src/assets/Dashboard/Value.tsx | 18 + frontend/src/assets/index.css | 10 +- .../src/components/Graph/Legend/index.tsx | 24 + .../src/components/Graph/Legend/styles.ts | 20 + frontend/src/components/Graph/index.tsx | 235 +++++++++ frontend/src/components/Graph/styles.ts | 8 + frontend/src/components/Input/index.tsx | 48 ++ frontend/src/components/Loadable/index.tsx | 2 +- frontend/src/components/Modal.tsx | 24 +- frontend/src/components/NotFound/index.tsx | 2 +- frontend/src/components/SideNav/index.tsx | 41 +- frontend/src/components/SideNav/menuItems.ts | 26 +- frontend/src/components/SideNav/styles.ts | 4 +- .../TimePreferenceDropDown/index.tsx | 52 ++ .../TimePreferenceDropDown/styles.ts | 14 + frontend/src/components/ValueGraph/index.tsx | 13 + frontend/src/components/ValueGraph/styles.ts | 6 + frontend/src/constants/query.ts | 2 +- frontend/src/constants/routes.ts | 3 + .../container/GridGraphComponent/index.tsx | 71 +++ .../container/GridGraphComponent/styles.ts | 19 + .../GridGraphLayout/AddWidget/index.tsx | 55 +++ .../GridGraphLayout/AddWidget/styles.ts | 18 + .../GridGraphLayout/Graph/Bar/index.tsx | 40 ++ .../GridGraphLayout/Graph/Bar/styles.ts | 15 + .../GridGraphLayout/Graph/FullView/index.tsx | 181 +++++++ .../GridGraphLayout/Graph/FullView/styles.ts | 21 + .../container/GridGraphLayout/Graph/index.tsx | 209 ++++++++ .../container/GridGraphLayout/Graph/styles.ts | 13 + .../src/container/GridGraphLayout/index.tsx | 130 +++++ .../src/container/GridGraphLayout/styles.ts | 42 ++ .../TableComponents/CreatedBy.tsx | 17 + .../ListOfDashboard/TableComponents/Date.tsx | 14 + .../TableComponents/DeleteButton.tsx | 56 +++ .../ListOfDashboard/TableComponents/Name.tsx | 23 + .../ListOfDashboard/TableComponents/Tags.tsx | 16 + .../src/container/ListOfDashboard/index.tsx | 171 +++++++ .../src/container/ListOfDashboard/styles.ts | 16 + .../NewDashboard/ComponentsSlider/index.tsx | 46 ++ .../ComponentsSlider/menuItems.ts | 25 + .../NewDashboard/ComponentsSlider/styles.ts | 25 + .../DescriptionOfDashboard/AddTags/index.tsx | 120 +++++ .../DescriptionOfDashboard/AddTags/styles.ts | 26 + .../Description/index.tsx | 34 ++ .../Description/styles.ts | 5 + .../NameOfTheDashboard/index.tsx | 30 ++ .../DescriptionOfDashboard/index.tsx | 122 +++++ .../DescriptionOfDashboard/styles.ts | 13 + .../NewDashboard/GridGraphs/index.tsx | 26 + .../NewDashboard/GridGraphs/styles.ts | 28 ++ frontend/src/container/NewDashboard/index.tsx | 15 + .../LeftContainer/QuerySection/Query.tsx | 95 ++++ .../LeftContainer/QuerySection/index.tsx | 89 ++++ .../LeftContainer/QuerySection/styles.ts | 17 + .../LeftContainer/WidgetGraph/WidgetGraph.tsx | 59 +++ .../LeftContainer/WidgetGraph/index.tsx | 57 +++ .../LeftContainer/WidgetGraph/styles.ts | 27 + .../NewWidget/LeftContainer/index.tsx | 28 ++ .../NewWidget/LeftContainer/styles.ts | 9 + .../NewWidget/RightContainer/index.tsx | 157 ++++++ .../NewWidget/RightContainer/styles.ts | 40 ++ .../NewWidget/RightContainer/timeItems.ts | 60 +++ frontend/src/container/NewWidget/index.tsx | 237 +++++++++ frontend/src/container/NewWidget/styles.ts | 33 ++ frontend/src/hooks/useFetch.ts | 85 ++++ frontend/src/hooks/useMountedState.ts | 18 + frontend/src/lib/convertDateToAmAndPm.ts | 9 + .../src/lib/convertToNanoSecondsToSecond.ts | 2 +- frontend/src/lib/covertIntoEpoc.ts | 5 + frontend/src/lib/getChartData.ts | 71 +++ frontend/src/lib/getLabelName.ts | 52 ++ frontend/src/lib/getMaxMinTime.ts | 27 + frontend/src/lib/getRandomColor.ts | 21 + .../lib/getStartAndEndTime/getMicroSeconds.ts | 9 + .../src/lib/getStartAndEndTime/getMinAgo.ts | 13 + frontend/src/lib/getStartAndEndTime/index.ts | 101 ++++ frontend/src/{utils => lib}/history.ts | 1 + frontend/src/lib/stringToHTML.ts | 9 + frontend/src/lib/updateUrl.ts | 9 + frontend/src/modules/AppLayout.tsx | 4 +- frontend/src/modules/Auth/Signup.tsx | 12 +- .../src/modules/Metrics/ErrorRateChart.tsx | 260 +++------- .../Metrics/ExternalApi/ExternalApiGraph.tsx | 32 +- .../modules/Metrics/GenericVisualization.tsx | 83 ---- .../src/modules/Metrics/LatencyLineChart.tsx | 234 --------- .../Metrics/LatencyLineChart/LatencyLine.tsx | 28 ++ .../Metrics/LatencyLineChart/index.tsx | 142 ++++++ .../src/modules/Metrics/RequestRateChart.tsx | 246 +++------- .../index.tsx} | 151 +++--- .../modules/Metrics/ServiceMetrics/styles.ts | 18 + .../index.tsx} | 180 +++---- .../modules/Metrics/ServiceTable/styles.ts | 5 + .../src/modules/Metrics/ServicesTableDef.tsx | 2 +- .../src/modules/Metrics/TopEndpointsTable.tsx | 4 +- frontend/src/modules/Metrics/styles.ts | 12 + .../Nav/TopNav/CustomDateTimeModal.tsx | 10 +- .../modules/Nav/TopNav/DateTimeSelector.tsx | 219 ++++++--- .../modules/Nav/TopNav/ShowBreadcrumbs.tsx | 8 +- frontend/src/modules/Nav/TopNav/index.tsx | 8 +- frontend/src/modules/RouteProvider.tsx | 2 +- .../src/modules/Servicemap/ServiceMap.tsx | 4 +- frontend/src/modules/Servicemap/utils.ts | 4 +- .../src/modules/Settings/settingsPage.tsx | 4 +- .../src/modules/Traces/FilterStateDisplay.tsx | 138 +++--- .../src/modules/Traces/LatencyModalForm.tsx | 2 +- .../Traces/TraceCustomVisualizations.tsx | 80 ++- frontend/src/modules/Traces/TraceDetail.tsx | 6 +- frontend/src/modules/Traces/TraceFilter.tsx | 462 +++++++++--------- .../TraceGanttChart/TraceGanttChart.tsx | 4 +- .../TraceGanttChart/{index.js => index.tsx} | 0 frontend/src/modules/Traces/TraceGraph.tsx | 8 +- .../src/modules/Traces/TraceGraphColumn.tsx | 6 +- frontend/src/modules/Traces/TraceList.tsx | 6 +- frontend/src/modules/Traces/styles.ts | 16 + frontend/src/modules/Usage/UsageExplorer.tsx | 14 +- frontend/src/modules/Usage/styles.ts | 14 + .../instrumentationPage.tsx | 12 +- frontend/src/pages/Dashboard/index.tsx | 40 ++ frontend/src/pages/DashboardWidget/index.tsx | 101 ++++ frontend/src/pages/NewDashboard/index.tsx | 56 +++ .../actions/MetricsActions/metricsActions.ts | 143 +++++- .../actions/dashboard/applySettingsToPanel.ts | 24 + .../store/actions/dashboard/createQuery.ts | 17 + .../actions/dashboard/deleteDashboard.ts | 43 ++ .../store/actions/dashboard/deleteWidget.ts | 56 +++ .../actions/dashboard/getAllDashboard.ts | 38 ++ .../store/actions/dashboard/getDashboard.ts | 78 +++ .../actions/dashboard/getQueryResults.ts | 99 ++++ frontend/src/store/actions/dashboard/index.ts | 7 + .../store/actions/dashboard/saveDashboard.ts | 116 +++++ .../actions/dashboard/toggleAddWidget.ts | 17 + .../store/actions/dashboard/toggleEditMode.ts | 12 + .../actions/dashboard/updateDashboardTitle.ts | 54 ++ .../store/actions/dashboard/updateQuery.ts | 44 ++ frontend/src/store/actions/global.ts | 80 +-- frontend/src/store/actions/index.ts | 1 + frontend/src/store/actions/serviceMap.ts | 6 +- frontend/src/store/actions/traces.ts | 12 +- frontend/src/store/actions/types.ts | 2 +- frontend/src/store/actions/usage.ts | 4 +- frontend/src/store/index.ts | 13 +- frontend/src/store/reducers/dashboard.ts | 446 +++++++++++++++++ frontend/src/store/reducers/global.ts | 8 +- frontend/src/store/reducers/index.ts | 26 +- frontend/src/store/reducers/metrics.ts | 188 +++---- frontend/src/store/reducers/serviceMap.ts | 24 +- frontend/src/store/reducers/traceFilters.ts | 8 +- frontend/src/store/reducers/traces.ts | 16 +- frontend/src/store/reducers/usage.ts | 8 +- frontend/src/types/actions/dashboard.ts | 180 +++++++ frontend/src/types/actions/index.ts | 5 + frontend/src/types/api/dashboard/create.ts | 8 + frontend/src/types/api/dashboard/delete.ts | 5 + frontend/src/types/api/dashboard/get.ts | 7 + frontend/src/types/api/dashboard/getAll.ts | 50 ++ frontend/src/types/api/dashboard/update.ts | 8 + frontend/src/types/api/index.ts | 15 + frontend/src/types/api/widgets/getQuery.ts | 19 + frontend/src/types/common/index.ts | 22 + frontend/src/types/reducer/dashboards.ts | 11 + frontend/src/utils/spanToTree.ts | 14 +- frontend/tsconfig.json | 4 +- frontend/webpack.config.js | 2 +- frontend/yarn.lock | 225 ++++++--- 184 files changed, 6992 insertions(+), 1755 deletions(-) rename frontend/src/{pages.ts => AppRoutes/pageComponents.ts} (75%) create mode 100644 frontend/src/api/ErrorResponseHandler.ts create mode 100644 frontend/src/api/dashboard/create.ts create mode 100644 frontend/src/api/dashboard/delete.ts create mode 100644 frontend/src/api/dashboard/get.ts create mode 100644 frontend/src/api/dashboard/getAll.ts create mode 100644 frontend/src/api/dashboard/update.ts create mode 100644 frontend/src/api/widgets/getQuery.ts create mode 100644 frontend/src/assets/Dashboard/TimeSeries.tsx create mode 100644 frontend/src/assets/Dashboard/Value.tsx create mode 100644 frontend/src/components/Graph/Legend/index.tsx create mode 100644 frontend/src/components/Graph/Legend/styles.ts create mode 100644 frontend/src/components/Graph/index.tsx create mode 100644 frontend/src/components/Graph/styles.ts create mode 100644 frontend/src/components/Input/index.tsx create mode 100644 frontend/src/components/TimePreferenceDropDown/index.tsx create mode 100644 frontend/src/components/TimePreferenceDropDown/styles.ts create mode 100644 frontend/src/components/ValueGraph/index.tsx create mode 100644 frontend/src/components/ValueGraph/styles.ts create mode 100644 frontend/src/container/GridGraphComponent/index.tsx create mode 100644 frontend/src/container/GridGraphComponent/styles.ts create mode 100644 frontend/src/container/GridGraphLayout/AddWidget/index.tsx create mode 100644 frontend/src/container/GridGraphLayout/AddWidget/styles.ts create mode 100644 frontend/src/container/GridGraphLayout/Graph/Bar/index.tsx create mode 100644 frontend/src/container/GridGraphLayout/Graph/Bar/styles.ts create mode 100644 frontend/src/container/GridGraphLayout/Graph/FullView/index.tsx create mode 100644 frontend/src/container/GridGraphLayout/Graph/FullView/styles.ts create mode 100644 frontend/src/container/GridGraphLayout/Graph/index.tsx create mode 100644 frontend/src/container/GridGraphLayout/Graph/styles.ts create mode 100644 frontend/src/container/GridGraphLayout/index.tsx create mode 100644 frontend/src/container/GridGraphLayout/styles.ts create mode 100644 frontend/src/container/ListOfDashboard/TableComponents/CreatedBy.tsx create mode 100644 frontend/src/container/ListOfDashboard/TableComponents/Date.tsx create mode 100644 frontend/src/container/ListOfDashboard/TableComponents/DeleteButton.tsx create mode 100644 frontend/src/container/ListOfDashboard/TableComponents/Name.tsx create mode 100644 frontend/src/container/ListOfDashboard/TableComponents/Tags.tsx create mode 100644 frontend/src/container/ListOfDashboard/index.tsx create mode 100644 frontend/src/container/ListOfDashboard/styles.ts create mode 100644 frontend/src/container/NewDashboard/ComponentsSlider/index.tsx create mode 100644 frontend/src/container/NewDashboard/ComponentsSlider/menuItems.ts create mode 100644 frontend/src/container/NewDashboard/ComponentsSlider/styles.ts create mode 100644 frontend/src/container/NewDashboard/DescriptionOfDashboard/AddTags/index.tsx create mode 100644 frontend/src/container/NewDashboard/DescriptionOfDashboard/AddTags/styles.ts create mode 100644 frontend/src/container/NewDashboard/DescriptionOfDashboard/Description/index.tsx create mode 100644 frontend/src/container/NewDashboard/DescriptionOfDashboard/Description/styles.ts create mode 100644 frontend/src/container/NewDashboard/DescriptionOfDashboard/NameOfTheDashboard/index.tsx create mode 100644 frontend/src/container/NewDashboard/DescriptionOfDashboard/index.tsx create mode 100644 frontend/src/container/NewDashboard/DescriptionOfDashboard/styles.ts create mode 100644 frontend/src/container/NewDashboard/GridGraphs/index.tsx create mode 100644 frontend/src/container/NewDashboard/GridGraphs/styles.ts create mode 100644 frontend/src/container/NewDashboard/index.tsx create mode 100644 frontend/src/container/NewWidget/LeftContainer/QuerySection/Query.tsx create mode 100644 frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx create mode 100644 frontend/src/container/NewWidget/LeftContainer/QuerySection/styles.ts create mode 100644 frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraph.tsx create mode 100644 frontend/src/container/NewWidget/LeftContainer/WidgetGraph/index.tsx create mode 100644 frontend/src/container/NewWidget/LeftContainer/WidgetGraph/styles.ts create mode 100644 frontend/src/container/NewWidget/LeftContainer/index.tsx create mode 100644 frontend/src/container/NewWidget/LeftContainer/styles.ts create mode 100644 frontend/src/container/NewWidget/RightContainer/index.tsx create mode 100644 frontend/src/container/NewWidget/RightContainer/styles.ts create mode 100644 frontend/src/container/NewWidget/RightContainer/timeItems.ts create mode 100644 frontend/src/container/NewWidget/index.tsx create mode 100644 frontend/src/container/NewWidget/styles.ts create mode 100644 frontend/src/hooks/useFetch.ts create mode 100644 frontend/src/hooks/useMountedState.ts create mode 100644 frontend/src/lib/convertDateToAmAndPm.ts create mode 100644 frontend/src/lib/covertIntoEpoc.ts create mode 100644 frontend/src/lib/getChartData.ts create mode 100644 frontend/src/lib/getLabelName.ts create mode 100644 frontend/src/lib/getMaxMinTime.ts create mode 100644 frontend/src/lib/getRandomColor.ts create mode 100644 frontend/src/lib/getStartAndEndTime/getMicroSeconds.ts create mode 100644 frontend/src/lib/getStartAndEndTime/getMinAgo.ts create mode 100644 frontend/src/lib/getStartAndEndTime/index.ts rename frontend/src/{utils => lib}/history.ts (98%) create mode 100644 frontend/src/lib/stringToHTML.ts create mode 100644 frontend/src/lib/updateUrl.ts delete mode 100644 frontend/src/modules/Metrics/GenericVisualization.tsx delete mode 100644 frontend/src/modules/Metrics/LatencyLineChart.tsx create mode 100644 frontend/src/modules/Metrics/LatencyLineChart/LatencyLine.tsx create mode 100644 frontend/src/modules/Metrics/LatencyLineChart/index.tsx rename frontend/src/modules/Metrics/{ServiceMetrics.tsx => ServiceMetrics/index.tsx} (63%) create mode 100644 frontend/src/modules/Metrics/ServiceMetrics/styles.ts rename frontend/src/modules/Metrics/{ServicesTable.tsx => ServiceTable/index.tsx} (55%) create mode 100644 frontend/src/modules/Metrics/ServiceTable/styles.ts create mode 100644 frontend/src/modules/Metrics/styles.ts rename frontend/src/modules/Traces/TraceGanttChart/{index.js => index.tsx} (100%) create mode 100644 frontend/src/modules/Traces/styles.ts create mode 100644 frontend/src/modules/Usage/styles.ts create mode 100644 frontend/src/pages/Dashboard/index.tsx create mode 100644 frontend/src/pages/DashboardWidget/index.tsx create mode 100644 frontend/src/pages/NewDashboard/index.tsx create mode 100644 frontend/src/store/actions/dashboard/applySettingsToPanel.ts create mode 100644 frontend/src/store/actions/dashboard/createQuery.ts create mode 100644 frontend/src/store/actions/dashboard/deleteDashboard.ts create mode 100644 frontend/src/store/actions/dashboard/deleteWidget.ts create mode 100644 frontend/src/store/actions/dashboard/getAllDashboard.ts create mode 100644 frontend/src/store/actions/dashboard/getDashboard.ts create mode 100644 frontend/src/store/actions/dashboard/getQueryResults.ts create mode 100644 frontend/src/store/actions/dashboard/index.ts create mode 100644 frontend/src/store/actions/dashboard/saveDashboard.ts create mode 100644 frontend/src/store/actions/dashboard/toggleAddWidget.ts create mode 100644 frontend/src/store/actions/dashboard/toggleEditMode.ts create mode 100644 frontend/src/store/actions/dashboard/updateDashboardTitle.ts create mode 100644 frontend/src/store/actions/dashboard/updateQuery.ts create mode 100644 frontend/src/store/reducers/dashboard.ts create mode 100644 frontend/src/types/actions/dashboard.ts create mode 100644 frontend/src/types/actions/index.ts create mode 100644 frontend/src/types/api/dashboard/create.ts create mode 100644 frontend/src/types/api/dashboard/delete.ts create mode 100644 frontend/src/types/api/dashboard/get.ts create mode 100644 frontend/src/types/api/dashboard/getAll.ts create mode 100644 frontend/src/types/api/dashboard/update.ts create mode 100644 frontend/src/types/api/index.ts create mode 100644 frontend/src/types/api/widgets/getQuery.ts create mode 100644 frontend/src/types/common/index.ts create mode 100644 frontend/src/types/reducer/dashboards.ts diff --git a/.gitignore b/.gitignore index e7fecd9a68..10736de861 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules yarn.lock +package.json deploy/docker/environment_tiny/common_test frontend/node_modules diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index 1ebe27f73c..b27d5be241 100644 --- a/frontend/.eslintrc.js +++ b/frontend/.eslintrc.js @@ -9,6 +9,7 @@ module.exports = { 'plugin:react/recommended', 'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/eslint-recommended', + 'plugin:prettier/recommended', ], parser: '@typescript-eslint/parser', parserOptions: { @@ -18,7 +19,18 @@ module.exports = { ecmaVersion: 12, sourceType: 'module', }, - plugins: ['react', '@typescript-eslint', 'simple-import-sort'], + plugins: [ + 'react', + '@typescript-eslint', + 'simple-import-sort', + 'react-hooks', + 'prettier', + ], + settings: { + react: { + version: 'latest', + }, + }, rules: { 'react/jsx-filename-extension': [ 'error', @@ -30,12 +42,21 @@ module.exports = { '@typescript-eslint/explicit-function-return-type': 'error', '@typescript-eslint/no-var-requires': 0, 'linebreak-style': ['error', 'unix'], - indent: ['error', 'tab'], - quotes: ['error', 'single'], - semi: ['error', 'always'], // simple sort error 'simple-import-sort/imports': 'error', 'simple-import-sort/exports': 'error', + + // hooks + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'warn', + + 'prettier/prettier': [ + 'error', + {}, + { + usePrettierrc: true, + }, + ], }, }; diff --git a/frontend/.prettierrc.json b/frontend/.prettierrc.json index fd2d5e75d1..a147736ad6 100644 --- a/frontend/.prettierrc.json +++ b/frontend/.prettierrc.json @@ -3,5 +3,6 @@ "useTabs": true, "tabWidth": 1, "singleQuote": true, - "jsxSingleQuote": false + "jsxSingleQuote": false, + "semi": true } diff --git a/frontend/cypress/CustomFunctions/Login.ts b/frontend/cypress/CustomFunctions/Login.ts index 0f2f4a47e1..7b99516b44 100644 --- a/frontend/cypress/CustomFunctions/Login.ts +++ b/frontend/cypress/CustomFunctions/Login.ts @@ -1,38 +1,38 @@ -import ROUTES from "constants/routes"; +import ROUTES from 'constants/routes'; const Login = ({ email, name }: LoginProps): void => { - const emailInput = cy.findByPlaceholderText("mike@netflix.com"); + const emailInput = cy.findByPlaceholderText('mike@netflix.com'); emailInput.then((emailInput) => { const element = emailInput[0]; // element is present expect(element).not.undefined; - expect(element.nodeName).to.be.equal("INPUT"); + expect(element.nodeName).to.be.equal('INPUT'); }); emailInput.type(email).then((inputElements) => { const inputElement = inputElements[0]; - const inputValue = inputElement.getAttribute("value"); + const inputValue = inputElement.getAttribute('value'); expect(inputValue).to.be.equals(email); }); - const firstNameInput = cy.findByPlaceholderText("Mike"); + const firstNameInput = cy.findByPlaceholderText('Mike'); firstNameInput.then((firstNameInput) => { const element = firstNameInput[0]; // element is present expect(element).not.undefined; - expect(element.nodeName).to.be.equal("INPUT"); + expect(element.nodeName).to.be.equal('INPUT'); }); firstNameInput.type(name).then((inputElements) => { const inputElement = inputElements[0]; - const inputValue = inputElement.getAttribute("value"); + const inputValue = inputElement.getAttribute('value'); expect(inputValue).to.be.equals(name); }); - const gettingStartedButton = cy.get("button"); + const gettingStartedButton = cy.get('button'); gettingStartedButton.click(); - cy.location("pathname").then((e) => { + cy.location('pathname').then((e) => { expect(e).to.be.equal(ROUTES.APPLICATION); }); }; diff --git a/frontend/cypress/integration/appLayout/index.spec.ts b/frontend/cypress/integration/appLayout/index.spec.ts index 1dd2e5e6b3..38a063953a 100644 --- a/frontend/cypress/integration/appLayout/index.spec.ts +++ b/frontend/cypress/integration/appLayout/index.spec.ts @@ -1,20 +1,20 @@ /// -import ROUTES from "constants/routes"; +import ROUTES from 'constants/routes'; -describe("App Layout", () => { +describe('App Layout', () => { beforeEach(() => { - cy.visit(Cypress.env("baseUrl")); + cy.visit(Cypress.env('baseUrl')); }); - it("Check the user is in Logged Out State", async () => { - cy.location("pathname").then((e) => { + it('Check the user is in Logged Out State', async () => { + cy.location('pathname').then((e) => { expect(e).to.be.equal(ROUTES.SIGN_UP); }); }); - it("Logged In State", () => { - const testEmail = "test@test.com"; - const firstName = "Test"; + it('Logged In State', () => { + const testEmail = 'test@test.com'; + const firstName = 'Test'; cy.login({ email: testEmail, diff --git a/frontend/cypress/integration/metrics/index.spec.ts b/frontend/cypress/integration/metrics/index.spec.ts index 2621556541..3169f5eb2d 100644 --- a/frontend/cypress/integration/metrics/index.spec.ts +++ b/frontend/cypress/integration/metrics/index.spec.ts @@ -1,16 +1,17 @@ /// -import defaultApps from "../../fixtures/defaultApp.json"; -import convertToNanoSecondsToSecond from "lib/convertToNanoSecondsToSecond"; +import convertToNanoSecondsToSecond from 'lib/convertToNanoSecondsToSecond'; -describe("Metrics", () => { +import defaultApps from '../../fixtures/defaultApp.json'; + +describe('Metrics', () => { beforeEach(() => { - cy.visit(Cypress.env("baseUrl")); + cy.visit(Cypress.env('baseUrl')); - const testEmail = "test@test.com"; - const firstName = "Test"; + const testEmail = 'test@test.com'; + const firstName = 'Test'; cy - .intercept("GET", "/api/v1//services?start*", { fixture: "defaultApp.json" }) - .as("defaultApps"); + .intercept('GET', '/api/v1//services?start*', { fixture: 'defaultApp.json' }) + .as('defaultApps'); cy.login({ email: testEmail, @@ -18,10 +19,10 @@ describe("Metrics", () => { }); }); - it("Default Apps", () => { - cy.wait("@defaultApps"); + it('Default Apps', () => { + cy.wait('@defaultApps'); - cy.get("tbody").then((elements) => { + cy.get('tbody').then((elements) => { const trElements = elements.children(); expect(trElements.length).to.be.equal(defaultApps.length); diff --git a/frontend/cypress/support/commands.ts b/frontend/cypress/support/commands.ts index ab52b22cf1..9fb840e8d9 100644 --- a/frontend/cypress/support/commands.ts +++ b/frontend/cypress/support/commands.ts @@ -1,4 +1,5 @@ import '@testing-library/cypress/add-commands'; + import Login, { LoginProps } from '../CustomFunctions/Login'; Cypress.Commands.add('login', Login); diff --git a/frontend/package.json b/frontend/package.json index 9ae091b47d..c2da7b39f6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -26,10 +26,8 @@ "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.1.0", "@testing-library/user-event": "^12.1.10", - "@types/chart.js": "^2.9.28", "@types/d3": "^6.2.0", "@types/jest": "^26.0.15", - "@types/node": "^14.14.7", "@types/react": "^17.0.0", "@types/react-dom": "^16.9.9", "@types/react-redux": "^7.1.11", @@ -37,7 +35,7 @@ "@types/redux": "^3.6.0", "@types/styled-components": "^5.1.4", "@types/vis": "^4.21.21", - "antd": "^4.8.0", + "antd": "^4.16.13", "axios": "^0.21.0", "babel-eslint": "^10.1.0", "babel-jest": "^26.6.0", @@ -48,7 +46,8 @@ "bfj": "^7.0.2", "camelcase": "^6.1.0", "case-sensitive-paths-webpack-plugin": "2.3.0", - "chart.js": "^2.9.4", + "chart.js": "^3.4.0", + "chartjs-adapter-date-fns": "^2.0.0", "css-loader": "4.3.0", "d3": "^6.2.0", "d3-flame-graph": "^3.1.1", @@ -64,6 +63,7 @@ "eslint-webpack-plugin": "^2.1.0", "file-loader": "6.1.1", "fs-extra": "^9.0.1", + "history": "4.10.1", "html-webpack-plugin": "5.1.0", "identity-obj-proxy": "3.0.0", "jest": "26.6.0", @@ -78,13 +78,13 @@ "prop-types": "^15.6.2", "react": "17.0.0", "react-app-polyfill": "^2.0.0", - "react-chartjs-2": "^2.11.1", "react-chips": "^0.8.0", "react-css-theme-switcher": "^0.1.6", "react-dev-utils": "^11.0.0", "react-dom": "17.0.0", "react-force-graph": "^1.41.0", "react-graph-vis": "^1.0.5", + "react-grid-layout": "^1.2.5", "react-modal": "^3.12.1", "react-redux": "^7.2.2", "react-refresh": "^0.8.3", @@ -103,6 +103,7 @@ "tsconfig-paths-webpack-plugin": "^3.5.1", "typescript": "^4.0.5", "url-loader": "4.1.1", + "uuid": "^8.3.2", "web-vitals": "^0.2.4", "webpack": "^5.23.0", "webpack-dev-server": "^3.11.2", @@ -129,7 +130,11 @@ "@babel/preset-react": "^7.12.13", "@babel/preset-typescript": "^7.12.17", "@testing-library/cypress": "^8.0.0", + "@types/d3-tip": "^3.5.5", "@types/lodash-es": "^4.17.4", + "@types/node": "^14.17.12", + "@types/react-grid-layout": "^1.1.2", + "@types/uuid": "^8.3.1", "@typescript-eslint/eslint-plugin": "^4.28.2", "@typescript-eslint/parser": "^4.28.2", "autoprefixer": "^9.0.0", @@ -138,9 +143,11 @@ "copy-webpack-plugin": "^7.0.0", "cypress": "^8.3.0", "eslint": "^7.30.0", + "eslint-config-prettier": "^8.3.0", "eslint-config-standard": "^16.0.3", "eslint-plugin-import": "^2.23.4", "eslint-plugin-node": "^11.1.0", + "eslint-plugin-prettier": "^4.0.0", "eslint-plugin-promise": "^5.1.0", "eslint-plugin-react": "^7.24.0", "eslint-plugin-simple-import-sort": "^7.0.0", diff --git a/frontend/src/AppRoutes/index.tsx b/frontend/src/AppRoutes/index.tsx index b38409b676..a99c68b9d1 100644 --- a/frontend/src/AppRoutes/index.tsx +++ b/frontend/src/AppRoutes/index.tsx @@ -2,28 +2,31 @@ import NotFound from 'components/NotFound'; import Spinner from 'components/Spinner'; import { IS_LOGGED_IN } from 'constants/auth'; import ROUTES from 'constants/routes'; +import history from 'lib/history'; import AppLayout from 'modules/AppLayout'; import { RouteProvider } from 'modules/RouteProvider'; import React, { Suspense } from 'react'; -import { BrowserRouter, Redirect,Route, Switch } from 'react-router-dom'; +import { Redirect, Route, Router, Switch } from 'react-router-dom'; import routes from './routes'; -const App = () => ( - +const App = (): JSX.Element => ( + }> - {routes.map(({ path, component, exact }) => { - return ; + {routes.map(({ path, component, exact }, index) => { + return ( + + ); })} {/* This logic should be moved to app layout */} { + render={(): JSX.Element => { return localStorage.getItem(IS_LOGGED_IN) === 'yes' ? ( ) : ( @@ -31,12 +34,12 @@ const App = () => ( ); }} /> - + - + ); export default App; diff --git a/frontend/src/pages.ts b/frontend/src/AppRoutes/pageComponents.ts similarity index 75% rename from frontend/src/pages.ts rename to frontend/src/AppRoutes/pageComponents.ts index a0d18b811a..b9e2b21886 100644 --- a/frontend/src/pages.ts +++ b/frontend/src/AppRoutes/pageComponents.ts @@ -1,4 +1,4 @@ -import Loadable from './components/Loadable'; +import Loadable from 'components/Loadable'; export const ServiceMetricsPage = Loadable( () => @@ -59,3 +59,16 @@ export const InstrumentationPage = Loadable( /* webpackChunkName: "InstrumentationPage" */ 'modules/add-instrumentation/instrumentationPage' ), ); + +export const DashboardPage = Loadable( + () => import(/* webpackChunkName: "DashboardPage" */ 'pages/Dashboard'), +); + +export const NewDashboardPage = Loadable( + () => import(/* webpackChunkName: "New DashboardPage" */ 'pages/NewDashboard'), +); + +export const DashboardWidget = Loadable( + () => + import(/* webpackChunkName: "New DashboardPage" */ 'pages/DashboardWidget'), +); diff --git a/frontend/src/AppRoutes/routes.ts b/frontend/src/AppRoutes/routes.ts index d247e9f6c3..f2dc54544d 100644 --- a/frontend/src/AppRoutes/routes.ts +++ b/frontend/src/AppRoutes/routes.ts @@ -1,6 +1,11 @@ import ROUTES from 'constants/routes'; +import DashboardWidget from 'pages/DashboardWidget'; +import { RouteProps } from 'react-router-dom'; + import { + DashboardPage, InstrumentationPage, + NewDashboardPage, ServiceMapPage, ServiceMetricsPage, ServicesTablePage, @@ -9,8 +14,7 @@ import { TraceDetailPage, TraceGraphPage, UsageExplorerPage, -} from 'pages'; -import { RouteProps } from 'react-router-dom'; +} from './pageComponents'; const routes: AppRoutes[] = [ { @@ -58,6 +62,21 @@ const routes: AppRoutes[] = [ exact: true, component: TraceDetailPage, }, + { + path: ROUTES.ALL_DASHBOARD, + exact: true, + component: DashboardPage, + }, + { + path: ROUTES.DASHBOARD, + exact: true, + component: NewDashboardPage, + }, + { + path: ROUTES.DASHBOARD_WIDGET, + exact: true, + component: DashboardWidget, + }, ]; interface AppRoutes { diff --git a/frontend/src/api/ErrorResponseHandler.ts b/frontend/src/api/ErrorResponseHandler.ts new file mode 100644 index 0000000000..277510146a --- /dev/null +++ b/frontend/src/api/ErrorResponseHandler.ts @@ -0,0 +1,57 @@ +import { AxiosError } from 'axios'; +import { ErrorResponse } from 'types/api'; +import { ErrorStatusCode } from 'types/common'; + +export const ErrorResponseHandler = (error: AxiosError): ErrorResponse => { + if (error.response) { + // client received an error response (5xx, 4xx) + // making the error status code as standard Error Status Code + const statusCode = error.response.status as ErrorStatusCode; + + if (statusCode >= 400 && statusCode < 500) { + const { data } = error.response; + + if (statusCode === 404) { + return { + statusCode, + payload: null, + error: 'Not Found', + message: null, + }; + } + + return { + statusCode, + payload: null, + error: data.error, + message: null, + }; + } + + return { + statusCode, + payload: null, + error: 'Something went wrong', + message: null, + }; + } + if (error.request) { + // client never received a response, or request never left + console.error('client never received a response, or request never left'); + + return { + statusCode: 500, + payload: null, + error: 'Something went wrong', + message: null, + }; + } + // anything else + console.error('any'); + return { + statusCode: 500, + payload: null, + error: error.toString(), + message: null, + }; +}; diff --git a/frontend/src/api/dashboard/create.ts b/frontend/src/api/dashboard/create.ts new file mode 100644 index 0000000000..ab2ace2144 --- /dev/null +++ b/frontend/src/api/dashboard/create.ts @@ -0,0 +1,26 @@ +import axios from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { PayloadProps, Props } from 'types/api/dashboard/create'; + +const create = async ( + props: Props, +): Promise | ErrorResponse> => { + try { + const response = await axios.post('/dashboards', { + ...props, + }); + + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default create; diff --git a/frontend/src/api/dashboard/delete.ts b/frontend/src/api/dashboard/delete.ts new file mode 100644 index 0000000000..2f7c8f16b9 --- /dev/null +++ b/frontend/src/api/dashboard/delete.ts @@ -0,0 +1,24 @@ +import axios from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { Props } from 'types/api/dashboard/delete'; + +const deleteDashboard = async ( + props: Props, +): Promise | ErrorResponse> => { + try { + const response = await axios.delete(`/dashboards/${props.uuid}`); + + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default deleteDashboard; diff --git a/frontend/src/api/dashboard/get.ts b/frontend/src/api/dashboard/get.ts new file mode 100644 index 0000000000..6c9c953e7d --- /dev/null +++ b/frontend/src/api/dashboard/get.ts @@ -0,0 +1,24 @@ +import axios from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { PayloadProps, Props } from 'types/api/dashboard/get'; + +const get = async ( + props: Props, +): Promise | ErrorResponse> => { + try { + const response = await axios.get(`/dashboards/${props.uuid}`); + + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default get; diff --git a/frontend/src/api/dashboard/getAll.ts b/frontend/src/api/dashboard/getAll.ts new file mode 100644 index 0000000000..abd3e8fc93 --- /dev/null +++ b/frontend/src/api/dashboard/getAll.ts @@ -0,0 +1,24 @@ +import axios from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { PayloadProps } from 'types/api/dashboard/getAll'; + +const getAll = async (): Promise< + SuccessResponse | ErrorResponse +> => { + try { + const response = await axios.get('/dashboards'); + + return { + statusCode: 200, + error: null, + message: response.data.message, + payload: response.data.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default getAll; diff --git a/frontend/src/api/dashboard/update.ts b/frontend/src/api/dashboard/update.ts new file mode 100644 index 0000000000..b22689c0ed --- /dev/null +++ b/frontend/src/api/dashboard/update.ts @@ -0,0 +1,26 @@ +import axios from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { PayloadProps, Props } from 'types/api/dashboard/update'; + +const update = async ( + props: Props, +): Promise | ErrorResponse> => { + try { + const response = await axios.put(`/dashboards/${props.uuid}`, { + ...props, + }); + + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default update; diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 4cd108b169..b1592ba7dc 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -1,10 +1,10 @@ -import axios, { AxiosRequestConfig } from 'axios'; +import axios from 'axios'; import { ENVIRONMENT } from 'constants/env'; import apiV1 from './apiV1'; export default axios.create({ - baseURL: `${ENVIRONMENT.baseURL}`, + baseURL: `${ENVIRONMENT.baseURL}${apiV1}`, }); export { apiV1 }; diff --git a/frontend/src/api/widgets/getQuery.ts b/frontend/src/api/widgets/getQuery.ts new file mode 100644 index 0000000000..a706db3b77 --- /dev/null +++ b/frontend/src/api/widgets/getQuery.ts @@ -0,0 +1,26 @@ +import axios from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { PayloadProps, Props } from 'types/api/widgets/getQuery'; + +const getQuery = async ( + props: Props, +): Promise | ErrorResponse> => { + try { + const response = await axios.get( + `/query_range?query=${props.query}&start=${props.start}&end=${props.end}&step=${props.step}`, + ); + + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default getQuery; diff --git a/frontend/src/assets/Dashboard/TimeSeries.tsx b/frontend/src/assets/Dashboard/TimeSeries.tsx new file mode 100644 index 0000000000..de50a3e512 --- /dev/null +++ b/frontend/src/assets/Dashboard/TimeSeries.tsx @@ -0,0 +1,35 @@ +import React from 'react'; + +const TimeSeries = (): JSX.Element => ( + + + + + + + + + +); + +export default TimeSeries; diff --git a/frontend/src/assets/Dashboard/Value.tsx b/frontend/src/assets/Dashboard/Value.tsx new file mode 100644 index 0000000000..61bf672fa5 --- /dev/null +++ b/frontend/src/assets/Dashboard/Value.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +const Value = (): JSX.Element => ( + + + +); + +export default Value; diff --git a/frontend/src/assets/index.css b/frontend/src/assets/index.css index a7f60fc45f..7e078e2a59 100644 --- a/frontend/src/assets/index.css +++ b/frontend/src/assets/index.css @@ -1,4 +1,4 @@ -@import "~antd/dist/antd.dark.css"; +@import '~antd/dist/antd.dark.css'; .ant-space-item { margin-right: 0 !important; @@ -10,3 +10,11 @@ #chart { width: 100%; } + +.ant-tabs-tab { + margin: 0 0 0 32px !important; +} + +.ant-tabs-nav-list > .ant-tabs-tab:first-child { + margin: 0 !important; +} diff --git a/frontend/src/components/Graph/Legend/index.tsx b/frontend/src/components/Graph/Legend/index.tsx new file mode 100644 index 0000000000..41255241db --- /dev/null +++ b/frontend/src/components/Graph/Legend/index.tsx @@ -0,0 +1,24 @@ +import { Typography } from 'antd'; +import React from 'react'; + +import { ColorContainer, Container } from './styles'; + +const Legend = ({ text, color }: LegendProps): JSX.Element => { + if (text.length === 0) { + return <>; + } + + return ( + + + {text} + + ); +}; + +interface LegendProps { + text: string; + color: string; +} + +export default Legend; diff --git a/frontend/src/components/Graph/Legend/styles.ts b/frontend/src/components/Graph/Legend/styles.ts new file mode 100644 index 0000000000..ac627bf0ea --- /dev/null +++ b/frontend/src/components/Graph/Legend/styles.ts @@ -0,0 +1,20 @@ +import styled from 'styled-components'; + +export const Container = styled.div` + margin-left: 2rem; + margin-right: 2rem; + display: flex; + cursor: pointer; +`; + +interface Props { + color: string; +} + +export const ColorContainer = styled.div` + background-color: ${({ color }): string => color}; + border-radius: 50%; + width: 20px; + height: 20px; + margin-right: 0.5rem; +`; diff --git a/frontend/src/components/Graph/index.tsx b/frontend/src/components/Graph/index.tsx new file mode 100644 index 0000000000..bb14085d62 --- /dev/null +++ b/frontend/src/components/Graph/index.tsx @@ -0,0 +1,235 @@ +import { + BarController, + BarElement, + CategoryScale, + Chart, + ChartOptions, + ChartType, + Decimation, + Filler, + Legend, + // LegendItem, + LinearScale, + LineController, + LineElement, + PointElement, + ScaleOptions, + SubTitle, + TimeScale, + TimeSeriesScale, + Title, + Tooltip, +} from 'chart.js'; +import chartjsAdapter from 'chartjs-adapter-date-fns'; +// import { colors } from 'lib/getRandomColor'; +// import stringToHTML from 'lib/stringToHTML'; +import React, { useCallback, useEffect, useRef } from 'react'; +import { useThemeSwitcher } from 'react-css-theme-switcher'; + +// import Legends from './Legend'; +// import { LegendsContainer } from './styles'; + +const Graph = ({ + data, + type, + title, + isStacked, + label, + xAxisType, + onClickHandler, +}: GraphProps): JSX.Element => { + const chartRef = useRef(null); + const { currentTheme } = useThemeSwitcher(); + + // const [tooltipVisible, setTooltipVisible] = useState(false); + const lineChartRef = useRef(); + + const getGridColor = useCallback(() => { + if (currentTheme === undefined) { + return 'rgba(231,233,237,0.1)'; + } + + if (currentTheme === 'dark') { + return 'rgba(231,233,237,0.1)'; + } + + return 'rgba(231,233,237,0.8)'; + }, [currentTheme]); + + const buildChart = useCallback(() => { + if (lineChartRef.current !== undefined) { + lineChartRef.current.destroy(); + } + + if (chartRef.current !== null) { + Chart.register( + LineElement, + PointElement, + LineController, + CategoryScale, + LinearScale, + TimeScale, + TimeSeriesScale, + Decimation, + Filler, + Legend, + Title, + Tooltip, + SubTitle, + BarController, + BarElement, + ); + + const options: ChartOptions = { + responsive: true, + maintainAspectRatio: false, + interaction: { + mode: 'index', + intersect: false, + }, + plugins: { + title: { + display: title === undefined ? false : true, + text: title, + }, + legend: { + // just making sure that label is present + display: !( + data.datasets.find((e) => e.label !== undefined) === undefined + ), + labels: { + usePointStyle: true, + pointStyle: 'circle', + }, + position: 'bottom', + // labels: { + // generateLabels: (chart: Chart): LegendItem[] => { + // return (data.datasets || []).map((e, index) => { + // return { + // text: e.label || '', + // datasetIndex: index, + // }; + // }); + // }, + // pointStyle: 'circle', + // usePointStyle: true, + // }, + }, + }, + layout: { + padding: 0, + }, + scales: { + x: { + animate: false, + grid: { + display: true, + color: getGridColor(), + }, + labels: label, + adapters: { + date: chartjsAdapter, + }, + type: xAxisType, + }, + y: { + display: true, + grid: { + display: true, + color: getGridColor(), + }, + }, + stacked: { + display: isStacked === undefined ? false : 'auto', + }, + }, + elements: { + line: { + tension: 0, + cubicInterpolationMode: 'monotone', + }, + }, + onClick: onClickHandler, + }; + + lineChartRef.current = new Chart(chartRef.current, { + type: type, + data: data, + options, + // plugins: [ + // { + // id: 'htmlLegendPlugin', + // afterUpdate: (chart: Chart): void => { + // if ( + // chart && + // chart.options && + // chart.options.plugins && + // chart.options.plugins.legend && + // chart.options.plugins.legend.labels && + // chart.options.plugins.legend.labels.generateLabels + // ) { + // const labels = chart.options.plugins?.legend?.labels?.generateLabels( + // chart, + // ); + + // const id = 'htmlLegend'; + + // const response = document.getElementById(id); + + // if (labels && response && response?.childNodes.length === 0) { + // const labelComponent = labels.map((e, index) => { + // return { + // element: Legends({ + // text: e.text, + // color: colors[index] || 'white', + // }), + // dataIndex: e.datasetIndex, + // }; + // }); + + // labelComponent.map((e) => { + // const el = stringToHTML(e.element); + + // if (el) { + // el.addEventListener('click', () => { + // chart.setDatasetVisibility( + // e.dataIndex, + // !chart.isDatasetVisible(e.dataIndex), + // ); + // chart.update(); + // }); + // response.append(el); + // } + // }); + // } + // } + // }, + // }, + // ], + }); + } + }, [chartRef, data, type, title, isStacked, label, xAxisType, getGridColor]); + + useEffect(() => { + buildChart(); + }, [buildChart]); + + return ( + <> + + {/* */} + + ); +}; + +interface GraphProps { + type: ChartType; + data: Chart['data']; + title?: string; + isStacked?: boolean; + label?: string[]; + xAxisType?: ScaleOptions['type']; + onClickHandler?: ChartOptions['onClick']; +} + +export default Graph; diff --git a/frontend/src/components/Graph/styles.ts b/frontend/src/components/Graph/styles.ts new file mode 100644 index 0000000000..204a78a57f --- /dev/null +++ b/frontend/src/components/Graph/styles.ts @@ -0,0 +1,8 @@ +import styled from 'styled-components'; + +export const LegendsContainer = styled.div` + display: flex; + overflow-y: scroll; + margin-right: 1rem; + margin-bottom: 1rem; +`; diff --git a/frontend/src/components/Input/index.tsx b/frontend/src/components/Input/index.tsx new file mode 100644 index 0000000000..939d59455f --- /dev/null +++ b/frontend/src/components/Input/index.tsx @@ -0,0 +1,48 @@ +import { Form, Input, InputProps } from 'antd'; +import React from 'react'; + +const InputComponent = ({ + value, + type = 'text', + onChangeHandler, + placeholder, + ref, + size = 'small', + onBlurHandler, + onPressEnterHandler, + label, + labelOnTop, + addonBefore, + ...props +}: InputComponentProps): JSX.Element => ( + + + +); + +interface InputComponentProps extends InputProps { + value: InputProps['value']; + type?: InputProps['type']; + onChangeHandler?: React.ChangeEventHandler; + placeholder?: InputProps['placeholder']; + ref?: React.LegacyRef; + size?: InputProps['size']; + onBlurHandler?: React.FocusEventHandler; + onPressEnterHandler?: React.KeyboardEventHandler; + label?: string; + labelOnTop?: boolean; + addonBefore?: React.ReactNode; +} + +export default InputComponent; diff --git a/frontend/src/components/Loadable/index.tsx b/frontend/src/components/Loadable/index.tsx index 5614b45110..f1b6774b16 100644 --- a/frontend/src/components/Loadable/index.tsx +++ b/frontend/src/components/Loadable/index.tsx @@ -1,4 +1,4 @@ -import { ComponentType,lazy } from 'react'; +import { ComponentType, lazy } from 'react'; function Loadable(importPath: { (): LoadableProps; diff --git a/frontend/src/components/Modal.tsx b/frontend/src/components/Modal.tsx index e026a6f06f..8d7df61d78 100644 --- a/frontend/src/components/Modal.tsx +++ b/frontend/src/components/Modal.tsx @@ -1,21 +1,13 @@ -import { Modal } from 'antd'; +import { Modal, ModalProps as Props } from 'antd'; import React, { ReactElement } from 'react'; -export const CustomModal = ({ +const CustomModal = ({ title, children, isModalVisible, - setIsModalVisible, footer, closable = true, -}: { - isModalVisible: boolean; - closable?: boolean; - setIsModalVisible: Function; - footer?: any; - title: string; - children: ReactElement; -}) => { +}: ModalProps): JSX.Element => { return ( <> ); }; + +interface ModalProps { + isModalVisible: boolean; + closable?: boolean; + footer?: Props['footer']; + title: string; + children: ReactElement; +} + +export default CustomModal; diff --git a/frontend/src/components/NotFound/index.tsx b/frontend/src/components/NotFound/index.tsx index b66af95fb7..1618484b24 100644 --- a/frontend/src/components/NotFound/index.tsx +++ b/frontend/src/components/NotFound/index.tsx @@ -2,7 +2,7 @@ import NotFoundImage from 'assets/NotFound'; import ROUTES from 'constants/routes'; import React from 'react'; -import { Button, Container,Text, TextContainer } from './styles'; +import { Button, Container, Text, TextContainer } from './styles'; const NotFound = (): JSX.Element => { return ( diff --git a/frontend/src/components/SideNav/index.tsx b/frontend/src/components/SideNav/index.tsx index 0d91094496..d2159259d2 100644 --- a/frontend/src/components/SideNav/index.tsx +++ b/frontend/src/components/SideNav/index.tsx @@ -1,13 +1,15 @@ -import React, { useState, useCallback } from "react"; -import { Layout, Menu, Switch as ToggleButton } from "antd"; -import { NavLink } from "react-router-dom"; -import { useThemeSwitcher } from "react-css-theme-switcher"; -import { useLocation } from "react-router-dom"; -import ROUTES from "constants/routes"; - -import { ThemeSwitcherWrapper, Logo } from "./styles"; +import { Layout, Menu, Switch as ToggleButton, Typography } from 'antd'; +import ROUTES from 'constants/routes'; +import React, { useCallback, useState } from 'react'; +import { useThemeSwitcher } from 'react-css-theme-switcher'; +import { NavLink } from 'react-router-dom'; +import { useLocation } from 'react-router-dom'; + +import { Logo, ThemeSwitcherWrapper } from './styles'; const { Sider } = Layout; -import menus from "./menuItems"; +import history from 'lib/history'; + +import menus from './menuItems'; const SideNav = (): JSX.Element => { const { switcher, currentTheme, themes } = useThemeSwitcher(); @@ -15,14 +17,21 @@ const SideNav = (): JSX.Element => { const [collapsed, setCollapsed] = useState(false); const { pathname } = useLocation(); - const toggleTheme = useCallback((isChecked: boolean) => { - switcher({ theme: isChecked ? themes.dark : themes.light }); - }, []); + const toggleTheme = useCallback( + (isChecked: boolean) => { + switcher({ theme: isChecked ? themes.dark : themes.light }); + }, + [switcher, themes], + ); const onCollapse = useCallback(() => { setCollapsed((collapsed) => !collapsed); }, []); + const onClickHandler = useCallback((to: string) => { + history.push(to); + }, []); + return ( @@ -32,7 +41,7 @@ const SideNav = (): JSX.Element => { /> - + { > {menus.map(({ to, Icon, name }) => ( }> - - {name} - +
onClickHandler(to)}> + {name} +
))}
diff --git a/frontend/src/components/SideNav/menuItems.ts b/frontend/src/components/SideNav/menuItems.ts index f857412389..acbf400d0c 100644 --- a/frontend/src/components/SideNav/menuItems.ts +++ b/frontend/src/components/SideNav/menuItems.ts @@ -1,43 +1,49 @@ import { - BarChartOutlined, AlignLeftOutlined, + ApiOutlined, + BarChartOutlined, + DashboardFilled, DeploymentUnitOutlined, LineChartOutlined, SettingOutlined, - ApiOutlined, -} from "@ant-design/icons"; -import ROUTES from "constants/routes"; +} from '@ant-design/icons'; +import ROUTES from 'constants/routes'; const menus: SidebarMenu[] = [ { Icon: BarChartOutlined, to: ROUTES.APPLICATION, - name: "Metrics", + name: 'Metrics', }, { Icon: AlignLeftOutlined, to: ROUTES.TRACES, - name: "Traces", + name: 'Traces', }, { to: ROUTES.SERVICE_MAP, - name: "Service Map", + name: 'Service Map', Icon: DeploymentUnitOutlined, }, { Icon: LineChartOutlined, to: ROUTES.USAGE_EXPLORER, - name: "Usage Explorer", + name: 'Usage Explorer', }, { Icon: SettingOutlined, to: ROUTES.SETTINGS, - name: "Settings", + name: 'Settings', }, { Icon: ApiOutlined, to: ROUTES.INSTRUMENTATION, - name: "Add instrumentation", + name: 'Add instrumentation', + }, + { + Icon: DashboardFilled, + to: ROUTES.ALL_DASHBOARD, + name: 'Dashboard', }, ]; diff --git a/frontend/src/components/SideNav/styles.ts b/frontend/src/components/SideNav/styles.ts index 886559a2ad..0885303d63 100644 --- a/frontend/src/components/SideNav/styles.ts +++ b/frontend/src/components/SideNav/styles.ts @@ -1,4 +1,4 @@ -import styled from "styled-components"; +import styled from 'styled-components'; export const ThemeSwitcherWrapper = styled.div` display: flex; @@ -10,7 +10,7 @@ export const ThemeSwitcherWrapper = styled.div` export const Logo = styled.img` width: 100px; margin: 5%; - display: ${({ collapsed }) => (!collapsed ? "block" : "none")}; + display: ${({ collapsed }): string => (!collapsed ? 'block' : 'none')}; `; interface LogoProps { diff --git a/frontend/src/components/TimePreferenceDropDown/index.tsx b/frontend/src/components/TimePreferenceDropDown/index.tsx new file mode 100644 index 0000000000..02b75c1e2b --- /dev/null +++ b/frontend/src/components/TimePreferenceDropDown/index.tsx @@ -0,0 +1,52 @@ +import { Button, Dropdown, Menu, Typography } from 'antd'; +import timeItems, { + timePreferance, + timePreferenceType, +} from 'container/NewWidget/RightContainer/timeItems'; +import React, { useCallback } from 'react'; + +import { TextContainer } from './styles'; + +const TimePreference = ({ + setSelectedTime, + selectedTime, +}: TimePreferenceDropDownProps): JSX.Element => { + const timeMenuItemOnChangeHandler = useCallback( + (event: TimeMenuItemOnChangeHandlerEvent) => { + const selectedTime = timeItems.find((e) => e.enum === event.key); + if (selectedTime !== undefined) { + setSelectedTime(selectedTime); + } + }, + [setSelectedTime], + ); + + return ( + + + {timeItems.map((item) => ( + + {item.name} + + ))} + + } + > + + + + ); +}; + +interface TimeMenuItemOnChangeHandlerEvent { + key: timePreferenceType | string; +} + +interface TimePreferenceDropDownProps { + setSelectedTime: React.Dispatch>; + selectedTime: timePreferance; +} + +export default TimePreference; diff --git a/frontend/src/components/TimePreferenceDropDown/styles.ts b/frontend/src/components/TimePreferenceDropDown/styles.ts new file mode 100644 index 0000000000..e2eeac983c --- /dev/null +++ b/frontend/src/components/TimePreferenceDropDown/styles.ts @@ -0,0 +1,14 @@ +import styled from 'styled-components'; + +interface TextContainerProps { + noButtonMargin?: boolean; +} + +export const TextContainer = styled.div` + display: flex; + + > button { + margin-left: ${({ noButtonMargin }): string => { + return noButtonMargin ? '0' : '0.5rem'; + }} +`; diff --git a/frontend/src/components/ValueGraph/index.tsx b/frontend/src/components/ValueGraph/index.tsx new file mode 100644 index 0000000000..c25fd60e15 --- /dev/null +++ b/frontend/src/components/ValueGraph/index.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +import { Value } from './styles'; + +const ValueGraph = ({ value }: ValueGraphProps): JSX.Element => ( + {value} +); + +interface ValueGraphProps { + value: string; +} + +export default ValueGraph; diff --git a/frontend/src/components/ValueGraph/styles.ts b/frontend/src/components/ValueGraph/styles.ts new file mode 100644 index 0000000000..af9f9c7ad0 --- /dev/null +++ b/frontend/src/components/ValueGraph/styles.ts @@ -0,0 +1,6 @@ +import { Typography } from 'antd'; +import styled from 'styled-components'; + +export const Value = styled(Typography)` + font-size: 3rem; +`; diff --git a/frontend/src/constants/query.ts b/frontend/src/constants/query.ts index 5752c7f343..9e327ed6d4 100644 --- a/frontend/src/constants/query.ts +++ b/frontend/src/constants/query.ts @@ -5,5 +5,5 @@ export enum METRICS_PAGE_QUERY_PARAM { service = 'service', error = 'error', operation = 'operation', - kind = 'kind' + kind = 'kind', } diff --git a/frontend/src/constants/routes.ts b/frontend/src/constants/routes.ts index 4714170b50..40ab99fe66 100644 --- a/frontend/src/constants/routes.ts +++ b/frontend/src/constants/routes.ts @@ -8,6 +8,9 @@ const ROUTES = { INSTRUMENTATION: '/add-instrumentation', USAGE_EXPLORER: '/usage-explorer', APPLICATION: '/application', + ALL_DASHBOARD: '/dashboard', + DASHBOARD: '/dashboard/:dashboardId', + DASHBOARD_WIDGET: '/dashboard/:dashboardId/:widgetId', }; export default ROUTES; diff --git a/frontend/src/container/GridGraphComponent/index.tsx b/frontend/src/container/GridGraphComponent/index.tsx new file mode 100644 index 0000000000..4831bf7794 --- /dev/null +++ b/frontend/src/container/GridGraphComponent/index.tsx @@ -0,0 +1,71 @@ +import { Typography } from 'antd'; +import { ChartData } from 'chart.js'; +import Graph from 'components/Graph'; +import ValueGraph from 'components/ValueGraph'; +import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; +import history from 'lib/history'; +import React from 'react'; + +import { TitleContainer, ValueContainer } from './styles'; + +const GridGraphComponent = ({ + GRAPH_TYPES, + data, + title, + opacity, + isStacked, +}: GridGraphComponentProps): JSX.Element | null => { + const location = history.location.pathname; + + const isDashboardPage = location.split('/').length === 3; + + if (GRAPH_TYPES === 'TIME_SERIES') { + return ( + + ); + } + + if (GRAPH_TYPES === 'VALUE') { + const value = (((data.datasets[0] || []).data || [])[0] || 0) as number; + + if (data.datasets.length === 0) { + return ( + + No Data + + ); + } + + return ( + <> + + {title} + + + + + + ); + } + + return null; +}; + +export interface GridGraphComponentProps { + GRAPH_TYPES: GRAPH_TYPES; + data: ChartData; + title?: string; + opacity?: string; + isStacked?: boolean; +} + +export default GridGraphComponent; diff --git a/frontend/src/container/GridGraphComponent/styles.ts b/frontend/src/container/GridGraphComponent/styles.ts new file mode 100644 index 0000000000..a4450f2797 --- /dev/null +++ b/frontend/src/container/GridGraphComponent/styles.ts @@ -0,0 +1,19 @@ +import styled from 'styled-components'; + +interface Props { + isDashboardPage: boolean; +} +export const ValueContainer = styled.div` + height: ${({ isDashboardPage }): string => + isDashboardPage ? '100%' : '55vh'}; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; +`; + +export const TitleContainer = styled.div` + text-align: center; + padding-top: ${({ isDashboardPage }): string => + !isDashboardPage ? '1rem' : '0rem'}; +`; diff --git a/frontend/src/container/GridGraphLayout/AddWidget/index.tsx b/frontend/src/container/GridGraphLayout/AddWidget/index.tsx new file mode 100644 index 0000000000..39f8c32448 --- /dev/null +++ b/frontend/src/container/GridGraphLayout/AddWidget/index.tsx @@ -0,0 +1,55 @@ +import { PlusOutlined } from '@ant-design/icons'; +import { Typography } from 'antd'; +import React, { useCallback } from 'react'; +import { connect, useSelector } from 'react-redux'; +import { bindActionCreators, Dispatch } from 'redux'; +import { ThunkDispatch } from 'redux-thunk'; +import { + ToggleAddWidget, + ToggleAddWidgetProps, +} from 'store/actions/dashboard/toggleAddWidget'; +import { AppState } from 'store/reducers'; +import AppActions from 'types/actions'; +import DashboardReducer from 'types/reducer/dashboards'; + +import { Button, Container } from './styles'; + +const AddWidget = ({ toggleAddWidget }: Props): JSX.Element => { + const { isAddWidget } = useSelector( + (state) => state.dashboards, + ); + + const onToggleHandler = useCallback(() => { + toggleAddWidget(true); + }, [toggleAddWidget]); + + return ( + + {!isAddWidget ? ( + <> + + + ) : ( + Click a widget icon to add it here + )} + + ); +}; + +interface DispatchProps { + toggleAddWidget: ( + props: ToggleAddWidgetProps, + ) => (dispatch: Dispatch) => void; +} + +const mapDispatchToProps = ( + dispatch: ThunkDispatch, +): DispatchProps => ({ + toggleAddWidget: bindActionCreators(ToggleAddWidget, dispatch), +}); + +type Props = DispatchProps; + +export default connect(null, mapDispatchToProps)(AddWidget); diff --git a/frontend/src/container/GridGraphLayout/AddWidget/styles.ts b/frontend/src/container/GridGraphLayout/AddWidget/styles.ts new file mode 100644 index 0000000000..9a7d6b58f3 --- /dev/null +++ b/frontend/src/container/GridGraphLayout/AddWidget/styles.ts @@ -0,0 +1,18 @@ +import { Button as ButtonComponent } from 'antd'; +import styled from 'styled-components'; + +export const Button = styled(ButtonComponent)` + &&& { + display: flex; + justify-content: center; + align-items: center; + border: none; + } +`; + +export const Container = styled.div` + display: flex; + justify-content: center; + align-items: center; + height: 100%; +`; diff --git a/frontend/src/container/GridGraphLayout/Graph/Bar/index.tsx b/frontend/src/container/GridGraphLayout/Graph/Bar/index.tsx new file mode 100644 index 0000000000..3c3cce7064 --- /dev/null +++ b/frontend/src/container/GridGraphLayout/Graph/Bar/index.tsx @@ -0,0 +1,40 @@ +import { + DeleteOutlined, + EditFilled, + FullscreenOutlined, +} from '@ant-design/icons'; +import React, { useCallback } from 'react'; +import { useHistory, useLocation } from 'react-router'; +import { Widgets } from 'types/api/dashboard/getAll'; + +import { Container } from './styles'; + +const Bar = ({ + widget, + onViewFullScreenHandler, + onDeleteHandler, +}: BarProps): JSX.Element => { + const { push } = useHistory(); + const { pathname } = useLocation(); + + const onEditHandler = useCallback(() => { + const widgetId = widget.id; + push(`${pathname}/new?widgetId=${widgetId}&graphType=${widget.panelTypes}`); + }, [push, pathname, widget]); + + return ( + + + + + + ); +}; + +interface BarProps { + widget: Widgets; + onViewFullScreenHandler: () => void; + onDeleteHandler: () => void; +} + +export default Bar; diff --git a/frontend/src/container/GridGraphLayout/Graph/Bar/styles.ts b/frontend/src/container/GridGraphLayout/Graph/Bar/styles.ts new file mode 100644 index 0000000000..ca8073a672 --- /dev/null +++ b/frontend/src/container/GridGraphLayout/Graph/Bar/styles.ts @@ -0,0 +1,15 @@ +import styled from 'styled-components'; + +export const Container = styled.div` + height: 15%; + align-items: center; + justify-content: flex-end; + display: flex; + gap: 1rem; + padding-right: 1rem; + padding-left: 1rem; + padding-top: 0.5rem; + position: absolute; + top: 0; + right: 0; +`; diff --git a/frontend/src/container/GridGraphLayout/Graph/FullView/index.tsx b/frontend/src/container/GridGraphLayout/Graph/FullView/index.tsx new file mode 100644 index 0000000000..5f9310d312 --- /dev/null +++ b/frontend/src/container/GridGraphLayout/Graph/FullView/index.tsx @@ -0,0 +1,181 @@ +import { Button, Typography } from 'antd'; +import getQueryResult from 'api/widgets/getQuery'; +import { AxiosError } from 'axios'; +import { ChartData } from 'chart.js'; +import Spinner from 'components/Spinner'; +import TimePreference from 'components/TimePreferenceDropDown'; +import GridGraphComponent from 'container/GridGraphComponent'; +import { + timeItems, + timePreferance, +} from 'container/NewWidget/RightContainer/timeItems'; +import getChartData from 'lib/getChartData'; +import GetMaxMinTime from 'lib/getMaxMinTime'; +import getStartAndEndTime from 'lib/getStartAndEndTime'; +import React, { useCallback, useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { GlobalTime } from 'store/actions'; +import { AppState } from 'store/reducers'; +import { Widgets } from 'types/api/dashboard/getAll'; + +import { GraphContainer, NotFoundContainer, TimeContainer } from './styles'; + +const FullView = ({ widget }: FullViewProps): JSX.Element => { + const { minTime, maxTime } = useSelector( + (state) => state.globalTime, + ); + + const [state, setState] = useState({ + error: false, + errorMessage: '', + loading: true, + payload: undefined, + }); + + const getSelectedTime = useCallback( + () => + timeItems.find((e) => e.enum === (widget?.timePreferance || 'GLOBAL_TIME')), + [widget], + ); + + const [selectedTime, setSelectedTime] = useState({ + name: getSelectedTime()?.name || '', + enum: widget?.timePreferance || 'GLOBAL_TIME', + }); + + const onFetchDataHandler = useCallback(async () => { + try { + const maxMinTime = GetMaxMinTime({ + graphType: widget.panelTypes, + maxTime, + minTime, + }); + + const { end, start } = getStartAndEndTime({ + type: selectedTime.enum, + maxTime: maxMinTime.maxTime, + minTime: maxMinTime.minTime, + }); + + const response = await Promise.all( + widget.query + .filter((e) => e.query.length !== 0) + .map(async (query) => { + const result = await getQueryResult({ + end, + query: query.query, + start: start, + step: '30', + }); + return { + query: query.query, + queryData: result, + legend: query.legend, + }; + }), + ); + + const isError = response.find((e) => e.queryData.statusCode !== 200); + + if (isError !== undefined) { + setState((state) => ({ + ...state, + error: true, + errorMessage: isError.queryData.error || 'Something went wrong', + loading: false, + })); + } else { + const chartDataSet = getChartData({ + queryData: { + data: response.map((e) => ({ + query: e.query, + legend: e.legend, + queryData: e.queryData.payload?.result || [], + })), + error: false, + errorMessage: '', + loading: false, + }, + }); + + setState((state) => ({ + ...state, + loading: false, + payload: chartDataSet, + })); + } + } catch (error) { + setState((state) => ({ + ...state, + error: true, + errorMessage: (error as AxiosError).toString(), + loading: false, + })); + } + }, [widget, maxTime, minTime, selectedTime.enum]); + + useEffect(() => { + onFetchDataHandler(); + }, [onFetchDataHandler]); + + if (state.loading || state.payload === undefined) { + return ; + } + + if (state.loading === false && state.payload.datasets.length === 0) { + return ( + <> + + + No Data + + + ); + } + + return ( + <> + + + + + + + + + + ); +}; + +interface FullViewState { + loading: boolean; + error: boolean; + errorMessage: string; + payload: ChartData | undefined; +} + +interface FullViewProps { + widget: Widgets; +} + +export default FullView; diff --git a/frontend/src/container/GridGraphLayout/Graph/FullView/styles.ts b/frontend/src/container/GridGraphLayout/Graph/FullView/styles.ts new file mode 100644 index 0000000000..10d21cd4da --- /dev/null +++ b/frontend/src/container/GridGraphLayout/Graph/FullView/styles.ts @@ -0,0 +1,21 @@ +import styled from 'styled-components'; + +export const GraphContainer = styled.div` + min-height: 70vh; + align-items: center; + justify-content: center; + display: flex; + flex-direction: column; +`; + +export const NotFoundContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; + min-height: 55vh; +`; + +export const TimeContainer = styled.div` + display: flex; + justify-content: flex-end; +`; diff --git a/frontend/src/container/GridGraphLayout/Graph/index.tsx b/frontend/src/container/GridGraphLayout/Graph/index.tsx new file mode 100644 index 0000000000..05adbd4654 --- /dev/null +++ b/frontend/src/container/GridGraphLayout/Graph/index.tsx @@ -0,0 +1,209 @@ +import { Typography } from 'antd'; +import getQueryResult from 'api/widgets/getQuery'; +import { AxiosError } from 'axios'; +import { ChartData } from 'chart.js'; +import Spinner from 'components/Spinner'; +import GridGraphComponent from 'container/GridGraphComponent'; +import getChartData from 'lib/getChartData'; +import GetMaxMinTime from 'lib/getMaxMinTime'; +import GetStartAndEndTime from 'lib/getStartAndEndTime'; +import React, { useCallback, useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { connect } from 'react-redux'; +import { bindActionCreators, Dispatch } from 'redux'; +import { ThunkDispatch } from 'redux-thunk'; +import { GlobalTime } from 'store/actions'; +import { + DeleteWidget, + DeleteWidgetProps, +} from 'store/actions/dashboard/deleteWidget'; +import { AppState } from 'store/reducers'; +import AppActions from 'types/actions'; +import { Widgets } from 'types/api/dashboard/getAll'; + +import Bar from './Bar'; +import FullView from './FullView'; +import { Modal } from './styles'; + +const GridCardGraph = ({ + widget, + deleteWidget, + isDeleted, +}: GridCardGraphProps): JSX.Element => { + const [state, setState] = useState({ + loading: true, + errorMessage: '', + error: false, + payload: undefined, + }); + const [modal, setModal] = useState(false); + const { minTime, maxTime } = useSelector( + (state) => state.globalTime, + ); + const [deleteModal, setDeletModal] = useState(false); + + useEffect(() => { + (async (): Promise => { + try { + const getMaxMinTime = GetMaxMinTime({ + graphType: widget.panelTypes, + maxTime, + minTime, + }); + + const { start, end } = GetStartAndEndTime({ + type: widget.timePreferance, + maxTime: getMaxMinTime.maxTime, + minTime: getMaxMinTime.minTime, + }); + + const response = await Promise.all( + widget.query + .filter((e) => e.query.length !== 0) + .map(async (query) => { + const result = await getQueryResult({ + end, + query: query.query, + start: start, + step: '30', + }); + + return { + query: query.query, + queryData: result, + legend: query.legend, + }; + }), + ); + + const isError = response.find((e) => e.queryData.statusCode !== 200); + + if (isError !== undefined) { + setState((state) => ({ + ...state, + error: true, + errorMessage: isError.queryData.error || 'Something went wrong', + loading: false, + })); + } else { + const chartDataSet = getChartData({ + queryData: { + data: response.map((e) => ({ + query: e.query, + legend: e.legend, + queryData: e.queryData.payload?.result || [], + })), + error: false, + errorMessage: '', + loading: false, + }, + }); + + setState((state) => ({ + ...state, + loading: false, + payload: chartDataSet, + })); + } + } catch (error) { + setState((state) => ({ + ...state, + error: true, + errorMessage: (error as AxiosError).toString(), + loading: false, + })); + } + })(); + }, [widget, maxTime, minTime]); + + const onToggleModal = useCallback( + (func: React.Dispatch>) => { + func((value) => !value); + }, + [], + ); + + const onDeleteHandler = useCallback(() => { + deleteWidget({ widgetId: widget.id }); + onToggleModal(setDeletModal); + isDeleted.current = true; + }, [deleteWidget, widget, onToggleModal, isDeleted]); + + if (state.error) { + return
{state.errorMessage}
; + } + + if (state.loading === true || state.payload === undefined) { + return ; + } + + return ( + <> + onToggleModal(setModal)} + widget={widget} + onDeleteHandler={(): void => onToggleModal(setDeletModal)} + /> + + onToggleModal(setDeletModal)} + visible={deleteModal} + title="Delete" + height="10vh" + onOk={onDeleteHandler} + centered + > + Are you sure you want to delete this widget + + + onToggleModal(setModal)} + width="85%" + destroyOnClose + > + + + + + + ); +}; + +interface GridCardGraphState { + loading: boolean; + error: boolean; + errorMessage: string; + payload: ChartData | undefined; +} + +interface DispatchProps { + deleteWidget: ({ + widgetId, + }: DeleteWidgetProps) => (dispatch: Dispatch) => void; +} + +interface GridCardGraphProps extends DispatchProps { + widget: Widgets; + isDeleted: React.MutableRefObject; +} + +const mapDispatchToProps = ( + dispatch: ThunkDispatch, +): DispatchProps => ({ + deleteWidget: bindActionCreators(DeleteWidget, dispatch), +}); + +export default connect(null, mapDispatchToProps)(GridCardGraph); diff --git a/frontend/src/container/GridGraphLayout/Graph/styles.ts b/frontend/src/container/GridGraphLayout/Graph/styles.ts new file mode 100644 index 0000000000..156b7553d7 --- /dev/null +++ b/frontend/src/container/GridGraphLayout/Graph/styles.ts @@ -0,0 +1,13 @@ +import { Modal as ModalComponent } from 'antd'; +import styled from 'styled-components'; + +interface Props { + height?: string; +} + +export const Modal = styled(ModalComponent)` + .ant-modal-content, + .ant-modal-body { + min-height: ${({ height = '80vh' }): string => height}; + } +`; diff --git a/frontend/src/container/GridGraphLayout/index.tsx b/frontend/src/container/GridGraphLayout/index.tsx new file mode 100644 index 0000000000..1d069bf835 --- /dev/null +++ b/frontend/src/container/GridGraphLayout/index.tsx @@ -0,0 +1,130 @@ +import Spinner from 'components/Spinner'; +import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; +import React, { memo, useCallback, useEffect, useRef, useState } from 'react'; +import { Layout } from 'react-grid-layout'; +import { useSelector } from 'react-redux'; +import { useHistory, useLocation } from 'react-router-dom'; +import { AppState } from 'store/reducers'; +import DashboardReducer from 'types/reducer/dashboards'; +import { v4 } from 'uuid'; + +import AddWidget from './AddWidget'; +import Graph from './Graph'; +import { Card, CardContainer, ReactGridLayout } from './styles'; + +const GridGraph = (): JSX.Element => { + const { push } = useHistory(); + const { pathname } = useLocation(); + + const { dashboards, loading } = useSelector( + (state) => state.dashboards, + ); + + const [selectedDashboard] = dashboards; + const { data } = selectedDashboard; + const { widgets } = data; + + const [layouts, setLayout] = useState([]); + + const AddWidgetWrapper = useCallback(() => , []); + + const isMounted = useRef(true); + const isDeleted = useRef(false); + + useEffect(() => { + if ( + loading === false && + (isMounted.current === true || isDeleted.current === true) + ) { + const getPreLayouts = (): LayoutProps[] => { + if (widgets === undefined) { + return []; + } + + return widgets.map((e, index) => { + return { + h: 2, + w: 6, + y: Infinity, + i: (index + 1).toString(), + x: (index % 2) * 6, + // eslint-disable-next-line react/display-name + Component: (): JSX.Element => ( + + ), + }; + }); + }; + + const preLayouts = getPreLayouts(); + + setLayout(() => [ + ...preLayouts, + { + i: (preLayouts.length + 1).toString(), + x: (preLayouts.length % 2) * 6, + y: Infinity, + w: 6, + h: 2, + Component: AddWidgetWrapper, + }, + ]); + } + + return (): void => { + isMounted.current = false; + }; + }, [widgets, layouts.length, AddWidgetWrapper, loading]); + + const onDropHandler = useCallback( + (allLayouts: Layout[], currectLayout: Layout, event: DragEvent) => { + event.preventDefault(); + if (event.dataTransfer) { + const graphType = event.dataTransfer.getData('text') as GRAPH_TYPES; + const generateWidgetId = v4(); + push(`${pathname}/new?graphType=${graphType}&widgetId=${generateWidgetId}`); + } + }, + [pathname, push], + ); + + if (layouts.length === 0) { + return ; + } + + return ( + + {layouts.map(({ Component, ...rest }, index) => { + const widget = (widgets || [])[index] || {}; + + const type = widget.panelTypes; + + const isQueryType = type === 'VALUE'; + + return ( + + + + + + ); + })} + + ); +}; + +interface LayoutProps extends Layout { + Component: () => JSX.Element; +} + +export default memo(GridGraph); diff --git a/frontend/src/container/GridGraphLayout/styles.ts b/frontend/src/container/GridGraphLayout/styles.ts new file mode 100644 index 0000000000..eac707db79 --- /dev/null +++ b/frontend/src/container/GridGraphLayout/styles.ts @@ -0,0 +1,42 @@ +import { Card as CardComponent } from 'antd'; +import RGL, { WidthProvider } from 'react-grid-layout'; +import styled from 'styled-components'; + +const ReactGridLayoutComponent = WidthProvider(RGL); + +interface Props { + isQueryType: boolean; +} +export const Card = styled(CardComponent)` + &&& { + height: 100%; + } + + .ant-card-body { + height: 100%; + padding: 0; + } +`; + +export const CardContainer = styled.div` + .react-resizable-handle { + position: absolute; + width: 20px; + height: 20px; + bottom: 0; + right: 0; + background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/Pg08IS0tIEdlbmVyYXRvcjogQWRvYmUgRmlyZXdvcmtzIENTNiwgRXhwb3J0IFNWRyBFeHRlbnNpb24gYnkgQWFyb24gQmVhbGwgKGh0dHA6Ly9maXJld29ya3MuYWJlYWxsLmNvbSkgLiBWZXJzaW9uOiAwLjYuMSAgLS0+DTwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DTxzdmcgaWQ9IlVudGl0bGVkLVBhZ2UlMjAxIiB2aWV3Qm94PSIwIDAgNiA2IiBzdHlsZT0iYmFja2dyb3VuZC1jb2xvcjojZmZmZmZmMDAiIHZlcnNpb249IjEuMSINCXhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHhtbDpzcGFjZT0icHJlc2VydmUiDQl4PSIwcHgiIHk9IjBweCIgd2lkdGg9IjZweCIgaGVpZ2h0PSI2cHgiDT4NCTxnIG9wYWNpdHk9IjAuMzAyIj4NCQk8cGF0aCBkPSJNIDYgNiBMIDAgNiBMIDAgNC4yIEwgNCA0LjIgTCA0LjIgNC4yIEwgNC4yIDAgTCA2IDAgTCA2IDYgTCA2IDYgWiIgZmlsbD0iIzAwMDAwMCIvPg0JPC9nPg08L3N2Zz4='); + background-position: bottom right; + padding: 0 3px 3px 0; + background-repeat: no-repeat; + background-origin: content-box; + box-sizing: border-box; + cursor: se-resize; + } +`; + +export const ReactGridLayout = styled(ReactGridLayoutComponent)` + border: 1px solid #434343; + margin-top: 1rem; + position: relative; +`; diff --git a/frontend/src/container/ListOfDashboard/TableComponents/CreatedBy.tsx b/frontend/src/container/ListOfDashboard/TableComponents/CreatedBy.tsx new file mode 100644 index 0000000000..9416261f39 --- /dev/null +++ b/frontend/src/container/ListOfDashboard/TableComponents/CreatedBy.tsx @@ -0,0 +1,17 @@ +import { Typography } from 'antd'; +import convertDateToAmAndPm from 'lib/convertDateToAmAndPm'; +import React from 'react'; + +import { Data } from '..'; + +const Created = (createdBy: Data['createdBy']): JSX.Element => { + const time = new Date(createdBy); + + return ( + {`${time.toLocaleDateString()} ${convertDateToAmAndPm( + time, + )}`} + ); +}; + +export default Created; diff --git a/frontend/src/container/ListOfDashboard/TableComponents/Date.tsx b/frontend/src/container/ListOfDashboard/TableComponents/Date.tsx new file mode 100644 index 0000000000..ef0bce03c9 --- /dev/null +++ b/frontend/src/container/ListOfDashboard/TableComponents/Date.tsx @@ -0,0 +1,14 @@ +import { Typography } from 'antd'; +import React from 'react'; + +import { Data } from '..'; + +const DateComponent = ( + lastUpdatedTime: Data['lastUpdatedTime'], +): JSX.Element => { + const date = new Date(lastUpdatedTime).toDateString(); + + return {date}; +}; + +export default DateComponent; diff --git a/frontend/src/container/ListOfDashboard/TableComponents/DeleteButton.tsx b/frontend/src/container/ListOfDashboard/TableComponents/DeleteButton.tsx new file mode 100644 index 0000000000..726ccf0253 --- /dev/null +++ b/frontend/src/container/ListOfDashboard/TableComponents/DeleteButton.tsx @@ -0,0 +1,56 @@ +import { Button } from 'antd'; +import React, { useCallback } from 'react'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import { Dispatch } from 'redux'; +import { ThunkDispatch } from 'redux-thunk'; +import { DeleteDashboard, DeleteDashboardProps } from 'store/actions'; +import AppActions from 'types/actions'; + +import { Data } from '../index'; + +const DeleteButton = ({ + deleteDashboard, + id, +}: DeleteButtonProps): JSX.Element => { + const onClickHandler = useCallback(() => { + deleteDashboard({ + uuid: id, + }); + }, [id, deleteDashboard]); + + return ( + + ); +}; + +interface DispatchProps { + deleteDashboard: ({ + uuid, + }: DeleteDashboardProps) => (dispatch: Dispatch) => void; +} + +const mapDispatchToProps = ( + dispatch: ThunkDispatch, +): DispatchProps => ({ + deleteDashboard: bindActionCreators(DeleteDashboard, dispatch), +}); + +type DeleteButtonProps = Data & DispatchProps; + +const WrapperDeleteButton = connect(null, mapDispatchToProps)(DeleteButton); + +// This is to avoid the type collision +const Wrapper = (props: Data): JSX.Element => { + return ( + + ); +}; + +export default Wrapper; diff --git a/frontend/src/container/ListOfDashboard/TableComponents/Name.tsx b/frontend/src/container/ListOfDashboard/TableComponents/Name.tsx new file mode 100644 index 0000000000..80787ff18e --- /dev/null +++ b/frontend/src/container/ListOfDashboard/TableComponents/Name.tsx @@ -0,0 +1,23 @@ +import { Button } from 'antd'; +import ROUTES from 'constants/routes'; +import updateUrl from 'lib/updateUrl'; +import React, { useCallback } from 'react'; +import { useHistory } from 'react-router-dom'; + +import { Data } from '..'; + +const Name = (name: Data['name'], data: Data): JSX.Element => { + const { push } = useHistory(); + + const onClickHandler = useCallback(() => { + push(updateUrl(ROUTES.DASHBOARD, ':dashboardId', data.id)); + }, []); + + return ( + + ); +}; + +export default Name; diff --git a/frontend/src/container/ListOfDashboard/TableComponents/Tags.tsx b/frontend/src/container/ListOfDashboard/TableComponents/Tags.tsx new file mode 100644 index 0000000000..7831409a7c --- /dev/null +++ b/frontend/src/container/ListOfDashboard/TableComponents/Tags.tsx @@ -0,0 +1,16 @@ +import { Tag } from 'antd'; +import React from 'react'; + +import { Data } from '../index'; + +const Tags = (props: Data['tags']): JSX.Element => { + return ( + <> + {props.map((e) => ( + {e} + ))} + + ); +}; + +export default Tags; diff --git a/frontend/src/container/ListOfDashboard/index.tsx b/frontend/src/container/ListOfDashboard/index.tsx new file mode 100644 index 0000000000..2961566e5c --- /dev/null +++ b/frontend/src/container/ListOfDashboard/index.tsx @@ -0,0 +1,171 @@ +import { PlusOutlined } from '@ant-design/icons'; +import { Row, Table, TableColumnProps, Typography } from 'antd'; +import createDashboard from 'api/dashboard/create'; +import { AxiosError } from 'axios'; +import ROUTES from 'constants/routes'; +import updateUrl from 'lib/updateUrl'; +import React, { useCallback, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { useHistory } from 'react-router-dom'; +import { AppState } from 'store/reducers'; +import DashboardReducer from 'types/reducer/dashboards'; +import { v4 } from 'uuid'; + +import { NewDashboardButton, TableContainer } from './styles'; +import Createdby from './TableComponents/CreatedBy'; +import DateComponent from './TableComponents/Date'; +import DeleteButton from './TableComponents/DeleteButton'; +import Name from './TableComponents/Name'; +import Tags from './TableComponents/Tags'; + +const ListOfAllDashboard = (): JSX.Element => { + const { dashboards } = useSelector( + (state) => state.dashboards, + ); + + const [newDashboardState, setNewDashboardState] = useState({ + loading: false, + error: false, + errorMessage: '', + }); + + const { push } = useHistory(); + + const columns: TableColumnProps[] = [ + { + title: 'Name', + dataIndex: 'name', + render: Name, + }, + { + title: 'Description', + dataIndex: 'description', + }, + { + title: 'Tags (can be multiple)', + dataIndex: 'tags', + render: Tags, + }, + { + title: 'Created By', + dataIndex: 'createdBy', + render: Createdby, + }, + { + title: 'Last Updated Time', + dataIndex: 'lastUpdatedTime', + sorter: (a: Data, b: Data): number => { + return parseInt(a.lastUpdatedTime, 10) - parseInt(b.lastUpdatedTime, 10); + }, + render: DateComponent, + }, + { + title: 'Action', + dataIndex: '', + key: 'x', + render: DeleteButton, + }, + ]; + + const data: Data[] = dashboards.map((e) => ({ + createdBy: e.created_at, + description: e.data.description || '', + id: e.uuid, + lastUpdatedTime: e.updated_at, + name: e.data.title, + tags: e.data.tags || [], + key: e.uuid, + })); + + const onNewDashboardHandler = useCallback(async () => { + try { + const newDashboardId = v4(); + setNewDashboardState({ + ...newDashboardState, + loading: true, + }); + const response = await createDashboard({ + uuid: newDashboardId, + title: 'Sample Title', + }); + + if (response.statusCode === 200) { + setNewDashboardState({ + ...newDashboardState, + loading: false, + }); + push(updateUrl(ROUTES.DASHBOARD, ':dashboardId', newDashboardId)); + } else { + setNewDashboardState({ + ...newDashboardState, + loading: false, + error: true, + errorMessage: response.error || 'Something went wrong', + }); + } + } catch (error) { + setNewDashboardState({ + ...newDashboardState, + error: true, + errorMessage: (error as AxiosError).toString() || 'Something went Wrong', + }); + } + }, [newDashboardState, push]); + + const getText = (): string => { + if (!newDashboardState.error && !newDashboardState.loading) { + return 'New Dashboard'; + } + + if (newDashboardState.loading) { + return 'Loading'; + } + + return newDashboardState.errorMessage; + }; + + return ( + + { + return ( + + Dashboard List + } + type="primary" + loading={newDashboardState.loading} + danger={newDashboardState.error} + > + {getText()} + + + ); + }} + columns={columns} + dataSource={data} + showSorterTooltip + /> + + ); +}; + +export interface Data { + key: React.Key; + name: string; + description: string; + tags: string[]; + createdBy: string; + lastUpdatedTime: string; + id: string; +} + +export default ListOfAllDashboard; diff --git a/frontend/src/container/ListOfDashboard/styles.ts b/frontend/src/container/ListOfDashboard/styles.ts new file mode 100644 index 0000000000..7ba5d0975f --- /dev/null +++ b/frontend/src/container/ListOfDashboard/styles.ts @@ -0,0 +1,16 @@ +import { Button, Row } from 'antd'; +import styled from 'styled-components'; + +export const NewDashboardButton = styled(Button)` + &&& { + display: flex; + justify-content: center; + align-items: center; + } +`; + +export const TableContainer = styled(Row)` + &&& { + margin-top: 1rem; + } +`; diff --git a/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx b/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx new file mode 100644 index 0000000000..078851260e --- /dev/null +++ b/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx @@ -0,0 +1,46 @@ +import React, { useCallback } from 'react'; +import { useHistory, useLocation } from 'react-router'; +import { v4 } from 'uuid'; + +import menuItems, { ITEMS } from './menuItems'; +import { Card, Container, Text } from './styles'; + +const DashboardGraphSlider = (): JSX.Element => { + const onDragStartHandler: React.DragEventHandler = useCallback( + (event: React.DragEvent) => { + event.dataTransfer.setData('text/plain', event.currentTarget.id); + }, + [], + ); + const { push } = useHistory(); + const { pathname } = useLocation(); + + const onClickHandler = useCallback( + (name: ITEMS) => { + const generateWidgetId = v4(); + push(`${pathname}/new?graphType=${name}&widgetId=${generateWidgetId}`); + }, + [push, pathname], + ); + + return ( + + {menuItems.map(({ name, Icon, display }) => ( + onClickHandler(name)} + id={name} + onDragStart={onDragStartHandler} + key={name} + draggable + > + + {display} + + ))} + + ); +}; + +export type GRAPH_TYPES = ITEMS; + +export default DashboardGraphSlider; diff --git a/frontend/src/container/NewDashboard/ComponentsSlider/menuItems.ts b/frontend/src/container/NewDashboard/ComponentsSlider/menuItems.ts new file mode 100644 index 0000000000..ced78bb14d --- /dev/null +++ b/frontend/src/container/NewDashboard/ComponentsSlider/menuItems.ts @@ -0,0 +1,25 @@ +import TimeSeries from 'assets/Dashboard/TimeSeries'; +import ValueIcon from 'assets/Dashboard/Value'; + +const Items: ItemsProps[] = [ + { + name: 'TIME_SERIES', + Icon: TimeSeries, + display: 'Time Series', + }, + { + name: 'VALUE', + Icon: ValueIcon, + display: 'Value', + }, +]; + +export type ITEMS = 'TIME_SERIES' | 'VALUE'; + +interface ItemsProps { + name: ITEMS; + Icon: () => JSX.Element; + display: string; +} + +export default Items; diff --git a/frontend/src/container/NewDashboard/ComponentsSlider/styles.ts b/frontend/src/container/NewDashboard/ComponentsSlider/styles.ts new file mode 100644 index 0000000000..ad59f774a8 --- /dev/null +++ b/frontend/src/container/NewDashboard/ComponentsSlider/styles.ts @@ -0,0 +1,25 @@ +import { Card as CardComponent, Typography } from 'antd'; +import styled from 'styled-components'; + +export const Container = styled.div` + display: flex; + gap: 0.6rem; +`; + +export const Card = styled(CardComponent)` + min-height: 10vh; + overflow-y: auto; + cursor: pointer; + + .ant-card-body { + height: 100%; + display: flex; + flex-direction: column; + justify-content: space-between; + } +`; + +export const Text = styled(Typography)` + text-align: center; + margin-top: 1rem; +`; diff --git a/frontend/src/container/NewDashboard/DescriptionOfDashboard/AddTags/index.tsx b/frontend/src/container/NewDashboard/DescriptionOfDashboard/AddTags/index.tsx new file mode 100644 index 0000000000..b16d15e7be --- /dev/null +++ b/frontend/src/container/NewDashboard/DescriptionOfDashboard/AddTags/index.tsx @@ -0,0 +1,120 @@ +import { PlusOutlined } from '@ant-design/icons'; +import { Col, Tooltip, Typography } from 'antd'; +import Input from 'components/Input'; +import React, { useState } from 'react'; + +import { InputContainer, NewTagContainer, TagsContainer } from './styles'; + +const AddTags = ({ tags, setTags }: AddTagsProps): JSX.Element => { + const [inputValue, setInputValue] = useState(''); + const [inputVisible, setInputVisible] = useState(false); + const [editInputIndex, setEditInputIndex] = useState(-1); + const [editInputValue, setEditInputValue] = useState(''); + + const handleInputConfirm = (): void => { + if (inputValue) { + setTags([...tags, inputValue]); + } + setInputVisible(false); + setInputValue(''); + }; + + const handleEditInputConfirm = (): void => { + const newTags = [...tags]; + newTags[editInputIndex] = editInputValue; + setTags(newTags); + setEditInputIndex(-1); + setInputValue(''); + }; + + const handleClose = (removedTag: string): void => { + const newTags = tags.filter((tag) => tag !== removedTag); + setTags(newTags); + }; + + const showInput = (): void => { + setInputVisible(true); + }; + + const onChangeHandler = ( + value: string, + func: React.Dispatch>, + ): void => { + func(value); + }; + + return ( + + {tags.map((tag, index) => { + if (editInputIndex === index) { + return ( + + + onChangeHandler(event.target.value, setEditInputValue) + } + onBlurHandler={handleEditInputConfirm} + onPressEnterHandler={handleEditInputConfirm} + /> + + ); + } + + const isLongTag = tag.length > 20; + + const tagElem = ( + handleClose(tag)}> + { + setEditInputIndex(index); + setEditInputValue(tag); + e.preventDefault(); + }} + > + {isLongTag ? `${tag.slice(0, 20)}...` : tag} + + + ); + + return isLongTag ? ( + + {tagElem} + + ) : ( + tagElem + ); + })} + + {inputVisible && ( + + + onChangeHandler(event.target.value, setInputValue) + } + onBlurHandler={handleInputConfirm} + onPressEnterHandler={handleInputConfirm} + /> + + )} + + {!inputVisible && ( + } onClick={showInput}> + New Tag + + )} + + ); +}; + +interface AddTagsProps { + tags: string[]; + setTags: React.Dispatch>; +} + +export default AddTags; diff --git a/frontend/src/container/NewDashboard/DescriptionOfDashboard/AddTags/styles.ts b/frontend/src/container/NewDashboard/DescriptionOfDashboard/AddTags/styles.ts new file mode 100644 index 0000000000..f2daefe65e --- /dev/null +++ b/frontend/src/container/NewDashboard/DescriptionOfDashboard/AddTags/styles.ts @@ -0,0 +1,26 @@ +import { Col, Tag } from 'antd'; +import styled from 'styled-components'; + +export const TagsContainer = styled.div` + display: flex; + align-items: center; +`; + +export const NewTagContainer = styled(Tag)` + &&& { + display: flex; + justify-content: space-between; + align-items: center; + height: 100%; + + svg { + margin-right: 0.2rem; + } + } +`; + +export const InputContainer = styled(Col)` + > div { + margin: 0; + } +`; diff --git a/frontend/src/container/NewDashboard/DescriptionOfDashboard/Description/index.tsx b/frontend/src/container/NewDashboard/DescriptionOfDashboard/Description/index.tsx new file mode 100644 index 0000000000..b368de2403 --- /dev/null +++ b/frontend/src/container/NewDashboard/DescriptionOfDashboard/Description/index.tsx @@ -0,0 +1,34 @@ +import { Input } from 'antd'; +import React, { useCallback } from 'react'; + +import { Container } from './styles'; +const { TextArea } = Input; + +const Description = ({ + description, + setDescription, +}: DescriptionProps): JSX.Element => { + const onChangeHandler = useCallback( + (e: React.ChangeEvent) => { + setDescription(e.target.value); + }, + [setDescription], + ); + + return ( + + + + ); +}; + +interface DescriptionProps { + description: string; + setDescription: React.Dispatch>; +} + +export default Description; diff --git a/frontend/src/container/NewDashboard/DescriptionOfDashboard/Description/styles.ts b/frontend/src/container/NewDashboard/DescriptionOfDashboard/Description/styles.ts new file mode 100644 index 0000000000..67fb0b6692 --- /dev/null +++ b/frontend/src/container/NewDashboard/DescriptionOfDashboard/Description/styles.ts @@ -0,0 +1,5 @@ +import styled from 'styled-components'; + +export const Container = styled.div` + margin-top: 1rem; +`; diff --git a/frontend/src/container/NewDashboard/DescriptionOfDashboard/NameOfTheDashboard/index.tsx b/frontend/src/container/NewDashboard/DescriptionOfDashboard/NameOfTheDashboard/index.tsx new file mode 100644 index 0000000000..528028bffb --- /dev/null +++ b/frontend/src/container/NewDashboard/DescriptionOfDashboard/NameOfTheDashboard/index.tsx @@ -0,0 +1,30 @@ +import Input from 'components/Input'; +import React, { useCallback } from 'react'; + +const NameOfTheDashboard = ({ + setName, + name, +}: NameOfTheDashboardProps): JSX.Element => { + const onChangeHandler = useCallback( + (e: React.ChangeEvent) => { + setName(e.target.value); + }, + [setName], + ); + + return ( + + ); +}; + +interface NameOfTheDashboardProps { + name: string; + setName: React.Dispatch>; +} + +export default NameOfTheDashboard; diff --git a/frontend/src/container/NewDashboard/DescriptionOfDashboard/index.tsx b/frontend/src/container/NewDashboard/DescriptionOfDashboard/index.tsx new file mode 100644 index 0000000000..7d8d670ebc --- /dev/null +++ b/frontend/src/container/NewDashboard/DescriptionOfDashboard/index.tsx @@ -0,0 +1,122 @@ +import { EditOutlined, SaveOutlined } from '@ant-design/icons'; +import { Card, Col, Row, Tag, Typography } from 'antd'; +import AddTags from 'container/NewDashboard/DescriptionOfDashboard/AddTags'; +import NameOfTheDashboard from 'container/NewDashboard/DescriptionOfDashboard/NameOfTheDashboard'; +import React, { useCallback, useState } from 'react'; +import { connect, useSelector } from 'react-redux'; +import { bindActionCreators, Dispatch } from 'redux'; +import { ThunkDispatch } from 'redux-thunk'; +import { + ToggleEditMode, + UpdateDashboardTitleDescriptionTags, + UpdateDashboardTitleDescriptionTagsProps, +} from 'store/actions'; +import { AppState } from 'store/reducers'; +import AppActions from 'types/actions'; +import DashboardReducer from 'types/reducer/dashboards'; + +import Description from './Description'; +import { Button, Container } from './styles'; + +const DescriptionOfDashboard = ({ + updateDashboardTitleDescriptionTags, + toggleEditMode, +}: DescriptionOfDashboardProps): JSX.Element => { + const { dashboards, isEditMode } = useSelector( + (state) => state.dashboards, + ); + + const [selectedDashboard] = dashboards; + const selectedData = selectedDashboard.data; + const title = selectedData.title; + const tags = selectedData.tags; + const description = selectedData.description; + + const [updatedTitle, setUpdatedTitle] = useState(title); + const [updatedTags, setUpdatedTags] = useState(tags || []); + const [updatedDescription, setUpdtatedDescription] = useState( + description || '', + ); + + const onClickEditHandler = useCallback(() => { + if (isEditMode) { + const dashboard = selectedDashboard; + // @TODO need to update this function to take title,description,tags only + updateDashboardTitleDescriptionTags({ + dashboard: { + ...dashboard, + data: { + ...dashboard.data, + description: updatedDescription, + tags: updatedTags, + title: updatedTitle, + }, + }, + }); + } else { + toggleEditMode(); + } + }, [isEditMode, updatedTitle, updatedTags, updatedDescription]); + + return ( + <> + + + {!isEditMode ? ( + <> + + {title} + + {tags?.map((e) => ( + {e} + ))} + + + {description} + + + + ) : ( + + + + + + )} + + + + + + + ); +}; + +interface DispatchProps { + updateDashboardTitleDescriptionTags: ( + props: UpdateDashboardTitleDescriptionTagsProps, + ) => (dispatch: Dispatch) => void; + toggleEditMode: () => void; +} + +const mapDispatchToProps = ( + dispatch: ThunkDispatch, +): DispatchProps => ({ + updateDashboardTitleDescriptionTags: bindActionCreators( + UpdateDashboardTitleDescriptionTags, + dispatch, + ), + toggleEditMode: bindActionCreators(ToggleEditMode, dispatch), +}); + +type DescriptionOfDashboardProps = DispatchProps; + +export default connect(null, mapDispatchToProps)(DescriptionOfDashboard); diff --git a/frontend/src/container/NewDashboard/DescriptionOfDashboard/styles.ts b/frontend/src/container/NewDashboard/DescriptionOfDashboard/styles.ts new file mode 100644 index 0000000000..74794e163c --- /dev/null +++ b/frontend/src/container/NewDashboard/DescriptionOfDashboard/styles.ts @@ -0,0 +1,13 @@ +import { Button as ButtonComponent } from 'antd'; +import styled from 'styled-components'; + +export const Container = styled.div` + margin-top: 0.5rem; +`; + +export const Button = styled(ButtonComponent)` + &&& { + display: flex; + align-items: center; + } +`; diff --git a/frontend/src/container/NewDashboard/GridGraphs/index.tsx b/frontend/src/container/NewDashboard/GridGraphs/index.tsx new file mode 100644 index 0000000000..bc689e0948 --- /dev/null +++ b/frontend/src/container/NewDashboard/GridGraphs/index.tsx @@ -0,0 +1,26 @@ +import GridGraphLayout from 'container/GridGraphLayout'; +import ComponentsSlider from 'container/NewDashboard/ComponentsSlider'; +import React, { useCallback, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import DashboardReducer from 'types/reducer/dashboards'; + +import { GridComponentSliderContainer } from './styles'; + +const GridGraphs = (): JSX.Element => { + const { isAddWidget } = useSelector( + (state) => state.dashboards, + ); + + return ( + <> + + {isAddWidget && } + + + + + ); +}; + +export default GridGraphs; diff --git a/frontend/src/container/NewDashboard/GridGraphs/styles.ts b/frontend/src/container/NewDashboard/GridGraphs/styles.ts new file mode 100644 index 0000000000..1bc68c27b0 --- /dev/null +++ b/frontend/src/container/NewDashboard/GridGraphs/styles.ts @@ -0,0 +1,28 @@ +import { Card as CardComponent, Row } from 'antd'; +import styled from 'styled-components'; + +export const CardContainer = styled(Row)` + &&& { + margin-top: 1rem; + } +`; + +export const Card = styled(CardComponent)` + &&& { + cursor: pointer; + } + .ant-card-body { + display: flex; + width: 100%; + justify-content: center; + align-items: center; + + > span { + margin-right: 0.5rem; + } + } +`; + +export const GridComponentSliderContainer = styled.div` + margin-top: 1rem; +`; diff --git a/frontend/src/container/NewDashboard/index.tsx b/frontend/src/container/NewDashboard/index.tsx new file mode 100644 index 0000000000..edf2936414 --- /dev/null +++ b/frontend/src/container/NewDashboard/index.tsx @@ -0,0 +1,15 @@ +import React from 'react'; + +import Description from './DescriptionOfDashboard'; +import GridGraphs from './GridGraphs'; + +const NewDashboard = (): JSX.Element => { + return ( +
+ + +
+ ); +}; + +export default NewDashboard; diff --git a/frontend/src/container/NewWidget/LeftContainer/QuerySection/Query.tsx b/frontend/src/container/NewWidget/LeftContainer/QuerySection/Query.tsx new file mode 100644 index 0000000000..02729ab7c3 --- /dev/null +++ b/frontend/src/container/NewWidget/LeftContainer/QuerySection/Query.tsx @@ -0,0 +1,95 @@ +import { Divider } from 'antd'; +import Input from 'components/Input'; +import { timePreferance } from 'container/NewWidget/RightContainer/timeItems'; +import React, { useCallback, useState } from 'react'; +import { connect } from 'react-redux'; +import { useLocation } from 'react-router'; +import { bindActionCreators, Dispatch } from 'redux'; +import { ThunkDispatch } from 'redux-thunk'; +import { + UpdateQuery, + UpdateQueryProps, +} from 'store/actions/dashboard/updateQuery'; +import AppActions from 'types/actions'; + +import { Container, InputContainer } from './styles'; + +const Query = ({ + currentIndex, + preLegend, + preQuery, + updateQuery, +}: QueryProps): JSX.Element => { + const [promqlQuery, setPromqlQuery] = useState(preQuery); + const [legendFormat, setLegendFormat] = useState(preLegend); + const { search } = useLocation(); + + const query = new URLSearchParams(search); + const widgetId = query.get('widgetId') || ''; + + const onChangeHandler = useCallback( + (setFunc: React.Dispatch>, value: string) => { + setFunc(value); + }, + [], + ); + + const onBlurHandler = (): void => { + updateQuery({ + currentIndex, + legend: legendFormat, + query: promqlQuery, + widgetId, + }); + }; + + return ( + + + + onChangeHandler(setPromqlQuery, event.target.value) + } + size="middle" + value={promqlQuery} + addonBefore={'PromQL Query'} + onBlur={(): void => onBlurHandler()} + /> + + + + + onChangeHandler(setLegendFormat, event.target.value) + } + size="middle" + value={legendFormat} + addonBefore={'Legend Format'} + onBlur={(): void => onBlurHandler()} + /> + + + + ); +}; + +interface DispatchProps { + updateQuery: ( + props: UpdateQueryProps, + ) => (dispatch: Dispatch) => void; +} + +const mapDispatchToProps = ( + dispatch: ThunkDispatch, +): DispatchProps => ({ + updateQuery: bindActionCreators(UpdateQuery, dispatch), +}); + +interface QueryProps extends DispatchProps { + selectedTime: timePreferance; + currentIndex: number; + preQuery: string; + preLegend: string; +} + +export default connect(null, mapDispatchToProps)(Query); diff --git a/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx b/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx new file mode 100644 index 0000000000..3ca10d230e --- /dev/null +++ b/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx @@ -0,0 +1,89 @@ +import { PlusOutlined } from '@ant-design/icons'; +import Spinner from 'components/Spinner'; +import { timePreferance } from 'container/NewWidget/RightContainer/timeItems'; +import React, { useCallback, useMemo } from 'react'; +import { connect, useSelector } from 'react-redux'; +import { useLocation } from 'react-router'; +import { bindActionCreators, Dispatch } from 'redux'; +import { ThunkDispatch } from 'redux-thunk'; +import { CreateQuery, CreateQueryProps } from 'store/actions'; +import { AppState } from 'store/reducers'; +import AppActions from 'types/actions'; +import { Widgets } from 'types/api/dashboard/getAll'; +import DashboardReducer from 'types/reducer/dashboards'; + +import Query from './Query'; +import { QueryButton } from './styles'; + +const QuerySection = ({ + selectedTime, + createQuery, +}: QueryProps): JSX.Element => { + const { dashboards } = useSelector( + (state) => state.dashboards, + ); + const [selectedDashboards] = dashboards; + const { search } = useLocation(); + const widgets = selectedDashboards.data.widgets; + + const urlQuery = useMemo(() => { + return new URLSearchParams(search); + }, [search]); + + const getWidget = useCallback(() => { + const widgetId = urlQuery.get('widgetId'); + return widgets?.find((e) => e.id === widgetId); + }, [widgets, urlQuery]); + + const selectedWidget = getWidget() as Widgets; + + const { query = [] } = selectedWidget || {}; + + const queryOnClickHandler = useCallback(() => { + const widgetId = urlQuery.get('widgetId'); + + createQuery({ + widgetId: String(widgetId), + }); + }, [createQuery, urlQuery]); + + if (query.length === 0) { + return ; + } + + return ( +
+ {query.map((e, index) => ( + + ))} + + }> + Query + +
+ ); +}; + +interface DispatchProps { + createQuery: ({ + widgetId, + }: CreateQueryProps) => (dispatch: Dispatch) => void; +} + +const mapDispatchToProps = ( + dispatch: ThunkDispatch, +): DispatchProps => ({ + createQuery: bindActionCreators(CreateQuery, dispatch), +}); + +interface QueryProps extends DispatchProps { + selectedTime: timePreferance; +} + +export default connect(null, mapDispatchToProps)(QuerySection); diff --git a/frontend/src/container/NewWidget/LeftContainer/QuerySection/styles.ts b/frontend/src/container/NewWidget/LeftContainer/QuerySection/styles.ts new file mode 100644 index 0000000000..6e63558615 --- /dev/null +++ b/frontend/src/container/NewWidget/LeftContainer/QuerySection/styles.ts @@ -0,0 +1,17 @@ +import { Button } from 'antd'; +import styled from 'styled-components'; + +export const InputContainer = styled.div` + width: 50%; +`; + +export const Container = styled.div` + margin-top: 1rem; +`; + +export const QueryButton = styled(Button)` + &&& { + display: flex; + align-items: center; + } +`; diff --git a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraph.tsx b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraph.tsx new file mode 100644 index 0000000000..dad79914b3 --- /dev/null +++ b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraph.tsx @@ -0,0 +1,59 @@ +import { Card, Typography } from 'antd'; +import GridGraphComponent from 'container/GridGraphComponent'; +import { NewWidgetProps } from 'container/NewWidget'; +import getChartData from 'lib/getChartData'; +import React from 'react'; +import { useSelector } from 'react-redux'; +import { useLocation } from 'react-router'; +import { AppState } from 'store/reducers'; +import DashboardReducer from 'types/reducer/dashboards'; + +import { NotFoundContainer } from './styles'; + +const WidgetGraph = ({ selectedGraph }: WidgetGraphProps): JSX.Element => { + const { dashboards } = useSelector( + (state) => state.dashboards, + ); + + const [selectedDashboard] = dashboards; + const { data } = selectedDashboard; + const { widgets = [] } = data; + const { search } = useLocation(); + + const params = new URLSearchParams(search); + const widgetId = params.get('widgetId'); + + const selectedWidget = widgets.find((e) => e.id === widgetId); + + if (selectedWidget === undefined) { + return Invalid widget; + } + + const { queryData, title, opacity, isStacked } = selectedWidget; + + if (queryData.data.length === 0) { + return ( + + No Data + + ); + } + + const chartDataSet = getChartData({ + queryData, + }); + + return ( + + ); +}; + +type WidgetGraphProps = NewWidgetProps; + +export default WidgetGraph; diff --git a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/index.tsx b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/index.tsx new file mode 100644 index 0000000000..d844862875 --- /dev/null +++ b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/index.tsx @@ -0,0 +1,57 @@ +import { InfoCircleOutlined } from '@ant-design/icons'; +import { Typography } from 'antd'; +import { Card } from 'container/GridGraphLayout/styles'; +import React, { memo } from 'react'; +import { useSelector } from 'react-redux'; +import { useLocation } from 'react-router-dom'; +import { AppState } from 'store/reducers'; +import DashboardReducer from 'types/reducer/dashboards'; + +import { NewWidgetProps } from '../../index'; +import { AlertIconContainer, Container, NotFoundContainer } from './styles'; +import WidgetGraphComponent from './WidgetGraph'; + +const WidgetGraph = ({ selectedGraph }: WidgetGraphProps): JSX.Element => { + const { dashboards, isQueryFired } = useSelector( + (state) => state.dashboards, + ); + const [selectedDashboard] = dashboards; + const { search } = useLocation(); + + const { data } = selectedDashboard; + + const { widgets = [] } = data; + + const params = new URLSearchParams(search); + const widgetId = params.get('widgetId'); + + const selectedWidget = widgets.find((e) => e.id === widgetId); + + if (selectedWidget === undefined) { + return Invalid widget; + } + + const { queryData } = selectedWidget; + + return ( + + {queryData.error && ( + + + + )} + + {!isQueryFired && ( + + No Data + + )} + + {isQueryFired && } + + ); +}; + +type WidgetGraphProps = NewWidgetProps; + +export default memo(WidgetGraph); diff --git a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/styles.ts b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/styles.ts new file mode 100644 index 0000000000..b376f0d247 --- /dev/null +++ b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/styles.ts @@ -0,0 +1,27 @@ +import { Card, Tooltip } from 'antd'; +import styled from 'styled-components'; + +export const Container = styled(Card)` + &&& { + position: relative; + } + + .ant-card-body { + padding: 0; + height: 55vh; + /* padding-bottom: 2rem; */ + } +`; + +export const AlertIconContainer = styled(Tooltip)` + position: absolute; + top: 10px; + left: 10px; +`; + +export const NotFoundContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; + min-height: 55vh; +`; diff --git a/frontend/src/container/NewWidget/LeftContainer/index.tsx b/frontend/src/container/NewWidget/LeftContainer/index.tsx new file mode 100644 index 0000000000..58b2c8f3ab --- /dev/null +++ b/frontend/src/container/NewWidget/LeftContainer/index.tsx @@ -0,0 +1,28 @@ +import React, { memo } from 'react'; + +import { NewWidgetProps } from '../index'; +import { timePreferance } from '../RightContainer/timeItems'; +import QuerySection from './QuerySection'; +import { QueryContainer } from './styles'; +import WidgetGraph from './WidgetGraph'; + +const LeftContainer = ({ + selectedGraph, + selectedTime, +}: LeftContainerProps): JSX.Element => { + return ( + <> + + + + + + + ); +}; + +interface LeftContainerProps extends NewWidgetProps { + selectedTime: timePreferance; +} + +export default memo(LeftContainer); diff --git a/frontend/src/container/NewWidget/LeftContainer/styles.ts b/frontend/src/container/NewWidget/LeftContainer/styles.ts new file mode 100644 index 0000000000..4d1988a4e1 --- /dev/null +++ b/frontend/src/container/NewWidget/LeftContainer/styles.ts @@ -0,0 +1,9 @@ +import { Card } from 'antd'; +import styled from 'styled-components'; + +export const QueryContainer = styled(Card)` + &&& { + margin-top: 1rem; + min-height: 23.5%; + } +`; diff --git a/frontend/src/container/NewWidget/RightContainer/index.tsx b/frontend/src/container/NewWidget/RightContainer/index.tsx new file mode 100644 index 0000000000..651725459e --- /dev/null +++ b/frontend/src/container/NewWidget/RightContainer/index.tsx @@ -0,0 +1,157 @@ +import { Button, Input, Slider, Switch, Typography } from 'antd'; +import InputComponent from 'components/Input'; +import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; +import GraphTypes from 'container/NewDashboard/ComponentsSlider/menuItems'; +import React, { useCallback } from 'react'; + +import { timePreferance } from './timeItems'; + +const { TextArea } = Input; +import TimePreference from 'components/TimePreferenceDropDown'; + +import { Container, NullButtonContainer, TextContainer, Title } from './styles'; + +const RightContainer = ({ + description, + opacity, + selectedNullZeroValue, + setDescription, + setOpacity, + setSelectedNullZeroValue, + setStacked, + setTitle, + stacked, + title, + selectedGraph, + setSelectedTime, + selectedTime, +}: RightContainerProps): JSX.Element => { + const onChangeHandler = useCallback( + (setFunc: React.Dispatch>, value: string) => { + setFunc(value); + }, + [], + ); + + const nullValueButtons = [ + { + check: 'zero', + name: 'Zero', + }, + { + check: 'interpolate', + name: 'Interpolate', + }, + { + check: 'blank', + name: 'Blank', + }, + ]; + + const selectedGraphType = + GraphTypes.find((e) => e.name === selectedGraph)?.display || ''; + + return ( + + + + Panel Attributes + + + onChangeHandler(setTitle, event.target.value) + } + value={title} + /> + + Description + +