From b039dc6fa7537bc7ea0c34d5e12b264128927078 Mon Sep 17 00:00:00 2001 From: Yunus M Date: Sun, 16 Mar 2025 19:41:08 +0530 Subject: [PATCH] feat: in product home page (#7270) * feat: base setup for in product home page * feat: base state * feat: add empty states for alerts, traces, dashboards, saved views * feat: add checklist component * feat: integrate all panels * feat: integrate preference api and clean up components * feat: handle done and skip states of the checklist * feat: update ui * feat: update ui * feat: code cleanup * feat: add events * feat: support time interval change in services * feat: add service time change event and cleanup code * feat: handle light mode * feat: address review comments * fix: routing issues * fix: testcase snapshot, a minor ui improvements * fix: noopener typo in window.open --- frontend/package.json | 2 + frontend/public/Icons/beacon.svg | 34 + frontend/public/Icons/circus-tent.svg | 26 + frontend/public/Icons/container-plus.svg | 44 + frontend/public/Icons/cracker.svg | 52 + frontend/public/Icons/dashboard.svg | 138 ++ frontend/public/Icons/dials.svg | 79 ++ frontend/public/Icons/eight-ball.svg | 8 + frontend/public/Icons/floppy-disc.svg | 7 + frontend/public/Icons/hello-wave.svg | 15 + frontend/public/Icons/hurray.svg | 104 ++ frontend/public/Icons/logs.svg | 8 + frontend/public/Icons/play-back.svg | 4 + frontend/public/Icons/spinner-half-blue.svg | 12 + frontend/public/Icons/spinner.svg | 10 + .../public/Icons/status-inprogress-pill.svg | 4 + frontend/public/Icons/status-skipped-pill.svg | 5 + frontend/public/Icons/triangle-ruler.svg | 78 ++ frontend/public/Icons/wrench.svg | 60 + frontend/public/Images/activeDot.svg | 17 + frontend/public/Images/allInOne.svg | 891 +++++++++++++ frontend/public/Images/dotted-divider.svg | 458 +++++++ frontend/public/Images/perilianBackground.svg | 1002 ++++++++++++++ frontend/public/locales/en-GB/titles.json | 1 + frontend/public/locales/en/titles.json | 1 + frontend/src/AppRoutes/Private.tsx | 4 +- frontend/src/AppRoutes/index.tsx | 3 - frontend/src/AppRoutes/pageComponents.ts | 4 + frontend/src/AppRoutes/routes.ts | 8 + .../api/preferences/updateUserPreference.ts | 9 +- .../src/components/Header/Header.styles.scss | 29 + frontend/src/components/Header/Header.tsx | 17 + .../__snapshots__/NotFound.test.tsx.snap | 4 +- frontend/src/components/NotFound/index.tsx | 4 +- frontend/src/constants/routes.ts | 3 +- .../container/AppLayout/AppLayout.styles.scss | 6 +- frontend/src/container/AppLayout/index.tsx | 10 +- .../container/Home/AlertRules/AlertRules.tsx | 232 ++++ .../container/Home/Dashboards/Dashboards.tsx | 216 +++ .../Home/DataSourceInfo/DataSourceInfo.tsx | 184 +++ frontend/src/container/Home/Home.styles.scss | 1059 +++++++++++++++ frontend/src/container/Home/Home.tsx | 721 ++++++++++ .../HomeChecklist/HomeChecklist.styles.scss | 381 ++++++ .../Home/HomeChecklist/HomeChecklist.tsx | 144 ++ .../container/Home/SavedViews/SavedViews.tsx | 339 +++++ .../Home/Services/ServiceMetrics.tsx | 339 +++++ .../container/Home/Services/ServiceTraces.tsx | 237 ++++ .../src/container/Home/Services/Services.tsx | 40 + .../src/container/Home/Services/constants.ts | 80 ++ .../StepsProgress/StepsProgress.styles.scss | 55 + .../Home/StepsProgress/StepsProgress.tsx | 45 + frontend/src/container/Home/constants.ts | 112 ++ frontend/src/container/Home/index.tsx | 3 + .../ModuleStepsContainer.tsx | 2 +- .../AddDataSource/AddDataSource.tsx | 2 +- .../ServiceMetrics/ServiceMetricTable.tsx | 1 + .../ServiceTraces/ServiceTracesTable.tsx | 1 + frontend/src/container/SideNav/SideNav.tsx | 4 +- frontend/src/container/SideNav/config.ts | 1 + frontend/src/container/SideNav/menuItems.tsx | 12 + .../container/TopNav/Breadcrumbs/index.tsx | 3 +- .../TopNav/DateTimeSelection/config.ts | 1 + .../TopNav/DateTimeSelectionV2/config.ts | 1 + frontend/src/pages/HomePage/HomePage.tsx | 7 + frontend/src/pages/HomePage/index.tsx | 3 + frontend/src/periscope.scss | 9 + .../components/Card/Card.styles.scss | 89 ++ .../src/periscope/components/Card/Card.tsx | 37 + .../api/preferences/userOrgPreferences.ts | 4 +- frontend/src/types/reducer/app.ts | 12 + frontend/src/utils/permission/index.ts | 5 +- frontend/tests/traces/utils.ts | 8 +- frontend/webpack.config.js | 19 +- frontend/webpack.config.prod.js | 20 +- frontend/yarn.lock | 1187 ++++++++++++++++- 75 files changed, 8724 insertions(+), 52 deletions(-) create mode 100644 frontend/public/Icons/beacon.svg create mode 100644 frontend/public/Icons/circus-tent.svg create mode 100644 frontend/public/Icons/container-plus.svg create mode 100644 frontend/public/Icons/cracker.svg create mode 100644 frontend/public/Icons/dashboard.svg create mode 100644 frontend/public/Icons/dials.svg create mode 100644 frontend/public/Icons/eight-ball.svg create mode 100644 frontend/public/Icons/floppy-disc.svg create mode 100644 frontend/public/Icons/hello-wave.svg create mode 100644 frontend/public/Icons/hurray.svg create mode 100644 frontend/public/Icons/logs.svg create mode 100644 frontend/public/Icons/play-back.svg create mode 100644 frontend/public/Icons/spinner-half-blue.svg create mode 100644 frontend/public/Icons/spinner.svg create mode 100644 frontend/public/Icons/status-inprogress-pill.svg create mode 100644 frontend/public/Icons/status-skipped-pill.svg create mode 100644 frontend/public/Icons/triangle-ruler.svg create mode 100644 frontend/public/Icons/wrench.svg create mode 100644 frontend/public/Images/activeDot.svg create mode 100644 frontend/public/Images/allInOne.svg create mode 100644 frontend/public/Images/dotted-divider.svg create mode 100644 frontend/public/Images/perilianBackground.svg create mode 100644 frontend/src/components/Header/Header.styles.scss create mode 100644 frontend/src/components/Header/Header.tsx create mode 100644 frontend/src/container/Home/AlertRules/AlertRules.tsx create mode 100644 frontend/src/container/Home/Dashboards/Dashboards.tsx create mode 100644 frontend/src/container/Home/DataSourceInfo/DataSourceInfo.tsx create mode 100644 frontend/src/container/Home/Home.styles.scss create mode 100644 frontend/src/container/Home/Home.tsx create mode 100644 frontend/src/container/Home/HomeChecklist/HomeChecklist.styles.scss create mode 100644 frontend/src/container/Home/HomeChecklist/HomeChecklist.tsx create mode 100644 frontend/src/container/Home/SavedViews/SavedViews.tsx create mode 100644 frontend/src/container/Home/Services/ServiceMetrics.tsx create mode 100644 frontend/src/container/Home/Services/ServiceTraces.tsx create mode 100644 frontend/src/container/Home/Services/Services.tsx create mode 100644 frontend/src/container/Home/Services/constants.ts create mode 100644 frontend/src/container/Home/StepsProgress/StepsProgress.styles.scss create mode 100644 frontend/src/container/Home/StepsProgress/StepsProgress.tsx create mode 100644 frontend/src/container/Home/constants.ts create mode 100644 frontend/src/container/Home/index.tsx create mode 100644 frontend/src/pages/HomePage/HomePage.tsx create mode 100644 frontend/src/pages/HomePage/index.tsx create mode 100644 frontend/src/periscope/components/Card/Card.styles.scss create mode 100644 frontend/src/periscope/components/Card/Card.tsx diff --git a/frontend/package.json b/frontend/package.json index 498ebe7c0e..79a79c9f94 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -92,6 +92,7 @@ "lodash-es": "^4.17.21", "lucide-react": "0.379.0", "mini-css-extract-plugin": "2.4.5", + "motion": "12.4.13", "overlayscrollbars": "^2.8.1", "overlayscrollbars-react": "^0.5.6", "papaparse": "5.4.1", @@ -215,6 +216,7 @@ "eslint-plugin-simple-import-sort": "^7.0.0", "eslint-plugin-sonarjs": "^0.12.0", "husky": "^7.0.4", + "image-webpack-loader": "8.1.0", "is-ci": "^3.0.1", "jest-styled-components": "^7.0.8", "lint-staged": "^12.5.0", diff --git a/frontend/public/Icons/beacon.svg b/frontend/public/Icons/beacon.svg new file mode 100644 index 0000000000..591aa8bee2 --- /dev/null +++ b/frontend/public/Icons/beacon.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/public/Icons/circus-tent.svg b/frontend/public/Icons/circus-tent.svg new file mode 100644 index 0000000000..61a4dda47b --- /dev/null +++ b/frontend/public/Icons/circus-tent.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/public/Icons/container-plus.svg b/frontend/public/Icons/container-plus.svg new file mode 100644 index 0000000000..503e528cba --- /dev/null +++ b/frontend/public/Icons/container-plus.svg @@ -0,0 +1,44 @@ + + + + + + + + \ No newline at end of file diff --git a/frontend/public/Icons/cracker.svg b/frontend/public/Icons/cracker.svg new file mode 100644 index 0000000000..a2142bc98a --- /dev/null +++ b/frontend/public/Icons/cracker.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/public/Icons/dashboard.svg b/frontend/public/Icons/dashboard.svg new file mode 100644 index 0000000000..c09e76e33b --- /dev/null +++ b/frontend/public/Icons/dashboard.svg @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/public/Icons/dials.svg b/frontend/public/Icons/dials.svg new file mode 100644 index 0000000000..68c684cce1 --- /dev/null +++ b/frontend/public/Icons/dials.svg @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/public/Icons/eight-ball.svg b/frontend/public/Icons/eight-ball.svg new file mode 100644 index 0000000000..7446c68c46 --- /dev/null +++ b/frontend/public/Icons/eight-ball.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/public/Icons/floppy-disc.svg b/frontend/public/Icons/floppy-disc.svg new file mode 100644 index 0000000000..6300686a69 --- /dev/null +++ b/frontend/public/Icons/floppy-disc.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/frontend/public/Icons/hello-wave.svg b/frontend/public/Icons/hello-wave.svg new file mode 100644 index 0000000000..eeb62bc34d --- /dev/null +++ b/frontend/public/Icons/hello-wave.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/frontend/public/Icons/hurray.svg b/frontend/public/Icons/hurray.svg new file mode 100644 index 0000000000..31e9e2fea6 --- /dev/null +++ b/frontend/public/Icons/hurray.svg @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/public/Icons/logs.svg b/frontend/public/Icons/logs.svg new file mode 100644 index 0000000000..7e1d95cf4e --- /dev/null +++ b/frontend/public/Icons/logs.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/public/Icons/play-back.svg b/frontend/public/Icons/play-back.svg new file mode 100644 index 0000000000..f86fc2a21d --- /dev/null +++ b/frontend/public/Icons/play-back.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/public/Icons/spinner-half-blue.svg b/frontend/public/Icons/spinner-half-blue.svg new file mode 100644 index 0000000000..3f354da3a1 --- /dev/null +++ b/frontend/public/Icons/spinner-half-blue.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/frontend/public/Icons/spinner.svg b/frontend/public/Icons/spinner.svg new file mode 100644 index 0000000000..800d69e34c --- /dev/null +++ b/frontend/public/Icons/spinner.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frontend/public/Icons/status-inprogress-pill.svg b/frontend/public/Icons/status-inprogress-pill.svg new file mode 100644 index 0000000000..7807becc53 --- /dev/null +++ b/frontend/public/Icons/status-inprogress-pill.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/public/Icons/status-skipped-pill.svg b/frontend/public/Icons/status-skipped-pill.svg new file mode 100644 index 0000000000..43b8ec7907 --- /dev/null +++ b/frontend/public/Icons/status-skipped-pill.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/Icons/triangle-ruler.svg b/frontend/public/Icons/triangle-ruler.svg new file mode 100644 index 0000000000..c9d12c007c --- /dev/null +++ b/frontend/public/Icons/triangle-ruler.svg @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/public/Icons/wrench.svg b/frontend/public/Icons/wrench.svg new file mode 100644 index 0000000000..23ccecaac8 --- /dev/null +++ b/frontend/public/Icons/wrench.svg @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/public/Images/activeDot.svg b/frontend/public/Images/activeDot.svg new file mode 100644 index 0000000000..d51bc89f5e --- /dev/null +++ b/frontend/public/Images/activeDot.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/frontend/public/Images/allInOne.svg b/frontend/public/Images/allInOne.svg new file mode 100644 index 0000000000..5544b943ca --- /dev/null +++ b/frontend/public/Images/allInOne.svg @@ -0,0 +1,891 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/public/Images/dotted-divider.svg b/frontend/public/Images/dotted-divider.svg new file mode 100644 index 0000000000..727dcfb3db --- /dev/null +++ b/frontend/public/Images/dotted-divider.svg @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/public/Images/perilianBackground.svg b/frontend/public/Images/perilianBackground.svg new file mode 100644 index 0000000000..af288bb846 --- /dev/null +++ b/frontend/public/Images/perilianBackground.svg @@ -0,0 +1,1002 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/public/locales/en-GB/titles.json b/frontend/public/locales/en-GB/titles.json index d7d9fe57cb..71a4a2ed3d 100644 --- a/frontend/public/locales/en-GB/titles.json +++ b/frontend/public/locales/en-GB/titles.json @@ -5,6 +5,7 @@ "SERVICE_METRICS": "SigNoz | Service Metrics", "SERVICE_MAP": "SigNoz | Service Map", "TRACE": "SigNoz | Trace", + "HOME": "SigNoz | Home", "TRACE_DETAIL": "SigNoz | Trace Detail", "TRACES_EXPLORER": "SigNoz | Traces Explorer", "SETTINGS": "SigNoz | Settings", diff --git a/frontend/public/locales/en/titles.json b/frontend/public/locales/en/titles.json index 1e2b8f0b4a..ed2c7d30fb 100644 --- a/frontend/public/locales/en/titles.json +++ b/frontend/public/locales/en/titles.json @@ -1,6 +1,7 @@ { "SIGN_UP": "SigNoz | Sign Up", "LOGIN": "SigNoz | Login", + "HOME": "SigNoz | Home", "SERVICE_METRICS": "SigNoz | Service Metrics", "SERVICE_MAP": "SigNoz | Service Map", "GET_STARTED_OLD": "SigNoz | Get Started", diff --git a/frontend/src/AppRoutes/Private.tsx b/frontend/src/AppRoutes/Private.tsx index de2e772254..110ff60eec 100644 --- a/frontend/src/AppRoutes/Private.tsx +++ b/frontend/src/AppRoutes/Private.tsx @@ -216,7 +216,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element { history.push(fromPathname); setLocalStorageApi(LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT, ''); } else if (pathname !== ROUTES.SOMETHING_WENT_WRONG) { - history.push(ROUTES.APPLICATION); + history.push(ROUTES.HOME); } } else { // do nothing as the unauthenticated routes are LOGIN and SIGNUP and the LOGIN container takes care of routing to signup if @@ -230,7 +230,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element { history.push(fromPathname); setLocalStorageApi(LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT, ''); } else { - history.push(ROUTES.APPLICATION); + history.push(ROUTES.HOME); } } else { setLocalStorageApi(LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT, pathname); diff --git a/frontend/src/AppRoutes/index.tsx b/frontend/src/AppRoutes/index.tsx index 15312fb66a..30a20c4b84 100644 --- a/frontend/src/AppRoutes/index.tsx +++ b/frontend/src/AppRoutes/index.tsx @@ -249,9 +249,6 @@ function App(): JSX.Element { // if the user is in logged in state if (isLoggedInState) { - if (pathname === ROUTES.HOME_PAGE) { - history.replace(ROUTES.APPLICATION); - } // if the setup calls are loading then return a spinner if (isFetchingLicenses || isFetchingUser || isFetchingFeatureFlags) { return ; diff --git a/frontend/src/AppRoutes/pageComponents.ts b/frontend/src/AppRoutes/pageComponents.ts index bf84f6c804..b5201f168b 100644 --- a/frontend/src/AppRoutes/pageComponents.ts +++ b/frontend/src/AppRoutes/pageComponents.ts @@ -1,5 +1,9 @@ import Loadable from 'components/Loadable'; +export const Home = Loadable( + () => import(/* webpackChunkName: "Home" */ 'pages/HomePage/HomePage'), +); + export const ServicesTablePage = Loadable( () => import(/* webpackChunkName: "ServicesTablePage" */ 'pages/Services'), ); diff --git a/frontend/src/AppRoutes/routes.ts b/frontend/src/AppRoutes/routes.ts index 90e7858ba7..7bfb4374ad 100644 --- a/frontend/src/AppRoutes/routes.ts +++ b/frontend/src/AppRoutes/routes.ts @@ -17,6 +17,7 @@ import { EditAlertChannelsAlerts, EditRulesPage, ErrorDetails, + Home, InfrastructureMonitoring, IngestionSettings, InstalledIntegrations, @@ -80,6 +81,13 @@ const routes: AppRoutes[] = [ isPrivate: true, key: 'GET_STARTED_WITH_CLOUD', }, + { + path: ROUTES.HOME, + exact: true, + component: Home, + isPrivate: true, + key: 'HOME', + }, { path: ROUTES.ONBOARDING, exact: false, diff --git a/frontend/src/api/preferences/updateUserPreference.ts b/frontend/src/api/preferences/updateUserPreference.ts index 5b6b0427d6..3cf215993f 100644 --- a/frontend/src/api/preferences/updateUserPreference.ts +++ b/frontend/src/api/preferences/updateUserPreference.ts @@ -10,9 +10,12 @@ const updateUserPreference = async ( ): Promise< SuccessResponse | ErrorResponse > => { - const response = await axios.put(`/user/preferences`, { - preference_value: preferencePayload.value, - }); + const response = await axios.put( + `/user/preferences/${preferencePayload.preferenceID}`, + { + preference_value: preferencePayload.value, + }, + ); return { statusCode: 200, diff --git a/frontend/src/components/Header/Header.styles.scss b/frontend/src/components/Header/Header.styles.scss new file mode 100644 index 0000000000..ec19029a96 --- /dev/null +++ b/frontend/src/components/Header/Header.styles.scss @@ -0,0 +1,29 @@ +.header-container { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 16px; + height: 48px; + box-sizing: border-box; + background: rgba(11, 12, 14, 0.9); + backdrop-filter: blur(20px); + border-bottom: 1px solid var(--Slate-500, #161922); +} + +.header-left { + display: flex; + align-items: center; +} + +.header-right { + display: flex; + align-items: center; +} + +.lightMode { + .header-container { + background: var(--bg-vanilla-100); + backdrop-filter: blur(20px); + border-bottom: 1px solid var(--bg-vanilla-300); + } +} diff --git a/frontend/src/components/Header/Header.tsx b/frontend/src/components/Header/Header.tsx new file mode 100644 index 0000000000..7adba0baa9 --- /dev/null +++ b/frontend/src/components/Header/Header.tsx @@ -0,0 +1,17 @@ +import './Header.styles.scss'; + +export default function Header({ + leftComponent, + rightComponent, +}: { + leftComponent: React.ReactNode; + rightComponent: React.ReactNode | null; +}): JSX.Element { + return ( +
+
{leftComponent}
+ + {rightComponent &&
{rightComponent}
} +
+ ); +} diff --git a/frontend/src/components/NotFound/__snapshots__/NotFound.test.tsx.snap b/frontend/src/components/NotFound/__snapshots__/NotFound.test.tsx.snap index 5415d86836..0d606a989d 100644 --- a/frontend/src/components/NotFound/__snapshots__/NotFound.test.tsx.snap +++ b/frontend/src/components/NotFound/__snapshots__/NotFound.test.tsx.snap @@ -120,10 +120,10 @@ exports[`Not Found page test should render Not Found page without errors 1`] = ` - Return To Services Page + Return Home diff --git a/frontend/src/components/NotFound/index.tsx b/frontend/src/components/NotFound/index.tsx index 01f9d4931d..9b73cdf531 100644 --- a/frontend/src/components/NotFound/index.tsx +++ b/frontend/src/components/NotFound/index.tsx @@ -14,8 +14,8 @@ function NotFound({ text = defaultText }: Props): JSX.Element { Page Not Found - ); diff --git a/frontend/src/constants/routes.ts b/frontend/src/constants/routes.ts index aca2ce5e33..957c5b64e7 100644 --- a/frontend/src/constants/routes.ts +++ b/frontend/src/constants/routes.ts @@ -1,6 +1,7 @@ const ROUTES = { SIGN_UP: '/signup', LOGIN: '/login', + HOME: '/home', SERVICE_METRICS: '/services/:servicename', SERVICE_TOP_LEVEL_OPERATIONS: '/services/:servicename/top-level-operations', SERVICE_MAP: '/service-map', @@ -47,7 +48,6 @@ const ROUTES = { LOGS_EXPLORER: '/logs/logs-explorer', LIVE_LOGS: '/logs/logs-explorer/live', LOGS_PIPELINES: '/logs/pipelines', - HOME_PAGE: '/', PASSWORD_RESET: '/password-reset', LIST_LICENSES: '/licenses', LOGS_INDEX_FIELDS: '/logs-explorer/index-fields', @@ -69,6 +69,7 @@ const ROUTES = { METRICS_EXPLORER: '/metrics-explorer/summary', METRICS_EXPLORER_EXPLORER: '/metrics-explorer/explorer', METRICS_EXPLORER_VIEWS: '/metrics-explorer/views', + HOME_PAGE: '/', } as const; export default ROUTES; diff --git a/frontend/src/container/AppLayout/AppLayout.styles.scss b/frontend/src/container/AppLayout/AppLayout.styles.scss index f5900c45b2..d04a479e64 100644 --- a/frontend/src/container/AppLayout/AppLayout.styles.scss +++ b/frontend/src/container/AppLayout/AppLayout.styles.scss @@ -4,11 +4,15 @@ width: 100%; .app-content { - width: 100%; + width: calc(100% - 64px); // width of the sidebar z-index: 0; margin: 0 auto; + &.full-screen-content { + width: 100% !important; + } + .content-container { position: relative; margin: 0 1rem; diff --git a/frontend/src/container/AppLayout/index.tsx b/frontend/src/container/AppLayout/index.tsx index 6594abaffa..189588d9f6 100644 --- a/frontend/src/container/AppLayout/index.tsx +++ b/frontend/src/container/AppLayout/index.tsx @@ -301,6 +301,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element { } }, [activeLicenseV3?.key, manageCreditCard]); + const isHome = (): boolean => routeKey === 'HOME'; + const isLogsView = (): boolean => routeKey === 'LOGS' || routeKey === 'LOGS_EXPLORER' || @@ -540,13 +542,19 @@ function AppLayout(props: AppLayoutProps): JSX.Element { {isToDisplayLayout && !renderFullScreen && } -
+
}> void; + loadingUserPreferences: boolean; +}): JSX.Element { + const { user } = useAppContext(); + const [rulesExist, setRulesExist] = useState(false); + + const [sortedAlertRules, setSortedAlertRules] = useState([]); + + const location = useLocation(); + const params = new URLSearchParams(location.search); + + // Fetch Alerts + const { data: alerts, isError, isLoading } = useQuery('allAlerts', { + queryFn: getAll, + cacheTime: 0, + }); + + useEffect(() => { + const rules = alerts?.payload || []; + setRulesExist(rules.length > 0); + + const sortedRules = rules.sort((a, b) => { + // First, prioritize firing alerts + if (a.state === 'firing' && b.state !== 'firing') return -1; + if (a.state !== 'firing' && b.state === 'firing') return 1; + + // Then sort by updateAt timestamp + const aUpdateAt = new Date(a.updateAt).getTime(); + const bUpdateAt = new Date(b.updateAt).getTime(); + return bUpdateAt - aUpdateAt; + }); + + if (sortedRules.length > 0 && !loadingUserPreferences) { + onUpdateChecklistDoneItem('SETUP_ALERTS'); + } + + setSortedAlertRules(sortedRules.slice(0, 5)); + }, [alerts, onUpdateChecklistDoneItem, loadingUserPreferences]); + + const emptyStateCard = (): JSX.Element => ( +
+
+
+ empty-alert-icon + +
No Alert rules yet.
+ + {user?.role !== USER_ROLES.VIEWER && ( +
+ Create an Alert Rule to get started +
+ )} +
+ + {user?.role !== USER_ROLES.VIEWER && ( +
+ + + + + +
+ )} +
+
+ ); + + const onEditHandler = (record: GettableAlert) => (): void => { + logEvent('Homepage: Alert clicked', { + ruleId: record.id, + ruleName: record.alert, + ruleState: record.state, + }); + + const compositeQuery = mapQueryDataFromApi(record.condition.compositeQuery); + params.set( + QueryParams.compositeQuery, + encodeURIComponent(JSON.stringify(compositeQuery)), + ); + + params.set(QueryParams.panelTypes, record.condition.compositeQuery.panelType); + + params.set(QueryParams.ruleId, record.id.toString()); + + history.push(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`); + }; + + const renderAlertRules = (): JSX.Element => ( +
+
+ {sortedAlertRules.map((rule) => ( +
{ + if (e.key === 'Enter') { + onEditHandler(rule); + } + }} + > +
+ alert-rules + +
+ {rule.alert} +
+
+ +
+ {rule?.labels?.severity} + + {rule?.state === 'firing' && ( + + {rule?.state} + + )} +
+
+ ))} +
+
+ ); + + if (isLoading) { + return ( + + + + + + ); + } + + if (isError) { + return ( + + + + + + ); + } + + return ( + + {rulesExist && ( + +
Alerts
+
+ )} + + {rulesExist ? renderAlertRules() : emptyStateCard()} + + + {rulesExist && ( + +
+ + + +
+
+ )} +
+ ); +} diff --git a/frontend/src/container/Home/Dashboards/Dashboards.tsx b/frontend/src/container/Home/Dashboards/Dashboards.tsx new file mode 100644 index 0000000000..4e10a42df4 --- /dev/null +++ b/frontend/src/container/Home/Dashboards/Dashboards.tsx @@ -0,0 +1,216 @@ +import { Button, Skeleton, Tag } from 'antd'; +import logEvent from 'api/common/logEvent'; +import ROUTES from 'constants/routes'; +import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard'; +import { useSafeNavigate } from 'hooks/useSafeNavigate'; +import { ArrowRight, ArrowUpRight, Plus } from 'lucide-react'; +import Card from 'periscope/components/Card/Card'; +import { useAppContext } from 'providers/App/App'; +import { useEffect, useState } from 'react'; +import { Link } from 'react-router-dom'; +import { Dashboard } from 'types/api/dashboard/getAll'; +import { USER_ROLES } from 'types/roles'; + +export default function Dashboards({ + onUpdateChecklistDoneItem, + loadingUserPreferences, +}: { + onUpdateChecklistDoneItem: (itemKey: string) => void; + loadingUserPreferences: boolean; +}): JSX.Element { + const { safeNavigate } = useSafeNavigate(); + const { user } = useAppContext(); + + const [sortedDashboards, setSortedDashboards] = useState([]); + + // Fetch Dashboards + const { + data: dashboardsList, + isLoading: isDashboardListLoading, + isError: isDashboardListError, + } = useGetAllDashboard(); + + useEffect(() => { + if (!dashboardsList) return; + + const sortedDashboards = dashboardsList.sort((a, b) => { + const aUpdateAt = new Date(a.updatedAt).getTime(); + const bUpdateAt = new Date(b.updatedAt).getTime(); + return bUpdateAt - aUpdateAt; + }); + + if (sortedDashboards.length > 0 && !loadingUserPreferences) { + onUpdateChecklistDoneItem('SETUP_DASHBOARDS'); + } + + setSortedDashboards(sortedDashboards.slice(0, 5)); + }, [dashboardsList, onUpdateChecklistDoneItem, loadingUserPreferences]); + + const emptyStateCard = (): JSX.Element => ( +
+
+
+ empty-alert-icon + +
You don’t have any dashboards yet.
+ + {user?.role !== USER_ROLES.VIEWER && ( +
Create a dashboard to get started
+ )} +
+ + {user?.role !== USER_ROLES.VIEWER && ( +
+ + + + + +
+ )} +
+
+ ); + + const renderDashboardsList = (): JSX.Element => ( +
+
+ {sortedDashboards.slice(0, 5).map((dashboard) => { + const getLink = (): string => `${ROUTES.ALL_DASHBOARD}/${dashboard.uuid}`; + + const onClickHandler = (event: React.MouseEvent): void => { + event.stopPropagation(); + logEvent('Homepage: Dashboard clicked', { + dashboardId: dashboard.id, + dashboardName: dashboard.data.title, + }); + if (event.metaKey || event.ctrlKey) { + window.open(getLink(), '_blank'); + } else { + safeNavigate(getLink()); + } + }; + + return ( +
{ + if (e.key === 'Enter') { + onClickHandler((e as unknown) as React.MouseEvent); + } + }} + > +
+ alert-rules + +
+ {dashboard.data.title} +
+
+ +
+ {dashboard.data.tags?.map((tag) => ( + + {tag} + + ))} +
+
+ ); + })} +
+
+ ); + + if (isDashboardListLoading) { + return ( + + + + + + ); + } + + if (isDashboardListError) { + return ( + + + + + + ); + } + + const dashboardsExist = sortedDashboards.length > 0; + + return ( + + {dashboardsExist && ( + +
Dashboards
+
+ )} + + {dashboardsExist ? renderDashboardsList() : emptyStateCard()} + + + {dashboardsExist && ( + +
+ + + +
+
+ )} +
+ ); +} diff --git a/frontend/src/container/Home/DataSourceInfo/DataSourceInfo.tsx b/frontend/src/container/Home/DataSourceInfo/DataSourceInfo.tsx new file mode 100644 index 0000000000..f368a858cc --- /dev/null +++ b/frontend/src/container/Home/DataSourceInfo/DataSourceInfo.tsx @@ -0,0 +1,184 @@ +/* eslint-disable sonarjs/no-identical-functions */ +import { Button, Skeleton, Tag, Typography } from 'antd'; +import logEvent from 'api/common/logEvent'; +import ROUTES from 'constants/routes'; +import { useGetDeploymentsData } from 'hooks/CustomDomain/useGetDeploymentsData'; +import history from 'lib/history'; +import { Globe, Link2 } from 'lucide-react'; +import Card from 'periscope/components/Card/Card'; +import { useEffect, useState } from 'react'; + +function DataSourceInfo({ + dataSentToSigNoz, + isLoading, +}: { + dataSentToSigNoz: boolean; + isLoading: boolean; +}): JSX.Element { + const notSendingData = !dataSentToSigNoz; + + const { + data: deploymentsData, + isError: isErrorDeploymentsData, + } = useGetDeploymentsData(); + + const [region, setRegion] = useState(''); + const [url, setUrl] = useState(''); + + useEffect(() => { + if (deploymentsData) { + switch (deploymentsData?.data.data.cluster.region.name) { + case 'in': + setRegion('India'); + break; + case 'us': + setRegion('United States'); + break; + case 'eu': + setRegion('Europe'); + break; + default: + setRegion(deploymentsData?.data.data.cluster.region.name); + break; + } + + setUrl( + `${deploymentsData?.data.data.name}.${deploymentsData?.data.data.cluster.region.dns}`, + ); + } + }, [deploymentsData]); + + const renderNotSendingData = (): JSX.Element => ( + <> + + Hello there, Welcome to your SigNoz workspace + + + + You’re not sending any data yet.
+ SigNoz is so much better with your data ⎯ start by sending your telemetry + data to SigNoz. +
+ + + +
+
+ + hurray + Your workspace is ready + + + +
+ + {!isErrorDeploymentsData && deploymentsData && ( +
+
+ + + {region} +
+ +
+ + + + {url} + + + default + + +
+
+ )} +
+
+
+ + ); + + const renderDataReceived = (): JSX.Element => ( + <> + + Hello there, Welcome to your SigNoz workspace + + + {!isErrorDeploymentsData && deploymentsData && ( + + +
+
+
+ + + {region} +
+ +
+ + + + {url} + + + default + + +
+
+
+
+
+ )} + + ); + + return ( +
+
+
+ hello-wave +
+
+ + {isLoading && ( + <> + + + + )} + + {!isLoading && dataSentToSigNoz && renderDataReceived()} + + {!isLoading && notSendingData && renderNotSendingData()} +
+ ); +} + +export default DataSourceInfo; diff --git a/frontend/src/container/Home/Home.styles.scss b/frontend/src/container/Home/Home.styles.scss new file mode 100644 index 0000000000..c86e4a96ea --- /dev/null +++ b/frontend/src/container/Home/Home.styles.scss @@ -0,0 +1,1059 @@ +.home-container { + display: flex; + flex-direction: column; + min-height: 100vh; + overflow-y: auto; + height: 100%; + width: 100%; + + background: rgba(11, 12, 14, 0.9); + + &::-webkit-scrollbar { + width: 0.3rem; + height: 0.3rem; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background: var(--bg-slate-500); + } + + &::-webkit-scrollbar-thumb:hover { + background: var(--bg-slate-500); + } + + .home-header-left { + display: flex; + align-items: center; + gap: 8px; + + color: var(--Vanilla-100, #fff); + font-family: Inter; + font-size: 13px; + font-style: normal; + font-weight: 400; + line-height: 24px; /* 142.857% */ + } + + .home-header-right { + .welcome-checklist-btn { + color: var(--Vanilla-100, #fff); + + /* Bifrost (Ancient)/Content/sm */ + font-family: Inter; + font-size: 13px; + font-style: normal; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + } + } + + .sticky-header { + position: sticky; + top: 0; + z-index: 100; + background-color: var(--bg-ink-100); + border-bottom: 1px solid var(--bg-ink-300); + } +} + +.hello-wave-container { + .hello-wave-img-container { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 8px; + border-radius: 4px; + background: var(--Ink-300, #16181d); + } +} + +.home-content { + padding: 1rem; + display: flex; + flex-direction: row; + gap: 1rem; + + .home-left-content { + width: 50%; + display: flex; + flex-direction: column; + gap: 1rem; + } + + .home-right-content { + width: 50%; + display: flex; + flex-direction: column; + gap: 1rem; + + position: relative; + + .checklist-card { + .checklist-container { + display: flex; + flex-direction: row; + justify-content: space-between; + gap: 1rem; + } + + .checklist-items-container { + width: 60%; + padding: 1rem; + display: flex; + flex-direction: column; + gap: 2rem; + + .steps-progress-container { + width: 100%; + margin-left: -1rem; + } + } + + .checklist-container-right-img { + position: relative; + width: 30%; + display: flex; + align-items: center; + + .checklist-img-bg-container { + width: 100%; + height: 100%; + position: absolute; + + .checklist-img-bg { + position: absolute; + top: 0; + right: 8px; + } + } + + .checklist-img-container { + width: 100%; + height: 100%; + + display: flex; + align-items: center; + justify-content: flex-start; + + .checklist-img { + padding: 1rem; + } + } + } + + .periscope-card-footer { + padding: 0px !important; + } + + .checklist-footer-container { + text-align: center; + + .ant-btn.ant-btn-link { + color: var(--Vanilla-400, #c0c1c3) !important; + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 24px; /* 150% */ + letter-spacing: 0.12px; + + padding: 0px; + } + } + } + } +} + +.welcome-container { + display: flex; + flex-direction: column; + gap: 12px; +} + +.welcome-title { + color: #fff; + font-family: Inter; + font-size: 18px; + font-style: normal; + font-weight: 500; + line-height: 28px; /* 155.556% */ + letter-spacing: -0.09px; +} + +.welcome-description { + color: var(--Vanilla-400, #c0c1c3); + font-family: Inter; + font-size: 13px; + font-style: normal; + font-weight: 400; + line-height: 24px; /* 171.429% */ +} + +.workspace-ready-container { + display: flex; + flex-direction: column; + gap: 14px; + + .workspace-ready-header { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + + .ant-btn.periscope-btn.secondary { + border-radius: 2px; + border: 1px solid var(--Slate-200, #2c3140); + background: var(--Ink-200, #23262e); + } + } + + .workspace-ready-title { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + } + + .workspace-details { + display: flex; + flex-direction: row; + align-items: center; + gap: 16px; + + .ant-typography { + color: var(--Vanilla-400, #c0c1c3); + font-variant-numeric: lining-nums tabular-nums slashed-zero; + font-feature-settings: 'dlig' on, 'salt' on; + font-family: Inter; + font-size: 11px !important; + font-style: normal; + } + + .workspace-region { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + } + + .workspace-url { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + + .workspace-url-text { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + + .workspace-url-tag { + font-size: 10px; + font-weight: 400; + line-height: 18px; /* 150% */ + letter-spacing: 0.12px; + + border-radius: 3px; + + border: 1px solid var(--Slate-400, #1d212d); + background: var(--Ink-400, #121317); + color: var(--Vanilla-400, #c0c1c3); + } + } + } + + .workspace-timezone { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + } + } +} + +.divider { + width: 100%; + + svg { + width: 100%; + } +} + +.active-ingestions-container { + display: flex; + flex-direction: column; + + .active-ingestion-card { + width: 100%; + display: flex; + flex-direction: row; + gap: 8px; + + &:not(:last-child) { + border-bottom-left-radius: 0px; + border-bottom-right-radius: 0px; + border-bottom: 0px; + } + + &:not(:first-child) { + border-top-left-radius: 0px; + border-top-right-radius: 0px; + } + + .active-ingestion-card-content-container { + display: flex; + flex-direction: row; + justify-content: space-between; + gap: 8px; + } + + .active-ingestion-card-content { + display: flex; + flex-direction: row; + align-items: center; + + padding: 8px !important; + font-weight: 400; + + gap: 8px; + } + + .active-ingestion-card-actions { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + + padding: 8px 16px !important; + + font-size: 11px; + font-weight: 400; + line-height: 18px; /* 150% */ + letter-spacing: 0.12px; + + border-left: 1px solid var(--Slate-400, #1d212d); + padding-left: 8px; + + width: 200px; + + cursor: pointer; + + &:hover { + background-color: var(--bg-ink-300); + } + } + + .periscope-card-content { + padding: 0px !important; + } + } +} + +.explorers-container { + display: flex; + flex-direction: column; + + .explorer-card:not(:last-child) { + border-bottom-left-radius: 0px; + border-bottom-right-radius: 0px; + border-bottom: 0px; + } + + .explorer-card:not(:first-child) { + border-top-left-radius: 0px; + border-top-right-radius: 0px; + } + + .section-container { + display: flex; + flex-direction: row; + justify-content: space-between; + + .section-content { + display: flex; + flex-direction: row; + gap: 14px; + + .section-icon { + display: flex; + flex-direction: row; + gap: 14px; + margin-top: 3px; + } + + .section-title { + display: flex; + flex-direction: column; + gap: 6px; + + color: var(--Vanilla-400, #c0c1c3); + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 166.667% */ + letter-spacing: -0.06px; + + .title { + color: var(--Vanilla-100, #fff); + font-family: Inter; + font-size: 13px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + } + } + } + + .section-actions { + display: flex; + flex-direction: column; + gap: 14px; + + width: 150px; + justify-content: flex-end; + + .ant-btn { + color: var(--Vanilla-400, #c0c1c3); + font-family: Inter; + font-size: 11px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 150% */ + letter-spacing: 0.12px; + + padding: 8px; + + justify-content: flex-start; + + &:hover { + background-color: var(--bg-ink-300) !important; + } + } + + .periscope-btn.secondary { + border-radius: 2px; + border: 1px solid var(--Slate-400, #1d212d); + background: var(--Ink-300, #16181d); + } + } + } +} + +.home-data-card { + .home-data-card-header { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + height: 32px; + + color: var(--Vanilla-400, #c0c1c3); + font-family: Inter; + font-size: 13px; + font-style: normal; + font-weight: 500; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + } + + .saved-views-header { + width: 100%; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 8px; + + .saved-views-header-actions { + display: flex; + flex-direction: row; + gap: 8px; + + .views-tabs { + .tab { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + + font-size: 11px; + + border-radius: 2px; + border: 1px solid var(--Slate-400, #1d212d); + background: var(--Ink-400, #121317); + box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1); + + &.selected { + background: var(--Ink-300, #16181d); + } + } + } + } + } + + .services-header { + width: 100%; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 8px; + + .services-header-actions { + display: flex; + flex-direction: row; + gap: 8px; + + .ant-select { + width: 150px; + + .ant-select-selector { + background-color: transparent !important; + border: none !important; + } + } + } + } + + .home-data-item-container { + height: 250px; + overflow: auto; + + &::-webkit-scrollbar { + width: 0.2rem; + height: 0.2rem; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background: var(--bg-ink-100); + } + + &::-webkit-scrollbar-thumb:hover { + background: var(--bg-ink-100); + } + + .home-data-item { + padding: 12px; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + + cursor: pointer; + + .home-data-item-name-container { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + } + + .home-data-item-name { + color: var(--Vanilla-400, #c0c1c3); + font-family: Inter; + font-size: 13px; + font-style: normal; + font-weight: 500; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + } + + &:nth-child(odd) { + background: rgba(255, 255, 255, 0.01); + } + } + + .home-data-item-tag { + display: flex; + + .ant-tag { + display: flex; + padding: 2px 12px; + justify-content: center; + align-items: center; + gap: 4px; + border-radius: 20px; + border: 1px solid rgba(173, 127, 88, 0.2); + background: rgba(173, 127, 88, 0.1); + + color: var(--Sienna-400, #bd9979); + text-align: center; + font-family: Inter; + font-size: 12px; + font-style: normal; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + } + + .firing-tag { + color: var(--bg-sakura-500); + background: rgba(255, 113, 113, 0.1); + } + } + + &.services-list-container { + height: 268px !important; + overflow: hidden; + + .ant-table-row { + cursor: pointer; + } + } + + .services-list { + overflow-y: auto; + } + } + + .periscope-card-header { + padding: 4px 12px !important; + } + + .periscope-card-content { + padding: 0px !important; + } + + .periscope-card-footer { + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-end; + gap: 8px; + + padding: 4px 12px !important; + + border-radius: 0px 0px 6px 6px; + border: 1px solid var(--Slate-400, #1d212d); + background: var(--Ink-300, #16181d); + box-shadow: 0px -8px 6px 0px rgba(0, 0, 0, 0.1); + + .home-data-card-footer { + display: flex; + justify-content: flex-end; + + .learn-more-link { + color: var(--Vanilla-400, #c0c1c3); + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + + &:hover { + color: var(--Robin-400, #7190f9); + } + } + } + } + + &.loading-card { + .periscope-card-content { + padding: 16px !important; + min-height: 320px; + } + } + + &.error-card { + .periscope-card-content { + padding: 16px !important; + min-height: 320px; + } + } +} + +.empty-state-container { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 12px; + + height: 100%; + width: 100%; + + height: 400px; + + border-radius: 6px; + border: 1px dashed var(--Slate-300, #242834); + + .empty-state-content-container { + display: flex; + flex-direction: column; + gap: 16px; + } + + .empty-state-content { + display: flex; + flex-direction: column; + gap: 6px; + } + + .empty-state-icon { + width: 36px; + height: 36px; + } + + .empty-title { + color: var(--Vanilla-100, #fff); + font-family: Inter; + font-size: 13px; + font-style: normal; + font-weight: 500; + line-height: 18px; /* 128.571% */ + letter-spacing: -0.07px; + } + + .empty-description { + color: var(--Vanilla-400, #c0c1c3); + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 128.571% */ + letter-spacing: -0.07px; + } + + .empty-actions-container { + display: flex; + flex-direction: row; + gap: 12px; + + .ant-btn.periscope-btn.secondary { + display: flex; + height: 32px; + padding: 8px 16px; + justify-content: center; + align-items: center; + gap: 8px; + + border-radius: 1.484px; + border: 1px solid var(--Slate-400, #1d212d); + background: var(--Ink-300, #16181d); + + color: var(--Vanilla-400, #c0c1c3); + font-family: Inter; + font-size: 11px; + font-style: normal; + font-weight: 500; + line-height: 17.812px; /* 150% */ + } + } +} + +.learn-more-link { + display: flex; + flex-direction: row; + align-items: center; + gap: 4px; + + color: var(--Robin-400, #7190f9); + font-family: Inter; + font-size: 11px; + font-style: normal; + font-weight: 500; + line-height: 18px; /* 128.571% */ + letter-spacing: -0.07px; +} + +.welcome-checklist-popover { + padding: 1rem 1.5rem; + background-color: var(--bg-ink-400); + border-radius: 2px; + border: 1px solid var(--Slate-400, #1d212d); + background: var(--Ink-400, #121317); + color: var(--bg-vanilla-100); + + .ant-popover-inner { + background-color: transparent !important; + box-shadow: none !important; + } + + .home-checklist-container { + background-color: transparent !important; + width: 400px; + } +} + +.home-services-container { + .ant-table-thead { + .ant-table-cell { + background-color: transparent !important; + color: var(--Vanilla-400, #c0c1c3); + font-family: Inter; + font-size: 11px; + font-style: normal; + font-weight: 600; + line-height: 18px; /* 150% */ + letter-spacing: 0.6px; + text-transform: uppercase; + padding: 12px !important; + + border-bottom: none !important; + + &::before { + content: none !important; + } + } + } + + .ant-table-row { + .ant-table-cell { + padding: 12px !important; + border-bottom: none !important; + } + + &:nth-child(odd) { + background: rgba(255, 255, 255, 0.01); + } + } +} + +.lightMode { + .home-container { + background: rgba(255, 255, 255, 0.9); + + &::-webkit-scrollbar-thumb { + background: var(--bg-slate-500); + } + + &::-webkit-scrollbar-thumb:hover { + background: var(--bg-slate-500); + } + + .home-header-left { + color: var(--bg-ink-300); + } + + .home-header-right { + .welcome-checklist-btn { + color: var(--bg-ink-300); + } + } + + .sticky-header { + background-color: var(--bg-vanilla-100); + border-bottom: 1px solid var(--bg-vanilla-300); + } + } + + .hello-wave-container { + .hello-wave-img-container { + background: var(--bg-vanilla-300); + } + } + + .home-content { + .home-right-content { + .checklist-card { + .checklist-footer-container { + .ant-btn.ant-btn-link { + color: var(--bg-ink-300) !important; + } + } + } + } + } + + .welcome-title { + color: var(--bg-slate-300); + } + + .welcome-description { + color: var(--bg-slate-400); + } + + .workspace-ready-container { + .workspace-ready-header { + .ant-btn.periscope-btn.secondary { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-200); + } + } + + .workspace-details { + .ant-typography { + color: var(--bg-slate-400); + } + } + } + + .active-ingestions-container { + .active-ingestion-card { + .active-ingestion-card-actions { + border-left: 1px solid var(--bg-vanilla-300); + + &:hover { + background-color: var(--bg-vanilla-100); + } + } + } + } + + .explorers-container { + .section-container { + .section-content { + .section-title { + color: var(--bg-slate-300); + + .title { + color: var(--bg-slate-300); + } + } + } + + .section-actions { + .ant-btn { + color: var(--bg-slate-300); + + &:hover { + background-color: var(--bg-vanilla-100) !important; + } + } + + .periscope-btn.secondary { + border-radius: 2px; + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100); + } + } + } + } + + .home-data-card { + .home-data-card-header { + color: var(--bg-ink-300); + } + + .saved-views-header { + .saved-views-header-actions { + .views-tabs { + .tab { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100); + box-shadow: 0px 0px 8px 0px rgba(255, 255, 255, 0.1); + + &.selected { + background: var(--bg-vanilla-100); + } + } + } + } + } + + .home-data-item-container { + .home-data-item { + .home-data-item-name { + color: var(--bg-ink-300); + } + + &:nth-child(odd) { + background: rgba(255, 255, 255, 0.01); + } + } + + .home-data-item-tag { + display: flex; + + .ant-tag { + border: 1px solid rgba(173, 127, 88, 0.2); + background: rgba(173, 127, 88, 0.1); + + color: var(--bg-sienna-400); + } + + .firing-tag { + color: var(--bg-sakura-500); + background: rgba(255, 113, 113, 0.1); + } + } + } + + .periscope-card-footer { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100); + box-shadow: 0px -8px 6px 0px rgba(255, 255, 255, 0.1); + + .home-data-card-footer { + .learn-more-link { + color: var(--bg-ink-300); + + &:hover { + color: var(--bg-robin-400); + } + } + } + } + } + + .empty-state-container { + border: 1px dashed var(--bg-vanilla-300); + + .empty-title { + color: var(--bg-ink-300); + } + + .empty-description { + color: var(--bg-ink-400); + } + + .empty-actions-container { + .ant-btn.periscope-btn.secondary { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100); + + color: var(--bg-ink-300); + } + } + } + + .welcome-checklist-popover { + padding: 1rem 1.5rem; + background-color: var(--bg-vanilla-100); + border-radius: 2px; + border: 1px solid var(--bg-vanilla-300); + color: var(--bg-ink-300); + + .ant-popover-inner { + background-color: transparent !important; + box-shadow: none !important; + } + + .home-checklist-container { + background-color: transparent !important; + width: 400px; + } + } + + .home-services-container { + .ant-table-thead { + .ant-table-cell { + background-color: transparent !important; + color: var(--bg-ink-300); + + border-bottom: none !important; + + &::before { + content: none !important; + } + } + } + + .ant-table-row { + &:nth-child(odd) { + background: rgba(0, 0, 0, 0.01); + } + } + } +} diff --git a/frontend/src/container/Home/Home.tsx b/frontend/src/container/Home/Home.tsx new file mode 100644 index 0000000000..18d896dcfe --- /dev/null +++ b/frontend/src/container/Home/Home.tsx @@ -0,0 +1,721 @@ +/* eslint-disable sonarjs/no-duplicate-string */ +import './Home.styles.scss'; + +import { Color } from '@signozhq/design-tokens'; +import { Button, Popover } from 'antd'; +import logEvent from 'api/common/logEvent'; +import { HostListPayload } from 'api/infraMonitoring/getHostLists'; +import { K8sPodsListPayload } from 'api/infraMonitoring/getK8sPodsList'; +import getAllUserPreferences from 'api/preferences/getAllUserPreference'; +import updateUserPreferenceAPI from 'api/preferences/updateUserPreference'; +import Header from 'components/Header/Header'; +import { DEFAULT_ENTITY_VERSION } from 'constants/app'; +import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; +import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; +import ROUTES from 'constants/routes'; +import { getHostListsQuery } from 'container/InfraMonitoringHosts/utils'; +import { useGetHostList } from 'hooks/infraMonitoring/useGetHostList'; +import { useGetK8sPodsList } from 'hooks/infraMonitoring/useGetK8sPodsList'; +import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; +import history from 'lib/history'; +import cloneDeep from 'lodash-es/cloneDeep'; +import { CompassIcon, DotIcon, HomeIcon, Plus, Wrench } from 'lucide-react'; +import { AnimatePresence } from 'motion/react'; +import * as motion from 'motion/react-client'; +import Card from 'periscope/components/Card/Card'; +import { useAppContext } from 'providers/App/App'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useMutation, useQuery } from 'react-query'; +import { DataSource } from 'types/common/queryBuilder'; +import { UserPreference } from 'types/reducer/app'; +import { USER_ROLES } from 'types/roles'; +import { popupContainer } from 'utils/selectPopupContainer'; + +import AlertRules from './AlertRules/AlertRules'; +import { defaultChecklistItemsState } from './constants'; +import Dashboards from './Dashboards/Dashboards'; +import DataSourceInfo from './DataSourceInfo/DataSourceInfo'; +import HomeChecklist, { ChecklistItem } from './HomeChecklist/HomeChecklist'; +import SavedViews from './SavedViews/SavedViews'; +import Services from './Services/Services'; +import StepsProgress from './StepsProgress/StepsProgress'; + +const homeInterval = 30 * 60 * 1000; + +// eslint-disable-next-line sonarjs/cognitive-complexity +export default function Home(): JSX.Element { + const { user } = useAppContext(); + + const [startTime, setStartTime] = useState(null); + const [endTime, setEndTime] = useState(null); + const [updatingUserPreferences, setUpdatingUserPreferences] = useState(false); + const [loadingUserPreferences, setLoadingUserPreferences] = useState(true); + + const [checklistItems, setChecklistItems] = useState( + defaultChecklistItemsState, + ); + + const [isWelcomeChecklistSkipped, setIsWelcomeChecklistSkipped] = useState( + false, + ); + + useEffect(() => { + const now = new Date(); + const startTime = new Date(now.getTime() - homeInterval); + const endTime = now; + + setStartTime(startTime.getTime()); + setEndTime(endTime.getTime()); + }, []); + + // Detect Logs + const { data: logsData, isLoading: isLogsLoading } = useGetQueryRange( + { + query: initialQueriesMap[DataSource.LOGS], + graphType: PANEL_TYPES.TABLE, + selectedTime: 'GLOBAL_TIME', + globalSelectedInterval: '30m', + params: { + dataSource: DataSource.LOGS, + }, + }, + DEFAULT_ENTITY_VERSION, + { + queryKey: [ + REACT_QUERY_KEY.GET_QUERY_RANGE, + '30m', + endTime || Date.now(), + startTime || Date.now(), + initialQueriesMap[DataSource.LOGS], + ], + enabled: !!startTime && !!endTime, + }, + ); + + // Detect Traces + const { data: tracesData, isLoading: isTracesLoading } = useGetQueryRange( + { + query: initialQueriesMap[DataSource.TRACES], + graphType: PANEL_TYPES.TABLE, + selectedTime: 'GLOBAL_TIME', + globalSelectedInterval: '30m', + params: { + dataSource: DataSource.TRACES, + }, + }, + DEFAULT_ENTITY_VERSION, + { + queryKey: [ + REACT_QUERY_KEY.GET_QUERY_RANGE, + '30m', + endTime || Date.now(), + startTime || Date.now(), + initialQueriesMap[DataSource.TRACES], + ], + enabled: !!startTime && !!endTime, + }, + ); + + // Detect Infra Metrics - Hosts + const query = useMemo(() => { + const baseQuery = getHostListsQuery(); + + let queryStartTime = startTime; + let queryEndTime = endTime; + + if (!startTime || !endTime) { + const now = new Date(); + const startTime = new Date(now.getTime() - homeInterval); + const endTime = now; + + queryStartTime = startTime.getTime(); + queryEndTime = endTime.getTime(); + } + + return { + ...baseQuery, + limit: 10, + offset: 0, + filters: { + items: [], + op: 'AND', + }, + start: queryStartTime, + end: queryEndTime, + }; + }, [startTime, endTime]); + + const { data: hostData } = useGetHostList(query as HostListPayload, { + queryKey: ['hostList', query], + enabled: !!query, + }); + + const { data: k8sPodsData } = useGetK8sPodsList(query as K8sPodsListPayload, { + queryKey: ['K8sPodsList', query], + enabled: !!query, + }); + + const [isLogsIngestionActive, setIsLogsIngestionActive] = useState(false); + const [isTracesIngestionActive, setIsTracesIngestionActive] = useState(false); + const [isMetricsIngestionActive, setIsMetricsIngestionActive] = useState( + false, + ); + + const processUserPreferences = (userPreferences: UserPreference[]): void => { + const checklistSkipped = userPreferences?.find( + (preference) => preference.key === 'WELCOME_CHECKLIST_DO_LATER', + )?.value; + + const updatedChecklistItems = cloneDeep(checklistItems); + + const newChecklistItems = updatedChecklistItems.map((item) => { + const newItem = { ...item }; + newItem.isSkipped = + userPreferences?.find( + (preference) => preference.key === item.skippedPreferenceKey, + )?.value || false; + return newItem; + }); + + setChecklistItems(newChecklistItems); + + setIsWelcomeChecklistSkipped(checklistSkipped || false); + }; + + // Fetch User Preferences + const { refetch: refetchUserPreferences } = useQuery({ + queryFn: () => getAllUserPreferences(), + queryKey: ['getUserPreferences'], + enabled: true, + refetchOnWindowFocus: false, + onSuccess: (response) => { + if (response.payload && response.payload.data) { + processUserPreferences(response.payload.data); + } + + setLoadingUserPreferences(false); + setUpdatingUserPreferences(false); + }, + onError: () => { + setUpdatingUserPreferences(false); + setLoadingUserPreferences(false); + }, + }); + + const { mutate: updateUserPreference } = useMutation(updateUserPreferenceAPI, { + onSuccess: () => { + setUpdatingUserPreferences(false); + refetchUserPreferences(); + }, + onError: () => { + setUpdatingUserPreferences(false); + }, + }); + + const handleWillDoThisLater = (): void => { + logEvent('Welcome Checklist: Will do this later clicked', {}); + setUpdatingUserPreferences(true); + + updateUserPreference({ + preferenceID: 'WELCOME_CHECKLIST_DO_LATER', + value: true, + }); + }; + + const handleSkipChecklistItem = (item: ChecklistItem): void => { + if (item.skippedPreferenceKey) { + setUpdatingUserPreferences(true); + + updateUserPreference({ + preferenceID: item.skippedPreferenceKey, + value: true, + }); + } + }; + + const renderWelcomeChecklistModal = (): JSX.Element => ( +
+ +
+ ); + + const handleUpdateChecklistDoneItem = useCallback((itemKey: string): void => { + setChecklistItems((prevItems) => + prevItems.map((item) => + item.id === itemKey ? { ...item, completed: true } : item, + ), + ); + }, []); + + useEffect(() => { + const logsDataTotal = parseInt( + logsData?.payload?.data?.newResult?.data?.result?.[0]?.series?.[0] + ?.values?.[0]?.value || '0', + 10, + ); + + if (logsDataTotal > 0) { + setIsLogsIngestionActive(true); + handleUpdateChecklistDoneItem('SEND_LOGS'); + handleUpdateChecklistDoneItem('ADD_DATA_SOURCE'); + } + }, [logsData, handleUpdateChecklistDoneItem]); + + useEffect(() => { + const tracesDataTotal = parseInt( + tracesData?.payload?.data?.newResult?.data?.result?.[0]?.series?.[0] + ?.values?.[0]?.value || '0', + 10, + ); + + if (tracesDataTotal > 0) { + setIsTracesIngestionActive(true); + handleUpdateChecklistDoneItem('SEND_TRACES'); + handleUpdateChecklistDoneItem('ADD_DATA_SOURCE'); + } + }, [tracesData, handleUpdateChecklistDoneItem]); + + useEffect(() => { + const hostDataTotal = hostData?.payload?.data?.total ?? 0; + const k8sPodsDataTotal = k8sPodsData?.payload?.data?.total ?? 0; + + if (hostDataTotal > 0 || k8sPodsDataTotal > 0) { + setIsMetricsIngestionActive(true); + handleUpdateChecklistDoneItem('ADD_DATA_SOURCE'); + handleUpdateChecklistDoneItem('SEND_INFRA_METRICS'); + } + }, [hostData, k8sPodsData, handleUpdateChecklistDoneItem]); + + useEffect(() => { + logEvent('Homepage: Visited', {}); + }, []); + + return ( +
+
+
+ Home +
+ } + rightComponent={ +
+ {isWelcomeChecklistSkipped && ( + { + if (visible) { + logEvent('Welcome Checklist: Expanded', {}); + } else { + logEvent('Welcome Checklist: Minimized', {}); + } + }} + content={renderWelcomeChecklistModal()} + getPopupContainer={popupContainer} + rootClassName="welcome-checklist-popover" + > + + + )} +
+ } + /> +
+ +
+
+ + +
+ divider +
+ +
+ {isLogsIngestionActive && ( + + +
+
+
+ +
+ +
+ Logs ingestion is active +
+
+ +
{ + // eslint-disable-next-line sonarjs/no-duplicate-string + logEvent('Homepage: Ingestion Active Explore clicked', { + source: 'Logs', + }); + history.push(ROUTES.LOGS_EXPLORER); + }} + onKeyDown={(e): void => { + if (e.key === 'Enter') { + logEvent('Homepage: Ingestion Active Explore clicked', { + source: 'Logs', + }); + history.push(ROUTES.LOGS_EXPLORER); + } + }} + > + + Explore Logs +
+
+
+
+ )} + + {isTracesIngestionActive && ( + + +
+
+
+ +
+ +
+ Traces ingestion is active +
+
+ +
{ + logEvent('Homepage: Ingestion Active Explore clicked', { + source: 'Traces', + }); + history.push(ROUTES.TRACES_EXPLORER); + }} + onKeyDown={(e): void => { + if (e.key === 'Enter') { + logEvent('Homepage: Ingestion Active Explore clicked', { + source: 'Traces', + }); + history.push(ROUTES.TRACES_EXPLORER); + } + }} + > + + Explore Traces +
+
+
+
+ )} + + {isMetricsIngestionActive && ( + + +
+
+
+ +
+ +
+ Metrics ingestion is active +
+
+ +
{ + logEvent('Homepage: Ingestion Active Explore clicked', { + source: 'Metrics', + }); + history.push(ROUTES.INFRASTRUCTURE_MONITORING_HOSTS); + }} + onKeyDown={(e): void => { + if (e.key === 'Enter') { + logEvent('Homepage: Ingestion Active Explore clicked', { + source: 'Metrics', + }); + history.push(ROUTES.INFRASTRUCTURE_MONITORING_HOSTS); + } + }} + > + + Explore Infra Metrics +
+
+
+
+ )} +
+ + {user?.role !== USER_ROLES.VIEWER && ( +
+ + +
+
+
+ wrench +
+ +
+
Filter and save views with the Explorer
+ +
+ Explore your data, and save useful views for everyone in the team. +
+
+
+ +
+ + + +
+
+
+
+ + + +
+
+
+ dashboard +
+ +
+
Create a dashboard
+ +
+ Create a dashboard to visualize your data. +
+
+
+ +
+ +
+
+
+
+ + + +
+
+
+ cracker +
+ +
+
Add an alert
+ +
+ Create bespoke alerting rules to suit your needs. +
+
+
+ +
+ +
+
+
+
+
+ )} + + {(isLogsIngestionActive || + isTracesIngestionActive || + isMetricsIngestionActive) && ( + <> + + + + )} +
+ +
+ {!isWelcomeChecklistSkipped && !loadingUserPreferences && ( + + + + +
+
+ + + +
+
+
+ not-found +
+ +
+ checklist-img +
+
+
+
+
+ + +
+ +
+
+
+
+ )} + + {(isLogsIngestionActive || + isTracesIngestionActive || + isMetricsIngestionActive) && ( + <> + + + + )} +
+
+
+ ); +} diff --git a/frontend/src/container/Home/HomeChecklist/HomeChecklist.styles.scss b/frontend/src/container/Home/HomeChecklist/HomeChecklist.styles.scss new file mode 100644 index 0000000000..c0508abbe9 --- /dev/null +++ b/frontend/src/container/Home/HomeChecklist/HomeChecklist.styles.scss @@ -0,0 +1,381 @@ +.home-checklist-container { + display: flex; + flex-direction: column; + gap: 24px; + + .completed-checklist-container { + flex: 1; + + .completed-checklist-title { + color: var(--Vanilla-400, #c0c1c3); + font-family: Inter; + font-size: 11px; + font-style: normal; + font-weight: 500; + line-height: 18px; /* 163.636% */ + letter-spacing: 0.88px; + text-transform: uppercase; + margin-left: -1rem; + + margin-bottom: 16px; + } + + .completed-checklist-item { + display: flex; + flex-direction: column; + gap: 0.25rem; + + padding-left: 16px; + margin-bottom: 8px; + + position: relative; + + &:before { + position: absolute; + left: -18px; + top: 2px; + width: 18px; + height: 18px; + border-radius: 50%; + background-color: var(--bg-ink-400); + border: 1px solid var(--bg-slate-400); + color: var(--bg-ink-400); + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + + background-color: var(--bg-forest-400); + content: '✓'; + font-size: 16px; + font-weight: 300; + } + + &:not(:last-child):after { + content: ''; + position: absolute; + left: -9px; + top: 24px; + bottom: 0px; + width: 1px; + height: 100%; + background: linear-gradient( + to bottom, + var(--bg-forest-400) 0%, + var(--bg-forest-400) 50%, + transparent 50%, + transparent 100% + ); + background-size: 2px 10px; + } + + .completed-checklist-item-title { + color: var(--Vanilla-400, #c0c1c3); + font-family: Inter; + font-size: 13px; + font-style: normal; + font-weight: 400; + line-height: 24px; /* 171.429% */ + letter-spacing: -0.07px; + } + + .completed-checklist-item-description { + color: var(--Vanilla-400, #c0c1c3); + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + } + } + } + + .whats-next-checklist-container { + flex: 1; + + .whats-next-checklist-title { + color: var(--Vanilla-400, #c0c1c3); + font-family: Inter; + font-size: 11px; + font-style: normal; + font-weight: 500; + line-height: 18px; /* 163.636% */ + letter-spacing: 0.88px; + text-transform: uppercase; + margin-left: -1rem; + + margin-bottom: 16px; + } + + .whats-next-checklist-items-container { + display: flex; + flex-direction: column; + gap: 1rem; + } + + .whats-next-checklist-item { + display: flex; + flex-direction: column; + gap: 0.25rem; + padding-left: 16px; + + position: relative; + + &:before { + content: ''; + position: absolute; + left: -18px; + top: 2px; + width: 18px; + height: 18px; + border-radius: 50%; + background-color: var(--bg-ink-400); + border: 1px dashed var(--bg-slate-100); + color: var(--bg-ink-400); + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + } + + &.active { + &:before { + background-color: #4568dc; + content: ''; + display: inline-block; + background: url('/public/Icons/status-inprogress-pill.svg') no-repeat + center; + background-size: contain; + border: none !important; + + color: #fff; + font-size: 16px; + border: 1px dashed white; + border-radius: 50%; + } + } + + &.skipped { + &:before { + background-color: var(--bg-amber-400); + content: ''; + display: inline-block; + background: url('/public/Icons/status-skipped-pill.svg') no-repeat center; + background-size: contain; + border: none; + + color: var(--bg-ink-400); + font-size: 16px; + border-radius: 50%; + + display: flex; + align-items: center; + justify-content: center; + } + } + + &:not(:last-child):after { + content: ''; + position: absolute; + left: -9px; + top: 24px; + bottom: 0px; + width: 1px; + height: 100%; + background: linear-gradient( + to bottom, + var(--bg-slate-100) 0%, + var(--bg-slate-100) 50%, + transparent 50%, + transparent 100% + ); + background-size: 2px 10px; + } + + &.active { + &:not(:last-child):after { + content: ''; + position: absolute; + left: -9px; + top: 24px; + bottom: 0px; + width: 1px; + height: 100%; + background: linear-gradient( + to bottom, + #4568dc 0%, + #4568dc 50%, + transparent 50%, + transparent 100% + ); + background-size: 2px 10px; + } + } + + &.skipped { + &:not(:last-child):after { + content: ''; + position: absolute; + left: -9px; + top: 24px; + bottom: 0px; + width: 1px; + height: 100%; + background: linear-gradient( + to bottom, + var(--bg-amber-400) 0%, + var(--bg-amber-400) 50%, + transparent 50%, + transparent 100% + ); + background-size: 2px 10px; + } + } + + .whats-next-checklist-item-title { + color: var(--Vanilla-400, #c0c1c3); + font-family: Inter; + font-size: 13px; + font-style: normal; + line-height: 24px; /* 171.429% */ + letter-spacing: -0.07px; + + cursor: pointer; + } + + .whats-next-checklist-item-description { + color: var(--Vanilla-400, #c0c1c3); + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + } + + .whats-next-checklist-item-content { + display: none; + flex-direction: column; + gap: 0.5rem; + } + + &:hover { + .whats-next-checklist-item-content { + display: flex; + } + } + + &:hover { + .whats-next-checklist-item-title { + color: var(--Vanilla-100, #fff) !important; + } + } + + .active { + .whats-next-checklist-item-content { + display: flex; + } + } + + &.active { + .whats-next-checklist-item-title { + color: var(--Vanilla-100, #fff) !important; + } + } + + .whats-next-checklist-item-action-buttons { + display: flex; + flex-direction: row; + justify-content: space-between; + gap: 0.5rem; + + .whats-next-checklist-item-action-buttons-container { + display: flex; + flex-direction: row; + gap: 0.5rem; + } + + .periscope-btn.secondary { + border-radius: 2px; + border: 1px solid var(--Slate-200, #2c3140); + background: var(--Ink-200, #23262e); + box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1); + } + + .skip-btn { + &:hover { + color: var(--bg-amber-400); + } + } + } + } + } +} + +.lightMode { + .home-checklist-container { + .completed-checklist-container { + .completed-checklist-title { + color: var(--bg-ink-300); + } + + .completed-checklist-item { + &:before { + border: 1px solid var(--bg-vanilla-300); + color: var(--bg-ink-300); + background-color: var(--bg-forest-400); + } + + .completed-checklist-item-title { + color: var(--bg-ink-300); + } + + .completed-checklist-item-description { + color: var(--bg-ink-300); + } + } + } + + .whats-next-checklist-container { + .whats-next-checklist-title { + color: var(--bg-ink-300); + } + + .whats-next-checklist-item { + &:before { + background-color: var(--bg-vanilla-100); + border: 1px dashed var(--bg-vanilla-300); + color: var(--bg-ink-300); + } + + .whats-next-checklist-item-title { + color: var(--bg-ink-300); + } + + .whats-next-checklist-item-description { + color: var(--bg-ink-300); + } + + &:hover { + .whats-next-checklist-item-title { + color: var(--bg-ink-300) !important; + } + } + + &.active { + .whats-next-checklist-item-title { + color: var(--bg-ink-300) !important; + } + } + + .whats-next-checklist-item-action-buttons { + .periscope-btn.secondary { + border-radius: 2px; + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100); + box-shadow: 0px 0px 8px 0px rgba(255, 255, 255, 0.1); + } + } + } + } + } +} diff --git a/frontend/src/container/Home/HomeChecklist/HomeChecklist.tsx b/frontend/src/container/Home/HomeChecklist/HomeChecklist.tsx new file mode 100644 index 0000000000..84148b0d0f --- /dev/null +++ b/frontend/src/container/Home/HomeChecklist/HomeChecklist.tsx @@ -0,0 +1,144 @@ +import './HomeChecklist.styles.scss'; + +import { Button } from 'antd'; +import logEvent from 'api/common/logEvent'; +import { ArrowRight, ArrowRightToLine, BookOpenText } from 'lucide-react'; +import { useAppContext } from 'providers/App/App'; +import { useEffect, useState } from 'react'; +import { Link } from 'react-router-dom'; +import { USER_ROLES } from 'types/roles'; + +export type ChecklistItem = { + id: string; + title: string; + description: string; + completed: boolean; + isSkipped: boolean; + skippedPreferenceKey?: string; + toRoute?: string; + docsLink?: string; +}; + +function HomeChecklist({ + checklistItems, + onSkip, + isLoading, +}: { + checklistItems: ChecklistItem[]; + onSkip: (item: ChecklistItem) => void; + isLoading: boolean; +}): JSX.Element { + const { user } = useAppContext(); + + const [completedChecklistItems, setCompletedChecklistItems] = useState< + ChecklistItem[] + >([]); + + const [whatsNextChecklistItems, setWhatsNextChecklistItems] = useState< + ChecklistItem[] + >([]); + + useEffect(() => { + setCompletedChecklistItems(checklistItems.filter((item) => item.completed)); + setWhatsNextChecklistItems(checklistItems.filter((item) => !item.completed)); + }, [checklistItems]); + + return ( +
+
+
Completed
+ + {completedChecklistItems.map((item) => ( +
+
{item.title}
+
+ ))} +
+ + {whatsNextChecklistItems.length > 0 && ( +
+
What's Next
+ +
+ {whatsNextChecklistItems.map((item, index) => ( +
+
{item.title}
+ +
+
+ {item.description} +
+ + {user?.role !== USER_ROLES.VIEWER && ( +
+
+ + + + + {item.docsLink && ( + + )} +
+ + {!item.isSkipped && ( +
+ +
+ )} +
+ )} +
+
+ ))} +
+
+ )} +
+ ); +} + +export default HomeChecklist; diff --git a/frontend/src/container/Home/SavedViews/SavedViews.tsx b/frontend/src/container/Home/SavedViews/SavedViews.tsx new file mode 100644 index 0000000000..f2dd154419 --- /dev/null +++ b/frontend/src/container/Home/SavedViews/SavedViews.tsx @@ -0,0 +1,339 @@ +import { Button, Skeleton, Tag } from 'antd'; +import logEvent from 'api/common/logEvent'; +import { getViewDetailsUsingViewKey } from 'components/ExplorerCard/utils'; +import ROUTES from 'constants/routes'; +import { useGetAllViews } from 'hooks/saveViews/useGetAllViews'; +import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange'; +import { + ArrowRight, + ArrowUpRight, + CompassIcon, + DraftingCompass, +} from 'lucide-react'; +import { SOURCEPAGE_VS_ROUTES } from 'pages/SaveView/constants'; +import Card from 'periscope/components/Card/Card'; +import { useAppContext } from 'providers/App/App'; +import { useEffect, useMemo, useState } from 'react'; +import { Link } from 'react-router-dom'; +import { ViewProps } from 'types/api/saveViews/types'; +import { DataSource } from 'types/common/queryBuilder'; +import { USER_ROLES } from 'types/roles'; + +export default function SavedViews({ + onUpdateChecklistDoneItem, + loadingUserPreferences, +}: { + onUpdateChecklistDoneItem: (itemKey: string) => void; + loadingUserPreferences: boolean; +}): JSX.Element { + const { user } = useAppContext(); + const [selectedEntity, setSelectedEntity] = useState('logs'); + const [selectedEntityViews, setSelectedEntityViews] = useState([]); + + const { + data: logsViewsData, + isLoading: logsViewsLoading, + isError: logsViewsError, + } = useGetAllViews(DataSource.LOGS); + + const { + data: tracesViewsData, + isLoading: tracesViewsLoading, + isError: tracesViewsError, + } = useGetAllViews(DataSource.TRACES); + + const logsViews = useMemo(() => [...(logsViewsData?.data.data || [])], [ + logsViewsData, + ]); + + const tracesViews = useMemo(() => [...(tracesViewsData?.data.data || [])], [ + tracesViewsData, + ]); + + useEffect(() => { + setSelectedEntityViews(selectedEntity === 'logs' ? logsViews : tracesViews); + }, [selectedEntity, logsViews, tracesViews]); + + const hasTracesViews = tracesViews.length > 0; + const hasLogsViews = logsViews.length > 0; + + const hasSavedViews = hasTracesViews || hasLogsViews; + + const { handleExplorerTabChange } = useHandleExplorerTabChange(); + + const handleRedirectQuery = (view: ViewProps): void => { + logEvent('Homepage: Saved view clicked', { + viewId: view.uuid, + viewName: view.name, + entity: selectedEntity, + }); + + const currentViewDetails = getViewDetailsUsingViewKey( + view.uuid, + selectedEntity === 'logs' ? logsViews : tracesViews, + ); + if (!currentViewDetails) return; + const { query, name, uuid, panelType: currentPanelType } = currentViewDetails; + + if (selectedEntity) { + handleExplorerTabChange( + currentPanelType, + { + query, + name, + uuid, + }, + SOURCEPAGE_VS_ROUTES[selectedEntity], + ); + } + }; + + useEffect(() => { + if (hasSavedViews && !loadingUserPreferences) { + onUpdateChecklistDoneItem('SETUP_SAVED_VIEWS'); + } + }, [hasSavedViews, onUpdateChecklistDoneItem, loadingUserPreferences]); + + const emptyStateCard = (): JSX.Element => ( +
+
+
+ empty-alert-icon + +
You have not saved any views yet.
+ + {user?.role !== USER_ROLES.VIEWER && ( +
+ Explore your data and save them as views. +
+ )} +
+ + {user?.role !== USER_ROLES.VIEWER && ( +
+ + + + + +
+ )} +
+
+ ); + + const renderSavedViews = (): JSX.Element => ( +
+
+ {selectedEntityViews.slice(0, 5).map((view) => ( +
handleRedirectQuery(view)} + onKeyDown={(e): void => { + if (e.key === 'Enter') { + handleRedirectQuery(view); + } + }} + > +
+ alert-rules + +
+ {view.name} +
+
+ +
+ {view.tags?.map((tag: string) => { + if (tag === '') { + return null; + } + + return ( + + {tag} + + ); + })} +
+ + +
+ ))} + + {selectedEntityViews.length === 0 && ( +
+
+ No saved views found. +
+
+ )} + + {selectedEntity === 'logs' && logsViewsError && ( +
+
+ Oops, something went wrong while loading your saved views. +
+
+ )} + + {selectedEntity === 'traces' && tracesViewsError && ( +
+
+ Oops, something went wrong while loading your saved views. +
+
+ )} +
+
+ ); + + const handleTabChange = (tab: string): void => { + logEvent('Homepage: Saved views switched', { + tab, + }); + setSelectedEntityViews(tab === 'logs' ? logsViews : tracesViews); + setSelectedEntity(tab); + }; + + if (logsViewsLoading || tracesViewsLoading) { + return ( + + + + + + ); + } + + if (logsViewsError || tracesViewsError) { + return ( + + + + + + ); + } + + return ( + + {hasSavedViews && ( + +
+ Saved Views +
+ + + + +
+
+
+ )} + + + {selectedEntityViews.length > 0 ? renderSavedViews() : emptyStateCard()} + + + {selectedEntityViews.length > 0 && ( + +
+ + + +
+
+ )} +
+ ); +} diff --git a/frontend/src/container/Home/Services/ServiceMetrics.tsx b/frontend/src/container/Home/Services/ServiceMetrics.tsx new file mode 100644 index 0000000000..15318244ad --- /dev/null +++ b/frontend/src/container/Home/Services/ServiceMetrics.tsx @@ -0,0 +1,339 @@ +import { Button, Select, Skeleton, Table } from 'antd'; +import logEvent from 'api/common/logEvent'; +import { ENTITY_VERSION_V4 } from 'constants/app'; +import ROUTES from 'constants/routes'; +import { + getQueryRangeRequestData, + getServiceListFromQuery, +} from 'container/ServiceApplication/utils'; +import { useGetQueriesRange } from 'hooks/queryBuilder/useGetQueriesRange'; +import useGetTopLevelOperations from 'hooks/useGetTopLevelOperations'; +import useResourceAttribute from 'hooks/useResourceAttribute'; +import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils'; +import { useSafeNavigate } from 'hooks/useSafeNavigate'; +import { ArrowRight, ArrowUpRight } from 'lucide-react'; +import Card from 'periscope/components/Card/Card'; +import { useAppContext } from 'providers/App/App'; +import { IUser } from 'providers/App/types'; +import { memo, useCallback, useEffect, useMemo, useState } from 'react'; +import { QueryKey } from 'react-query'; +import { useSelector } from 'react-redux'; +import { Link } from 'react-router-dom'; +import { AppState } from 'store/reducers'; +import { ServicesList } from 'types/api/metrics/getService'; +import { GlobalReducer } from 'types/reducer/globalTime'; +import { Tags } from 'types/reducer/trace'; +import { USER_ROLES } from 'types/roles'; + +import { columns, TIME_PICKER_OPTIONS } from './constants'; + +const homeInterval = 30 * 60 * 1000; + +// Extracted EmptyState component +const EmptyState = memo( + ({ user }: { user: IUser }): JSX.Element => ( +
+
+
+ empty-alert-icon +
You are not sending traces yet.
+
+ Start sending traces to see your services. +
+
+ + {user?.role !== USER_ROLES.VIEWER && ( +
+ + + + +
+ )} +
+
+ ), +); +EmptyState.displayName = 'EmptyState'; + +// Extracted ServicesList component +const ServicesListTable = memo( + ({ + services, + onRowClick, + }: { + services: ServicesList[]; + onRowClick: (record: ServicesList) => void; + }): JSX.Element => ( +
+
+ + columns={columns} + dataSource={services} + pagination={false} + className="services-table" + onRow={(record): { onClick: () => void } => ({ + onClick: (): void => onRowClick(record), + })} + /> +
+
+ ), +); +ServicesListTable.displayName = 'ServicesListTable'; + +function ServiceMetrics({ + onUpdateChecklistDoneItem, + loadingUserPreferences, +}: { + onUpdateChecklistDoneItem: (itemKey: string) => void; + loadingUserPreferences: boolean; +}): JSX.Element { + const { selectedTime: globalSelectedInterval } = useSelector< + AppState, + GlobalReducer + >((state) => state.globalTime); + + const { user } = useAppContext(); + + const [timeRange, setTimeRange] = useState(() => { + const now = new Date().getTime(); + return { + startTime: now - homeInterval, + endTime: now, + selectedInterval: homeInterval, + }; + }); + + const { queries } = useResourceAttribute(); + const { safeNavigate } = useSafeNavigate(); + + const selectedTags = useMemo( + () => (convertRawQueriesToTraceSelectedTags(queries) as Tags[]) || [], + [queries], + ); + + const [isError, setIsError] = useState(false); + + const queryKey: QueryKey = useMemo( + () => [ + timeRange.startTime, + timeRange.endTime, + selectedTags, + globalSelectedInterval, + ], + [ + timeRange.startTime, + timeRange.endTime, + selectedTags, + globalSelectedInterval, + ], + ); + + const { + data, + isLoading: isLoadingTopLevelOperations, + isError: isErrorTopLevelOperations, + } = useGetTopLevelOperations(queryKey, { + start: timeRange.startTime * 1e6, + end: timeRange.endTime * 1e6, + }); + + const handleTimeIntervalChange = useCallback((value: number): void => { + const timeInterval = TIME_PICKER_OPTIONS.find( + (option) => option.value === value, + ); + + logEvent('Homepage: Services time interval updated', { + updatedTimeInterval: timeInterval?.label, + }); + + const now = new Date(); + setTimeRange({ + startTime: now.getTime() - value, + endTime: now.getTime(), + selectedInterval: value, + }); + }, []); + + const topLevelOperations = useMemo(() => Object.entries(data || {}), [data]); + + const queryRangeRequestData = useMemo( + () => + getQueryRangeRequestData({ + topLevelOperations, + minTime: timeRange.startTime * 1e6, + maxTime: timeRange.endTime * 1e6, + globalSelectedInterval, + }), + [ + globalSelectedInterval, + timeRange.endTime, + timeRange.startTime, + topLevelOperations, + ], + ); + + const dataQueries = useGetQueriesRange( + queryRangeRequestData, + ENTITY_VERSION_V4, + { + queryKey: useMemo( + () => [ + `GetMetricsQueryRange-home-${globalSelectedInterval}`, + timeRange.endTime, + timeRange.startTime, + globalSelectedInterval, + ], + [globalSelectedInterval, timeRange.endTime, timeRange.startTime], + ), + keepPreviousData: true, + enabled: true, + refetchOnMount: false, + onError: () => { + setIsError(true); + }, + }, + ); + + const isLoading = useMemo(() => dataQueries.some((query) => query.isLoading), [ + dataQueries, + ]); + + const services: ServicesList[] = useMemo( + () => + getServiceListFromQuery({ + queries: dataQueries, + topLevelOperations, + isLoading, + }), + [dataQueries, topLevelOperations, isLoading], + ); + + const sortedServices = useMemo( + () => + services?.sort((a, b) => { + const aUpdateAt = new Date(a.p99).getTime(); + const bUpdateAt = new Date(b.p99).getTime(); + return bUpdateAt - aUpdateAt; + }) || [], + [services], + ); + + const servicesExist = sortedServices.length > 0; + const top5Services = useMemo(() => sortedServices.slice(0, 5), [ + sortedServices, + ]); + + useEffect(() => { + if (!loadingUserPreferences && servicesExist) { + onUpdateChecklistDoneItem('SETUP_SERVICES'); + } + }, [onUpdateChecklistDoneItem, loadingUserPreferences, servicesExist]); + + const handleRowClick = useCallback( + (record: ServicesList) => { + logEvent('Homepage: Service clicked', { + serviceName: record.serviceName, + }); + safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`); + }, + [safeNavigate], + ); + + if (isLoadingTopLevelOperations || isLoading) { + return ( + + + + + + ); + } + + if (isErrorTopLevelOperations || isError) { + return ( + + + + + + ); + } + + return ( + + {servicesExist && ( + +
+ {' '} + Services +
+ +
+
+
+ )} + + {servicesExist ? renderDashboardsList() : emptyStateCard} + + + {servicesExist && ( + +
+ + + +
+
+ )} +
+ ); +} diff --git a/frontend/src/container/Home/Services/Services.tsx b/frontend/src/container/Home/Services/Services.tsx new file mode 100644 index 0000000000..f3e0490586 --- /dev/null +++ b/frontend/src/container/Home/Services/Services.tsx @@ -0,0 +1,40 @@ +import * as Sentry from '@sentry/react'; +import { FeatureKeys } from 'constants/features'; +import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback'; +import { useAppContext } from 'providers/App/App'; + +import ServiceMetrics from './ServiceMetrics'; +import ServiceTraces from './ServiceTraces'; + +function Services({ + onUpdateChecklistDoneItem, + loadingUserPreferences, +}: { + onUpdateChecklistDoneItem: (itemKey: string) => void; + loadingUserPreferences: boolean; +}): JSX.Element { + const { featureFlags } = useAppContext(); + const isSpanMetricEnabled = + featureFlags?.find((flag) => flag.name === FeatureKeys.USE_SPAN_METRICS) + ?.active || false; + + return ( + }> +
+ {isSpanMetricEnabled ? ( + + ) : ( + + )} +
+
+ ); +} + +export default Services; diff --git a/frontend/src/container/Home/Services/constants.ts b/frontend/src/container/Home/Services/constants.ts new file mode 100644 index 0000000000..8eec85696e --- /dev/null +++ b/frontend/src/container/Home/Services/constants.ts @@ -0,0 +1,80 @@ +import { TableProps } from 'antd'; +import { ServicesList } from 'types/api/metrics/getService'; + +export const columns: TableProps['columns'] = [ + { + title: 'APPLICATION', + dataIndex: 'serviceName', + key: 'serviceName', + }, + { + title: 'P99 LATENCY (in ms)', + dataIndex: 'p99', + key: 'p99', + render: (value: number): string => (value / 1000000).toFixed(2), + }, + { + title: 'ERROR RATE (% of total)', + dataIndex: 'errorRate', + key: 'errorRate', + + render: (value: number): string => value.toFixed(2), + }, + { + title: 'OPS / SEC', + dataIndex: 'callRate', + key: 'callRate', + render: (value: number): string => value.toFixed(2), + }, +]; + +export enum TimeIntervalsEnum { + LAST_5_MINUTES = 60 * 5 * 1000, // 300000 + LAST_15_MINUTES = 60 * 15 * 1000, // 900000 + LAST_30_MINUTES = 60 * 30 * 1000, // 1800000 + LAST_1_HOUR = 60 * 60 * 1000, // 3600000 + LAST_6_HOURS = 60 * 60 * 6 * 1000, // 21600000 + LAST_1_DAY = 60 * 60 * 24 * 1000, // 86400000 + LAST_3_DAYS = 60 * 60 * 24 * 3 * 1000, // 259200000 + LAST_7_DAYS = 60 * 60 * 24 * 7 * 1000, // 604800000 + LAST_30_DAYS = 60 * 60 * 24 * 30 * 1000, // 2592000000 +} + +export const TIME_PICKER_OPTIONS = [ + { + value: TimeIntervalsEnum.LAST_5_MINUTES, + label: 'Last 5 minutes', + }, + { + value: TimeIntervalsEnum.LAST_15_MINUTES, + label: 'Last 15 minutes', + }, + { + value: TimeIntervalsEnum.LAST_30_MINUTES, + label: 'Last 30 minutes', + }, + { + value: TimeIntervalsEnum.LAST_1_HOUR, + label: 'Last 1 hour', + }, + { + value: TimeIntervalsEnum.LAST_6_HOURS, + label: 'Last 6 hours', + }, + { + value: TimeIntervalsEnum.LAST_1_DAY, + label: 'Last 1 day', + }, + { + value: TimeIntervalsEnum.LAST_3_DAYS, + label: 'Last 3 days', + }, + { + value: TimeIntervalsEnum.LAST_7_DAYS, + label: 'Last 1 week', + }, + { + value: TimeIntervalsEnum.LAST_30_DAYS, + label: 'Last 1 month', + }, +]; diff --git a/frontend/src/container/Home/StepsProgress/StepsProgress.styles.scss b/frontend/src/container/Home/StepsProgress/StepsProgress.styles.scss new file mode 100644 index 0000000000..dc14c278fe --- /dev/null +++ b/frontend/src/container/Home/StepsProgress/StepsProgress.styles.scss @@ -0,0 +1,55 @@ +.steps-progress-container { + display: flex; + flex-direction: column; + gap: 8px; +} + +.steps-progress-title { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + gap: 0.5rem; +} + +.steps-progress-title-text { + color: var(--Vanilla-400, #c0c1c3); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; +} + +.steps-progress-count { + color: var(--Vanilla-400, #c0c1c3); + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; +} + +.steps-progress-progress { + display: flex; + flex-direction: column; + gap: 0.5rem; + + width: 100% !important; + + .ant-progress-steps-outer { + width: 100% !important; + } +} + +.lightMode { + .steps-progress-title-text { + color: var(--bg-ink-300); + } + + .steps-progress-count { + color: var(--bg-ink-300); + } +} diff --git a/frontend/src/container/Home/StepsProgress/StepsProgress.tsx b/frontend/src/container/Home/StepsProgress/StepsProgress.tsx new file mode 100644 index 0000000000..f341943c34 --- /dev/null +++ b/frontend/src/container/Home/StepsProgress/StepsProgress.tsx @@ -0,0 +1,45 @@ +import './StepsProgress.styles.scss'; + +import { Progress } from 'antd'; + +import { ChecklistItem } from '../HomeChecklist/HomeChecklist'; + +function StepsProgress({ + checklistItems, +}: { + checklistItems: ChecklistItem[]; +}): JSX.Element { + const completedChecklistItems = checklistItems.filter( + (item) => item.completed, + ); + + const totalChecklistItems = checklistItems.length; + + const progress = Math.round( + (completedChecklistItems.length / totalChecklistItems) * 100, + ); + + return ( +
+
+
+ Build your observability base +
+
+ Step {completedChecklistItems.length} / {totalChecklistItems} +
+
+ +
+ +
+
+ ); +} + +export default StepsProgress; diff --git a/frontend/src/container/Home/constants.ts b/frontend/src/container/Home/constants.ts new file mode 100644 index 0000000000..baeef91f71 --- /dev/null +++ b/frontend/src/container/Home/constants.ts @@ -0,0 +1,112 @@ +import ROUTES from 'constants/routes'; + +import { ChecklistItem } from './HomeChecklist/HomeChecklist'; + +export const checkListStepToPreferenceKeyMap = { + WILL_DO_LATER: 'WELCOME_CHECKLIST_DO_LATER', + SEND_LOGS: 'WELCOME_CHECKLIST_SEND_LOGS_SKIPPED', + SEND_TRACES: 'WELCOME_CHECKLIST_SEND_TRACES_SKIPPED', + SEND_INFRA_METRICS: 'WELCOME_CHECKLIST_SEND_INFRA_METRICS_SKIPPED', + SETUP_DASHBOARDS: 'WELCOME_CHECKLIST_SETUP_DASHBOARDS_SKIPPED', + SETUP_ALERTS: 'WELCOME_CHECKLIST_SETUP_ALERTS_SKIPPED', + SETUP_SAVED_VIEWS: 'WELCOME_CHECKLIST_SETUP_SAVED_VIEW_SKIPPED', + SETUP_WORKSPACE: 'WELCOME_CHECKLIST_SETUP_WORKSPACE_SKIPPED', + ADD_DATA_SOURCE: 'WELCOME_CHECKLIST_ADD_DATA_SOURCE_SKIPPED', +}; + +export const DOCS_LINKS = { + SEND_LOGS: 'https://signoz.io/docs/userguide/logs/', + SEND_TRACES: 'https://signoz.io/docs/userguide/traces/', + SEND_INFRA_METRICS: + 'https://signoz.io/docs/infrastructure-monitoring/overview/', + SETUP_ALERTS: 'https://signoz.io/docs/userguide/alerts-management/', + SETUP_SAVED_VIEWS: + 'https://signoz.io/docs/product-features/saved-view/#step-2-save-your-view', + SETUP_DASHBOARDS: 'https://signoz.io/docs/userguide/manage-dashboards/', +}; + +export const defaultChecklistItemsState: ChecklistItem[] = [ + { + id: 'SETUP_WORKSPACE', + title: 'Set up your workspace', + description: '', + completed: true, + isSkipped: false, + skippedPreferenceKey: checkListStepToPreferenceKeyMap.SETUP_WORKSPACE, + }, + { + id: 'ADD_DATA_SOURCE', + title: 'Add your first data source', + description: '', + completed: false, + isSkipped: false, + skippedPreferenceKey: checkListStepToPreferenceKeyMap.ADD_DATA_SOURCE, + toRoute: ROUTES.GET_STARTED, + }, + { + id: 'SEND_LOGS', + title: 'Send your logs', + description: + 'Send your logs to SigNoz to get more visibility into how your resources interact.', + completed: false, + isSkipped: false, + skippedPreferenceKey: checkListStepToPreferenceKeyMap.SEND_LOGS, + toRoute: ROUTES.GET_STARTED, + docsLink: DOCS_LINKS.SEND_LOGS, + }, + { + id: 'SEND_TRACES', + title: 'Send your traces', + description: + 'Send your traces to SigNoz to get more visibility into how your resources interact.', + completed: false, + isSkipped: false, + skippedPreferenceKey: checkListStepToPreferenceKeyMap.SEND_TRACES, + toRoute: ROUTES.GET_STARTED, + docsLink: DOCS_LINKS.SEND_TRACES, + }, + { + id: 'SEND_INFRA_METRICS', + title: 'Send your infra metrics', + description: + 'Send your infra metrics to SigNoz to get more visibility into your infrastructure.', + completed: false, + isSkipped: false, + skippedPreferenceKey: checkListStepToPreferenceKeyMap.SEND_INFRA_METRICS, + toRoute: ROUTES.GET_STARTED, + docsLink: DOCS_LINKS.SEND_INFRA_METRICS, + }, + { + id: 'SETUP_ALERTS', + title: 'Setup Alerts', + description: + 'Setup alerts to get notified when your resources are not performing as expected.', + completed: false, + isSkipped: false, + skippedPreferenceKey: checkListStepToPreferenceKeyMap.SETUP_ALERTS, + toRoute: ROUTES.ALERTS_NEW, + docsLink: DOCS_LINKS.SETUP_ALERTS, + }, + { + id: 'SETUP_SAVED_VIEWS', + title: 'Setup Saved Views', + description: + 'Save your views to get a quick overview of your data and share it with your team.', + completed: false, + isSkipped: false, + skippedPreferenceKey: checkListStepToPreferenceKeyMap.SETUP_SAVED_VIEWS, + toRoute: ROUTES.LOGS_EXPLORER, + docsLink: DOCS_LINKS.SETUP_SAVED_VIEWS, + }, + { + id: 'SETUP_DASHBOARDS', + title: 'Setup Dashboards', + description: + 'Create dashboards to visualize your data and share it with your team.', + completed: false, + isSkipped: false, + skippedPreferenceKey: checkListStepToPreferenceKeyMap.SETUP_DASHBOARDS, + toRoute: ROUTES.ALL_DASHBOARD, + docsLink: DOCS_LINKS.SETUP_DASHBOARDS, + }, +]; diff --git a/frontend/src/container/Home/index.tsx b/frontend/src/container/Home/index.tsx new file mode 100644 index 0000000000..fbe3fed6bf --- /dev/null +++ b/frontend/src/container/Home/index.tsx @@ -0,0 +1,3 @@ +import Home from './Home'; + +export default Home; diff --git a/frontend/src/container/OnboardingContainer/common/ModuleStepsContainer/ModuleStepsContainer.tsx b/frontend/src/container/OnboardingContainer/common/ModuleStepsContainer/ModuleStepsContainer.tsx index f9efa061f7..c44b483082 100644 --- a/frontend/src/container/OnboardingContainer/common/ModuleStepsContainer/ModuleStepsContainer.tsx +++ b/frontend/src/container/OnboardingContainer/common/ModuleStepsContainer/ModuleStepsContainer.tsx @@ -380,7 +380,7 @@ export default function ModuleStepsContainer({ }; const handleLogoClick = (): void => { - history.push('/'); + history.push('/home'); }; return ( diff --git a/frontend/src/container/OnboardingV2Container/AddDataSource/AddDataSource.tsx b/frontend/src/container/OnboardingV2Container/AddDataSource/AddDataSource.tsx index cd3febae06..d336928f95 100644 --- a/frontend/src/container/OnboardingV2Container/AddDataSource/AddDataSource.tsx +++ b/frontend/src/container/OnboardingV2Container/AddDataSource/AddDataSource.tsx @@ -426,7 +426,7 @@ function OnboardingAddDataSource(): JSX.Element { }, ); - history.push(ROUTES.APPLICATION); + history.push(ROUTES.HOME); }} /> Get Started (2/4) diff --git a/frontend/src/container/ServiceApplication/ServiceMetrics/ServiceMetricTable.tsx b/frontend/src/container/ServiceApplication/ServiceMetrics/ServiceMetricTable.tsx index 9541ee0936..85049ea692 100644 --- a/frontend/src/container/ServiceApplication/ServiceMetrics/ServiceMetricTable.tsx +++ b/frontend/src/container/ServiceApplication/ServiceMetrics/ServiceMetricTable.tsx @@ -114,6 +114,7 @@ function ServiceMetricTable({ loading={isLoading} dataSource={services} rowKey="serviceName" + className="service-metrics-table" /> ); diff --git a/frontend/src/container/ServiceApplication/ServiceTraces/ServiceTracesTable.tsx b/frontend/src/container/ServiceApplication/ServiceTraces/ServiceTracesTable.tsx index fcc4c78933..32cb805651 100644 --- a/frontend/src/container/ServiceApplication/ServiceTraces/ServiceTracesTable.tsx +++ b/frontend/src/container/ServiceApplication/ServiceTraces/ServiceTracesTable.tsx @@ -71,6 +71,7 @@ function ServiceTraceTable({ loading={loading} dataSource={services} rowKey="serviceName" + className="service-traces-table" /> ); diff --git a/frontend/src/container/SideNav/SideNav.tsx b/frontend/src/container/SideNav/SideNav.tsx index 8c1305c970..9f71c2f810 100644 --- a/frontend/src/container/SideNav/SideNav.tsx +++ b/frontend/src/container/SideNav/SideNav.tsx @@ -352,7 +352,7 @@ function SideNav(): JSX.Element { // eslint-disable-next-line react/no-unknown-property onClick={(event: MouseEvent): void => { // Current home page - onClickHandler(ROUTES.APPLICATION, event); + onClickHandler(ROUTES.HOME, event); }} > SigNoz @@ -366,7 +366,7 @@ function SideNav(): JSX.Element {
- {isCloudUserVal && ( + {isCloudUserVal && user?.role !== USER_ROLES.VIEWER && (