From 2a000cc5537969fb55f5985c83f76a894633adab Mon Sep 17 00:00:00 2001 From: Yunus M Date: Mon, 2 Sep 2024 23:52:12 +0530 Subject: [PATCH 01/15] feat: onboarding v2 base setup --- frontend/src/AppRoutes/pageComponents.ts | 4 ++++ frontend/src/AppRoutes/routes.ts | 8 ++++++++ frontend/src/constants/routes.ts | 1 + frontend/src/container/AppLayout/index.tsx | 2 ++ frontend/src/container/SideNav/config.ts | 1 + frontend/src/container/SideNav/menuItems.tsx | 11 +++++++++++ frontend/src/container/TopNav/Breadcrumbs/index.tsx | 1 + .../src/pages/OnboardingPageV2/OnboardingPageV2.tsx | 11 +++++++++++ frontend/src/pages/OnboardingPageV2/index.tsx | 3 +++ frontend/src/utils/permission/index.ts | 1 + 10 files changed, 43 insertions(+) create mode 100644 frontend/src/pages/OnboardingPageV2/OnboardingPageV2.tsx create mode 100644 frontend/src/pages/OnboardingPageV2/index.tsx diff --git a/frontend/src/AppRoutes/pageComponents.ts b/frontend/src/AppRoutes/pageComponents.ts index 0a7764149b..c267304e6b 100644 --- a/frontend/src/AppRoutes/pageComponents.ts +++ b/frontend/src/AppRoutes/pageComponents.ts @@ -66,6 +66,10 @@ export const Onboarding = Loadable( () => import(/* webpackChunkName: "Onboarding" */ 'pages/OnboardingPage'), ); +export const OnboardingV2 = Loadable( + () => import(/* webpackChunkName: "Onboarding" */ 'pages/OnboardingPageV2'), +); + export const DashboardPage = Loadable( () => import(/* webpackChunkName: "DashboardPage" */ 'pages/DashboardsListPage'), diff --git a/frontend/src/AppRoutes/routes.ts b/frontend/src/AppRoutes/routes.ts index 42ce00c0fb..fc59fef1e3 100644 --- a/frontend/src/AppRoutes/routes.ts +++ b/frontend/src/AppRoutes/routes.ts @@ -31,6 +31,7 @@ import { NewDashboardPage, OldLogsExplorer, Onboarding, + OnboardingV2, OrganizationSettings, PasswordReset, PipelinePage, @@ -68,6 +69,13 @@ const routes: AppRoutes[] = [ isPrivate: true, key: 'GET_STARTED', }, + { + path: ROUTES.GET_STARTED_V2, + exact: false, + component: OnboardingV2, + isPrivate: true, + key: 'GET_STARTED_V2', + }, { component: LogsIndexToFields, path: ROUTES.LOGS_INDEX_FIELDS, diff --git a/frontend/src/constants/routes.ts b/frontend/src/constants/routes.ts index b4f43ee684..50d189f4a8 100644 --- a/frontend/src/constants/routes.ts +++ b/frontend/src/constants/routes.ts @@ -8,6 +8,7 @@ const ROUTES = { TRACE_DETAIL: '/trace/:id', TRACES_EXPLORER: '/traces-explorer', GET_STARTED: '/get-started', + GET_STARTED_V2: '/get-started-v2', GET_STARTED_APPLICATION_MONITORING: '/get-started/application-monitoring', GET_STARTED_LOGS_MANAGEMENT: '/get-started/logs-management', GET_STARTED_INFRASTRUCTURE_MONITORING: diff --git a/frontend/src/container/AppLayout/index.tsx b/frontend/src/container/AppLayout/index.tsx index beb4cea61c..898481e2a5 100644 --- a/frontend/src/container/AppLayout/index.tsx +++ b/frontend/src/container/AppLayout/index.tsx @@ -191,6 +191,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element { const pageTitle = t(routeKey); const renderFullScreen = pathname === ROUTES.GET_STARTED || + pathname === ROUTES.GET_STARTED_V2 || + pathname === ROUTES.WORKSPACE_LOCKED || pathname === ROUTES.GET_STARTED_APPLICATION_MONITORING || pathname === ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING || pathname === ROUTES.GET_STARTED_LOGS_MANAGEMENT || diff --git a/frontend/src/container/SideNav/config.ts b/frontend/src/container/SideNav/config.ts index 37e9db4d9f..7b37f64bb7 100644 --- a/frontend/src/container/SideNav/config.ts +++ b/frontend/src/container/SideNav/config.ts @@ -27,6 +27,7 @@ export const routeConfig: Record = { [ROUTES.ERROR_DETAIL]: [QueryParams.resourceAttributes], [ROUTES.HOME_PAGE]: [QueryParams.resourceAttributes], [ROUTES.GET_STARTED]: [QueryParams.resourceAttributes], + [ROUTES.GET_STARTED_V2]: [QueryParams.resourceAttributes], [ROUTES.LIST_ALL_ALERT]: [QueryParams.resourceAttributes], [ROUTES.LIST_LICENSES]: [QueryParams.resourceAttributes], [ROUTES.LOGIN]: [QueryParams.resourceAttributes], diff --git a/frontend/src/container/SideNav/menuItems.tsx b/frontend/src/container/SideNav/menuItems.tsx index 6d24b74c53..a1c41ddea8 100644 --- a/frontend/src/container/SideNav/menuItems.tsx +++ b/frontend/src/container/SideNav/menuItems.tsx @@ -29,6 +29,12 @@ export const getStartedMenuItem = { icon: , }; +export const getStartedV2MenuItem = { + key: ROUTES.GET_STARTED_V2, + label: 'Get Started V2', + icon: , +}; + export const inviteMemberMenuItem = { key: `${ROUTES.ORG_SETTINGS}#invite-team-members`, label: 'Invite Team Member', @@ -66,6 +72,11 @@ export const trySignozCloudMenuItem: SidebarItem = { }; const menuItems: SidebarItem[] = [ + { + key: ROUTES.GET_STARTED_V2, + label: 'Get Started V2', + icon: , + }, { key: ROUTES.APPLICATION, label: 'Services', diff --git a/frontend/src/container/TopNav/Breadcrumbs/index.tsx b/frontend/src/container/TopNav/Breadcrumbs/index.tsx index 9efd50d2c3..c98f4d05e2 100644 --- a/frontend/src/container/TopNav/Breadcrumbs/index.tsx +++ b/frontend/src/container/TopNav/Breadcrumbs/index.tsx @@ -9,6 +9,7 @@ const breadcrumbNameMap: Record = { [ROUTES.SERVICE_MAP]: 'Service Map', [ROUTES.USAGE_EXPLORER]: 'Usage Explorer', [ROUTES.GET_STARTED]: 'Get Started', + [ROUTES.GET_STARTED_V2]: 'Get Started V2', [ROUTES.ALL_CHANNELS]: 'Channels', [ROUTES.SETTINGS]: 'Settings', [ROUTES.DASHBOARD]: 'Dashboard', diff --git a/frontend/src/pages/OnboardingPageV2/OnboardingPageV2.tsx b/frontend/src/pages/OnboardingPageV2/OnboardingPageV2.tsx new file mode 100644 index 0000000000..ccdc0226f8 --- /dev/null +++ b/frontend/src/pages/OnboardingPageV2/OnboardingPageV2.tsx @@ -0,0 +1,11 @@ +import { Typography } from 'antd'; + +function OnboardingPageV2(): JSX.Element { + return ( +
+ Onboarding V2 +
+ ); +} + +export default OnboardingPageV2; diff --git a/frontend/src/pages/OnboardingPageV2/index.tsx b/frontend/src/pages/OnboardingPageV2/index.tsx new file mode 100644 index 0000000000..9fad953dfe --- /dev/null +++ b/frontend/src/pages/OnboardingPageV2/index.tsx @@ -0,0 +1,3 @@ +import OnboardingPage from './OnboardingPageV2'; + +export default OnboardingPage; diff --git a/frontend/src/utils/permission/index.ts b/frontend/src/utils/permission/index.ts index 8a35121f57..6052900a05 100644 --- a/frontend/src/utils/permission/index.ts +++ b/frontend/src/utils/permission/index.ts @@ -86,6 +86,7 @@ export const routePermission: Record = { LOGS_PIPELINES: ['ADMIN', 'EDITOR', 'VIEWER'], TRACE_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'], GET_STARTED: ['ADMIN', 'EDITOR', 'VIEWER'], + GET_STARTED_V2: ['ADMIN', 'EDITOR', 'VIEWER'], GET_STARTED_APPLICATION_MONITORING: ['ADMIN', 'EDITOR', 'VIEWER'], GET_STARTED_INFRASTRUCTURE_MONITORING: ['ADMIN', 'EDITOR', 'VIEWER'], GET_STARTED_LOGS_MANAGEMENT: ['ADMIN', 'EDITOR', 'VIEWER'], From 31359c46bcae141b11a5e1c401094ae0141f52eb Mon Sep 17 00:00:00 2001 From: Yunus M Date: Wed, 18 Sep 2024 13:10:56 +0530 Subject: [PATCH 02/15] feat: add questionaire components (#5998) * feat: add questionaire components * feat: update css * feat: delete icon svgs and update css * feat: update component names --- frontend/public/Logos/hippa.svg | 10 + .../public/Logos/signoz-brand-logo-new.svg | 30 ++ frontend/public/Logos/soc2.svg | 72 +++++ frontend/public/fonts/Satoshi-Regular.woff2 | Bin 0 -> 25516 bytes .../container/AppLayout/AppLayout.styles.scss | 2 + .../AboutSigNozQuestions.tsx | 202 +++++++++++++ .../InviteTeamMembers/InviteTeamMembers.tsx | 82 ++++++ .../OnboardingFooter.styles.scss | 46 +++ .../OnboardingFooter/OnboardingFooter.tsx | 55 ++++ .../OnboardingFooter/index.ts | 1 + .../OnboardingHeader.styles.scss | 58 ++++ .../OnboardingHeader/OnboardingHeader.tsx | 19 ++ .../OnboardingHeader/index.ts | 1 + .../OnboardingQuestionaire.styles.scss | 278 ++++++++++++++++++ .../OptimiseSignozNeeds.tsx | 127 ++++++++ .../OrgQuestions/OrgQuestions.styles.scss | 0 .../OrgQuestions/OrgQuestions.tsx | 257 ++++++++++++++++ .../OnboardingQuestionaire/index.tsx | 55 ++++ .../OnboardingPageV2/OnboardingPageV2.tsx | 4 +- 19 files changed, 1297 insertions(+), 2 deletions(-) create mode 100644 frontend/public/Logos/hippa.svg create mode 100644 frontend/public/Logos/signoz-brand-logo-new.svg create mode 100644 frontend/public/Logos/soc2.svg create mode 100644 frontend/public/fonts/Satoshi-Regular.woff2 create mode 100644 frontend/src/container/OnboardingQuestionaire/AboutSigNozQuestions/AboutSigNozQuestions.tsx create mode 100644 frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.tsx create mode 100644 frontend/src/container/OnboardingQuestionaire/OnboardingFooter/OnboardingFooter.styles.scss create mode 100644 frontend/src/container/OnboardingQuestionaire/OnboardingFooter/OnboardingFooter.tsx create mode 100644 frontend/src/container/OnboardingQuestionaire/OnboardingFooter/index.ts create mode 100644 frontend/src/container/OnboardingQuestionaire/OnboardingHeader/OnboardingHeader.styles.scss create mode 100644 frontend/src/container/OnboardingQuestionaire/OnboardingHeader/OnboardingHeader.tsx create mode 100644 frontend/src/container/OnboardingQuestionaire/OnboardingHeader/index.ts create mode 100644 frontend/src/container/OnboardingQuestionaire/OnboardingQuestionaire.styles.scss create mode 100644 frontend/src/container/OnboardingQuestionaire/OptimiseSignozNeeds/OptimiseSignozNeeds.tsx create mode 100644 frontend/src/container/OnboardingQuestionaire/OrgQuestions/OrgQuestions.styles.scss create mode 100644 frontend/src/container/OnboardingQuestionaire/OrgQuestions/OrgQuestions.tsx create mode 100644 frontend/src/container/OnboardingQuestionaire/index.tsx diff --git a/frontend/public/Logos/hippa.svg b/frontend/public/Logos/hippa.svg new file mode 100644 index 0000000000..2a1b74b818 --- /dev/null +++ b/frontend/public/Logos/hippa.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/frontend/public/Logos/signoz-brand-logo-new.svg b/frontend/public/Logos/signoz-brand-logo-new.svg new file mode 100644 index 0000000000..f57d3c5def --- /dev/null +++ b/frontend/public/Logos/signoz-brand-logo-new.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/public/Logos/soc2.svg b/frontend/public/Logos/soc2.svg new file mode 100644 index 0000000000..234321c95c --- /dev/null +++ b/frontend/public/Logos/soc2.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/public/fonts/Satoshi-Regular.woff2 b/frontend/public/fonts/Satoshi-Regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..81c40ab08a87d975b0e61e0d8497fe4dd3c11da4 GIT binary patch literal 25516 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 literal 0 HcmV?d00001 diff --git a/frontend/src/container/AppLayout/AppLayout.styles.scss b/frontend/src/container/AppLayout/AppLayout.styles.scss index a991f08351..98ca9084f2 100644 --- a/frontend/src/container/AppLayout/AppLayout.styles.scss +++ b/frontend/src/container/AppLayout/AppLayout.styles.scss @@ -7,6 +7,8 @@ width: calc(100% - 64px); z-index: 0; + margin: 0 auto; + .content-container { position: relative; margin: 0 1rem; diff --git a/frontend/src/container/OnboardingQuestionaire/AboutSigNozQuestions/AboutSigNozQuestions.tsx b/frontend/src/container/OnboardingQuestionaire/AboutSigNozQuestions/AboutSigNozQuestions.tsx new file mode 100644 index 0000000000..09ca867e5f --- /dev/null +++ b/frontend/src/container/OnboardingQuestionaire/AboutSigNozQuestions/AboutSigNozQuestions.tsx @@ -0,0 +1,202 @@ +/* eslint-disable sonarjs/cognitive-complexity */ +import '../OnboardingQuestionaire.styles.scss'; + +import { Button, Typography } from 'antd'; +import { ArrowLeft, ArrowRight } from 'lucide-react'; +import { useEffect, useState } from 'react'; + +interface AboutSigNozQuestionsProps { + onNext: () => void; + onBack: () => void; +} + +export function AboutSigNozQuestions({ + onNext, + onBack, +}: AboutSigNozQuestionsProps): JSX.Element { + const [hearAboutSignoz, setHearAboutSignoz] = useState(null); + const [otherAboutSignoz, setOtherAboutSignoz] = useState(''); + const [interestedSignoz, setInterestedSignoz] = useState(null); + const [otherInterest, setOtherInterest] = useState(''); + const [isNextDisabled, setIsNextDisabled] = useState(true); + + useEffect((): void => { + if ( + hearAboutSignoz !== null && + (hearAboutSignoz !== 'Others' || otherAboutSignoz !== '') && + interestedSignoz !== null && + (interestedSignoz !== 'Others' || otherInterest !== '') + ) { + setIsNextDisabled(false); + } else { + setIsNextDisabled(true); + } + }, [hearAboutSignoz, otherAboutSignoz, interestedSignoz, otherInterest]); + + return ( +
+ + Tell Us About Your Interest in SigNoz + + + We'd love to know a little bit about you and your interest in SigNoz + + +
+
+
+
Where did you hear about SigNoz?
+
+ + + + + + + + {hearAboutSignoz === 'Others' ? ( + setOtherAboutSignoz(e.target.value)} + /> + ) : ( + + )} +
+
+ +
+
+ What are you interested in doing with SigNoz? +
+
+ + + + + {interestedSignoz === 'Others' ? ( + setOtherInterest(e.target.value)} + /> + ) : ( + + )} +
+
+
+ +
+ + + +
+
+
+ ); +} diff --git a/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.tsx b/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.tsx new file mode 100644 index 0000000000..e531e586a6 --- /dev/null +++ b/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.tsx @@ -0,0 +1,82 @@ +import { Button, Input, Select, Typography } from 'antd'; +import { ArrowLeft, ArrowRight } from 'lucide-react'; + +interface InviteTeamMembersProps { + onNext: () => void; + onBack: () => void; +} + +const userRolesOptions = ( + +); + +function InviteTeamMembers({ + onNext, + onBack, +}: InviteTeamMembersProps): JSX.Element { + return ( +
+ + Observability made collaborative + + + The more your team uses SigNoz, the stronger your observability. Share + dashboards, collaborate on alerts, and troubleshoot faster together. + + +
+
+
+
+ Collaborate with your team +
+ Invite your team to the SigNoz workspace +
+
+ +
+ + + + + +
+
+
+ +
+ + + +
+ +
+ +
+
+
+ ); +} + +export default InviteTeamMembers; diff --git a/frontend/src/container/OnboardingQuestionaire/OnboardingFooter/OnboardingFooter.styles.scss b/frontend/src/container/OnboardingQuestionaire/OnboardingFooter/OnboardingFooter.styles.scss new file mode 100644 index 0000000000..beddf2441e --- /dev/null +++ b/frontend/src/container/OnboardingQuestionaire/OnboardingFooter/OnboardingFooter.styles.scss @@ -0,0 +1,46 @@ +.footer-main-container { + display: flex; + justify-content: center; + box-sizing: border-box; +} + +.footer-container { + display: inline-flex; + height: 36px; + padding: 12px; + justify-content: center; + align-items: center; + gap: 32px; + flex-shrink: 0; + border-radius: 4px; + border: 1px solid var(--Greyscale-Slate-500, #161922); + background: var(--Ink-400, #121317); +} + +.footer-container .footer-content { + display: flex; + align-items: center; + gap: 10px; +} + +.footer-container .footer-link { + display: flex; + align-items: center; + gap: 6px; + color: #c0c1c3; +} + +.footer-container .footer-text { + color: var(--Vanilla-400, var(--Greyscale-Vanilla-400, #c0c1c3)); + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: 0.2px; +} + +.footer-container .footer-dot { + width: 4px; + height: 4px; + fill: var(--Greyscale-Slate-200, #2c3140); +} diff --git a/frontend/src/container/OnboardingQuestionaire/OnboardingFooter/OnboardingFooter.tsx b/frontend/src/container/OnboardingQuestionaire/OnboardingFooter/OnboardingFooter.tsx new file mode 100644 index 0000000000..25482e2436 --- /dev/null +++ b/frontend/src/container/OnboardingQuestionaire/OnboardingFooter/OnboardingFooter.tsx @@ -0,0 +1,55 @@ +import './OnboardingFooter.styles.scss'; + +import { ArrowUpRight, Dot } from 'lucide-react'; + +export function OnboardingFooter(): JSX.Element { + return ( +
+ +
+ ); +} diff --git a/frontend/src/container/OnboardingQuestionaire/OnboardingFooter/index.ts b/frontend/src/container/OnboardingQuestionaire/OnboardingFooter/index.ts new file mode 100644 index 0000000000..cc029cb359 --- /dev/null +++ b/frontend/src/container/OnboardingQuestionaire/OnboardingFooter/index.ts @@ -0,0 +1 @@ +export { OnboardingFooter } from './OnboardingFooter'; diff --git a/frontend/src/container/OnboardingQuestionaire/OnboardingHeader/OnboardingHeader.styles.scss b/frontend/src/container/OnboardingQuestionaire/OnboardingHeader/OnboardingHeader.styles.scss new file mode 100644 index 0000000000..0b2cecaac6 --- /dev/null +++ b/frontend/src/container/OnboardingQuestionaire/OnboardingHeader/OnboardingHeader.styles.scss @@ -0,0 +1,58 @@ +.header-container { + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 0px; + + box-sizing: border-box; +} + +.header-container .logo-container { + display: flex; + align-items: center; + gap: 10px; +} + +.header-container .logo-container img { + height: 17.5px; + width: 17.5px; +} + +.header-container .logo-text { + font-family: 'Satoshi', sans-serif; + color: #fff; + font-size: 15.4px; + font-style: normal; + font-weight: 500; + line-height: 17.5px; +} + +.header-container .get-help-container { + display: flex; + width: 113px; + height: 32px; + padding: 6px; + justify-content: center; + align-items: center; + gap: 6px; + flex-shrink: 0; + border-radius: 2px; + border: 1px solid var(--Greyscale-Slate-400, #1d212d); + background: var(--Ink-300, #16181d); +} + +.header-container .get-help-container img { + width: 12px; + height: 12px; + flex-shrink: 0; +} + +.header-container .get-help-text { + color: var(--Vanilla-400, var(--Greyscale-Vanilla-400, #c0c1c3)); + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 10px; + letter-spacing: 0.12px; +} diff --git a/frontend/src/container/OnboardingQuestionaire/OnboardingHeader/OnboardingHeader.tsx b/frontend/src/container/OnboardingQuestionaire/OnboardingHeader/OnboardingHeader.tsx new file mode 100644 index 0000000000..76801d7620 --- /dev/null +++ b/frontend/src/container/OnboardingQuestionaire/OnboardingHeader/OnboardingHeader.tsx @@ -0,0 +1,19 @@ +import './OnboardingHeader.styles.scss'; + +import { Color } from '@signozhq/design-tokens'; +import { LifeBuoy } from 'lucide-react'; + +export function OnboardingHeader(): JSX.Element { + return ( +
+
+ SigNoz + SigNoz +
+
+ + Get Help +
+
+ ); +} diff --git a/frontend/src/container/OnboardingQuestionaire/OnboardingHeader/index.ts b/frontend/src/container/OnboardingQuestionaire/OnboardingHeader/index.ts new file mode 100644 index 0000000000..644a6d9b84 --- /dev/null +++ b/frontend/src/container/OnboardingQuestionaire/OnboardingHeader/index.ts @@ -0,0 +1 @@ +export { OnboardingHeader } from './OnboardingHeader'; diff --git a/frontend/src/container/OnboardingQuestionaire/OnboardingQuestionaire.styles.scss b/frontend/src/container/OnboardingQuestionaire/OnboardingQuestionaire.styles.scss new file mode 100644 index 0000000000..df07b47f27 --- /dev/null +++ b/frontend/src/container/OnboardingQuestionaire/OnboardingQuestionaire.styles.scss @@ -0,0 +1,278 @@ +.onboarding-questionaire-container { + width: 100%; + display: flex; + margin: 0 auto; + align-items: center; + flex-direction: column; + height: 100vh; + max-width: 1176px; + + .onboarding-questionaire-header { + width: 100%; + display: flex; + justify-content: center; + align-items: center; + + height: 56px; + } + + .onboarding-questionaire-content { + height: calc(100vh - 56px - 60px); + width: 100%; + display: flex; + flex-direction: column; + overflow-y: auto; + + .questions-container { + color: var(--Vanilla-100, #fff); + font-family: Inter; + font-size: 24px; + font-style: normal; + font-weight: 600; + line-height: 32px; + max-width: 600px; + margin: 0 auto; + border-radius: 8px; + max-height: 100%; + } + + .title { + color: var(--Vanilla-100, #fff) !important; + font-size: 24px !important; + line-height: 32px !important; + margin-bottom: 8px !important; + } + + .sub-title { + color: var(--Vanilla-400, var(--Greyscale-Vanilla-400, #c0c1c3)) !important; + font-size: 14px !important; + font-style: normal; + font-weight: 400 !important; + line-height: 24px !important; + margin-top: 0px !important; + margin-bottom: 24px !important; + } + + .questions-form-container { + max-width: 600px; + width: 600px; + margin: 0 auto; + } + + .questions-form { + width: 100%; + display: flex; + min-height: 420px; + padding: 20px 24px 24px 24px; + flex-direction: column; + align-items: center; + gap: 24px; + border-radius: 4px; + border: 1px solid var(--Greyscale-Slate-500, #161922); + background: var(--Ink-400, #121317); + + .ant-form-item { + margin-bottom: 0px !important; + + .ant-form-item-label { + label { + color: var(--Vanilla-100, #fff) !important; + font-size: 13px !important; + font-weight: 500; + line-height: 20px; + } + } + } + } + + .invite-team-members-container { + display: flex; + width: 100%; + flex-direction: column; + gap: 12px; + + .ant-input-group { + width: 100%; + + .ant-input { + font-size: 12px; + + height: 32px; + background: var(--Ink-300, #16181d); + border: 1px solid var(--Greyscale-Slate-400, #1d212d); + color: var(--Vanilla-400, var(--Greyscale-Vanilla-400, #c0c1c3)); + } + + .ant-input-group-addon { + font-size: 11px; + height: 32px; + min-width: 80px; + background: var(--Ink-300, #16181d); + border: 1px solid var(--Greyscale-Slate-400, #1d212d); + border-left: 0px; + color: var(--Vanilla-400, var(--Greyscale-Vanilla-400, #c0c1c3)); + } + } + } + + .question-label { + color: var(--Vanilla-100, #fff); + font-size: 13px; + font-style: normal; + font-weight: 500; + line-height: 20px; + } + + .question-sub-label { + color: var(--Vanilla-400, var(--Greyscale-Vanilla-400, #c0c1c3)); + font-size: 11px; + font-style: normal; + font-weight: 400; + line-height: 16px; + } + + .next-prev-container { + display: flex; + justify-content: space-between; + align-items: center; + gap: 10px; + margin-bottom: 24px; + + .ant-btn { + flex: 1; + } + } + + .form-group { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + align-self: stretch; + } + + .slider-container { + width: 100%; + + .ant-slider .ant-slider-mark { + font-size: 10px; + } + } + + .do-later-container { + width: 100%; + display: flex; + justify-content: center; + align-items: center; + margin-top: 24px; + } + + .question { + color: var(--Vanilla-100, #fff); + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: 20px; + } + + input[type='text'] { + width: 100%; + padding: 12px; + border-radius: 2px; + font-size: 14px; + height: 40px; + border: 1px solid var(--Greyscale-Slate-400, #1d212d); + background: var(--Ink-300, #16181d); + color: #fff; + + &:focus-visible { + outline: none; + } + } + + .radio-group, + .grid, + .tool-grid { + display: flex; + align-items: flex-start; + align-content: flex-start; + gap: 10px; + align-self: stretch; + flex-wrap: wrap; + } + + .radio-button, + .grid-button, + .tool-button { + border-radius: 4px; + border: 1px solid var(--Greyscale-Slate-400, #1d212d); + background: var(--Ink-300, #16181d); + padding: 12px; + color: var(--Vanilla-400, var(--Greyscale-Vanilla-400, #c0c1c3)); + font-size: 14px; + font-style: normal; + text-align: left; + font-weight: 400; + transition: background-color 0.3s ease; + min-width: 258px; + cursor: pointer; + } + + .radio-button.active, + .grid-button.active, + .tool-button.active, + .radio-button:hover, + .grid-button:hover, + .tool-button:hover { + border: 1px solid rgba(78, 116, 248, 0.4); + background: rgba(78, 116, 248, 0.2); + } + + .tool-grid { + grid-template-columns: repeat(4, 1fr); + } + + .input-field { + flex: 0; + padding: 12px; + border: 1px solid var(--Greyscale-Slate-400, #1d212d); + background: var(--Ink-300, #16181d); + color: #fff; + border-radius: 4px; + font-size: 14px; + min-width: 258px; + } + + .next-button { + display: flex; + height: 40px; + padding: 8px 12px 8px 16px; + justify-content: center; + align-items: center; + gap: 6px; + align-self: stretch; + border: 0px; + border-radius: 50px; + margin-top: 24px; + cursor: pointer; + } + + .next-button.disabled { + opacity: 0.5; + cursor: not-allowed; + pointer-events: none; + } + + .arrow { + font-size: 18px; + color: #fff; + } + } + + .onboarding-questionaire-footer { + width: 100%; + height: 60px; + padding: 12px 24px; + box-sizing: border-box; + } +} diff --git a/frontend/src/container/OnboardingQuestionaire/OptimiseSignozNeeds/OptimiseSignozNeeds.tsx b/frontend/src/container/OnboardingQuestionaire/OptimiseSignozNeeds/OptimiseSignozNeeds.tsx new file mode 100644 index 0000000000..52f45ca0b5 --- /dev/null +++ b/frontend/src/container/OnboardingQuestionaire/OptimiseSignozNeeds/OptimiseSignozNeeds.tsx @@ -0,0 +1,127 @@ +import { Button, Slider, SliderSingleProps, Typography } from 'antd'; +import { ArrowLeft, ArrowRight, Minus } from 'lucide-react'; + +interface OptimiseSignozNeedsProps { + onNext: () => void; + onBack: () => void; +} + +const logMarks: SliderSingleProps['marks'] = { + 0: '2 GB', + 25: '25 GB', + 50: '50 GB', + 100: '100 GB', +}; + +const hostMarks: SliderSingleProps['marks'] = { + 0: '0', + 20: '20', + 40: '40', + 60: '60', + 80: '80', + 100: '100', +}; + +const serviceMarks: SliderSingleProps['marks'] = { + 0: '0', + 20: '20', + 40: '40', + 60: '60', + 80: '80', + 100: '100', +}; + +function OptimiseSignozNeeds({ + onNext, + onBack, +}: OptimiseSignozNeedsProps): JSX.Element { + return ( +
+ + Optimize SigNoz for Your Needs + + + Give us a quick sense of your scale so SigNoz can keep up! + + +
+
+ + What does your scale approximately look like? + + +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+
+ +
+ + + +
+ +
+ +
+
+
+ ); +} + +export default OptimiseSignozNeeds; diff --git a/frontend/src/container/OnboardingQuestionaire/OrgQuestions/OrgQuestions.styles.scss b/frontend/src/container/OnboardingQuestionaire/OrgQuestions/OrgQuestions.styles.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/src/container/OnboardingQuestionaire/OrgQuestions/OrgQuestions.tsx b/frontend/src/container/OnboardingQuestionaire/OrgQuestions/OrgQuestions.tsx new file mode 100644 index 0000000000..9988c9e97c --- /dev/null +++ b/frontend/src/container/OnboardingQuestionaire/OrgQuestions/OrgQuestions.tsx @@ -0,0 +1,257 @@ +/* eslint-disable sonarjs/cognitive-complexity */ +import '../OnboardingQuestionaire.styles.scss'; + +import { Button, Typography } from 'antd'; +import { ArrowRight } from 'lucide-react'; +import { useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import AppReducer from 'types/reducer/app'; + +interface OrgQuestionsProps { + onNext: () => void; +} + +function OrgQuestions({ onNext }: OrgQuestionsProps): JSX.Element { + const [organisationName, setOrganisationName] = useState(''); + const [usesObservability, setUsesObservability] = useState( + null, + ); + const [observabilityTool, setObservabilityTool] = useState( + null, + ); + const [otherTool, setOtherTool] = useState(''); + const [familiarity, setFamiliarity] = useState(null); + const [isNextDisabled, setIsNextDisabled] = useState(true); + + const { user } = useSelector((state) => state.app); + + useEffect(() => { + if ( + organisationName !== '' && + usesObservability !== null && + familiarity !== null && + (observabilityTool !== 'Others' || (usesObservability && otherTool !== '')) + ) { + setIsNextDisabled(false); + } else { + setIsNextDisabled(true); + } + }, [ + organisationName, + usesObservability, + familiarity, + observabilityTool, + otherTool, + ]); + + return ( +
+ + Welcome, {user?.name}! + + + We'll help you get the most out of SigNoz, whether you're new to + observability or a seasoned pro. + + +
+
+
+ + setOrganisationName(e.target.value)} + /> +
+ +
+ + +
+ + +
+
+ + {usesObservability && ( +
+ +
+ + + + + + + + + {observabilityTool === 'Others' ? ( + setOtherTool(e.target.value)} + /> + ) : ( + + )} +
+
+ )} + +
+
+ Are you familiar with observability (o11y)? +
+
+ + + + +
+
+
+ +
+ +
+
+
+ ); +} + +export default OrgQuestions; diff --git a/frontend/src/container/OnboardingQuestionaire/index.tsx b/frontend/src/container/OnboardingQuestionaire/index.tsx new file mode 100644 index 0000000000..719da996fc --- /dev/null +++ b/frontend/src/container/OnboardingQuestionaire/index.tsx @@ -0,0 +1,55 @@ +import './OnboardingQuestionaire.styles.scss'; + +import { useState } from 'react'; + +import { AboutSigNozQuestions } from './AboutSigNozQuestions/AboutSigNozQuestions'; +import InviteTeamMembers from './InviteTeamMembers/InviteTeamMembers'; +import { OnboardingFooter } from './OnboardingFooter/OnboardingFooter'; +import { OnboardingHeader } from './OnboardingHeader/OnboardingHeader'; +import OptimiseSignozNeeds from './OptimiseSignozNeeds/OptimiseSignozNeeds'; +import OrgQuestions from './OrgQuestions/OrgQuestions'; + +function OnboardingQuestionaire(): JSX.Element { + const [currentStep, setCurrentStep] = useState(1); + + return ( +
+
+ +
+ +
+ {currentStep === 1 && ( + setCurrentStep(2)} /> + )} + + {currentStep === 2 && ( + setCurrentStep(1)} + onNext={(): void => setCurrentStep(3)} + /> + )} + + {currentStep === 3 && ( + setCurrentStep(2)} + onNext={(): void => setCurrentStep(4)} + /> + )} + + {currentStep === 4 && ( + setCurrentStep(3)} + onNext={(): void => setCurrentStep(5)} + /> + )} +
+ +
+ +
+
+ ); +} + +export default OnboardingQuestionaire; diff --git a/frontend/src/pages/OnboardingPageV2/OnboardingPageV2.tsx b/frontend/src/pages/OnboardingPageV2/OnboardingPageV2.tsx index ccdc0226f8..7100894a41 100644 --- a/frontend/src/pages/OnboardingPageV2/OnboardingPageV2.tsx +++ b/frontend/src/pages/OnboardingPageV2/OnboardingPageV2.tsx @@ -1,9 +1,9 @@ -import { Typography } from 'antd'; +import OnboardingQuestionaire from 'container/OnboardingQuestionaire'; function OnboardingPageV2(): JSX.Element { return (
- Onboarding V2 +
); } From 465fdcdf2a8d4374c0d2b1ea259b64b225f57e27 Mon Sep 17 00:00:00 2001 From: Yunus M Date: Fri, 20 Sep 2024 01:51:46 +0530 Subject: [PATCH 03/15] feat: maintain state and add log events --- .../AboutSigNozQuestions.tsx | 238 +++++++++-------- .../InviteTeamMembers/InviteTeamMembers.tsx | 94 +++++-- .../OnboardingQuestionaire.styles.scss | 64 +++++ .../OptimiseSignozNeeds.tsx | 81 +++++- .../OrgQuestions/OrgQuestions.tsx | 243 +++++++++--------- .../OnboardingQuestionaire/index.tsx | 26 +- 6 files changed, 499 insertions(+), 247 deletions(-) diff --git a/frontend/src/container/OnboardingQuestionaire/AboutSigNozQuestions/AboutSigNozQuestions.tsx b/frontend/src/container/OnboardingQuestionaire/AboutSigNozQuestions/AboutSigNozQuestions.tsx index 09ca867e5f..c573d552fe 100644 --- a/frontend/src/container/OnboardingQuestionaire/AboutSigNozQuestions/AboutSigNozQuestions.tsx +++ b/frontend/src/container/OnboardingQuestionaire/AboutSigNozQuestions/AboutSigNozQuestions.tsx @@ -1,23 +1,52 @@ /* eslint-disable sonarjs/cognitive-complexity */ import '../OnboardingQuestionaire.styles.scss'; -import { Button, Typography } from 'antd'; -import { ArrowLeft, ArrowRight } from 'lucide-react'; +import { Color } from '@signozhq/design-tokens'; +import { Button, Input, Typography } from 'antd'; +import logEvent from 'api/common/logEvent'; +import { ArrowLeft, ArrowRight, CheckCircle } from 'lucide-react'; import { useEffect, useState } from 'react'; interface AboutSigNozQuestionsProps { + signozDetails: any; + setSignozDetails: (details: any) => void; onNext: () => void; onBack: () => void; } +const hearAboutSignozOptions: Record = { + blog: 'Blog', + hackerNews: 'Hacker News', + linkedin: 'LinkedIn', + twitter: 'Twitter', + reddit: 'Reddit', + colleaguesFriends: 'Colleagues / Friends', +}; + +const interestedInOptions: Record = { + savingCosts: 'Saving costs', + otelNativeStack: 'Interested in Otel-native stack', + allInOne: 'All in one', +}; + export function AboutSigNozQuestions({ + signozDetails, + setSignozDetails, onNext, onBack, }: AboutSigNozQuestionsProps): JSX.Element { - const [hearAboutSignoz, setHearAboutSignoz] = useState(null); - const [otherAboutSignoz, setOtherAboutSignoz] = useState(''); - const [interestedSignoz, setInterestedSignoz] = useState(null); - const [otherInterest, setOtherInterest] = useState(''); + const [hearAboutSignoz, setHearAboutSignoz] = useState( + signozDetails?.hearAboutSignoz || null, + ); + const [otherAboutSignoz, setOtherAboutSignoz] = useState( + signozDetails?.otherAboutSignoz || '', + ); + const [interestedSignoz, setInterestedSignoz] = useState( + signozDetails?.interestedSignoz || null, + ); + const [otherInterest, setOtherInterest] = useState( + signozDetails?.otherInterest || '', + ); const [isNextDisabled, setIsNextDisabled] = useState(true); useEffect((): void => { @@ -33,6 +62,42 @@ export function AboutSigNozQuestions({ } }, [hearAboutSignoz, otherAboutSignoz, interestedSignoz, otherInterest]); + const handleOnNext = (): void => { + setSignozDetails({ + hearAboutSignoz, + otherAboutSignoz, + interestedSignoz, + otherInterest, + }); + + logEvent('Onboarding: SigNoz Questions: Next', { + hearAboutSignoz, + otherAboutSignoz, + interestedSignoz, + otherInterest, + }); + + onNext(); + }; + + const handleOnBack = (): void => { + setSignozDetails({ + hearAboutSignoz, + otherAboutSignoz, + interestedSignoz, + otherInterest, + }); + + logEvent('Onboarding: SigNoz Questions: Back', { + hearAboutSignoz, + otherAboutSignoz, + interestedSignoz, + otherInterest, + }); + + onBack(); + }; + return (
@@ -46,78 +111,49 @@ export function AboutSigNozQuestions({
Where did you hear about SigNoz?
-
- - - - - - +
+ {Object.keys(hearAboutSignozOptions).map((option: string) => ( + + ))} {hearAboutSignoz === 'Others' ? ( - + ) : ( + '' + ) + } onChange={(e): void => setOtherAboutSignoz(e.target.value)} /> ) : ( - + )}
@@ -126,62 +162,56 @@ export function AboutSigNozQuestions({
What are you interested in doing with SigNoz?
-
- - - +
+ {Object.keys(interestedInOptions).map((option: string) => ( + + ))} {interestedSignoz === 'Others' ? ( - + ) : ( + '' + ) + } onChange={(e): void => setOtherInterest(e.target.value)} /> ) : ( - + )}
- @@ -189,7 +219,7 @@ export function AboutSigNozQuestions({
@@ -63,7 +127,7 @@ function InviteTeamMembers({ Back - diff --git a/frontend/src/container/OnboardingQuestionaire/OnboardingQuestionaire.styles.scss b/frontend/src/container/OnboardingQuestionaire/OnboardingQuestionaire.styles.scss index df07b47f27..8be4dc608c 100644 --- a/frontend/src/container/OnboardingQuestionaire/OnboardingQuestionaire.styles.scss +++ b/frontend/src/container/OnboardingQuestionaire/OnboardingQuestionaire.styles.scss @@ -216,6 +216,7 @@ transition: background-color 0.3s ease; min-width: 258px; cursor: pointer; + box-sizing: border-box; } .radio-button.active, @@ -228,6 +229,61 @@ background: rgba(78, 116, 248, 0.2); } + .two-column-grid { + width: 100%; + display: grid; + grid-template-columns: 1fr 1fr; /* Two equal columns */ + gap: 10px; + } + + .onboarding-questionaire-button, + .add-another-member-button { + display: flex; + align-items: center; + justify-content: space-between; + border-radius: 2px; + border: 1px solid var(--Greyscale-Slate-400, #1d212d); + background: var(--Ink-300, #16181d); + color: var(--Vanilla-400, var(--Greyscale-Vanilla-400, #c0c1c3)); + box-shadow: none; + font-size: 14px; + font-style: normal; + text-align: left; + font-weight: 400; + transition: background-color 0.3s ease; + cursor: pointer; + height: 40px; + box-sizing: border-box; + + &:hover { + border: 1px solid rgba(78, 116, 248, 0.4); + background: rgba(78, 116, 248, 0.2); + } + + &:focus-visible { + outline: none; + } + + &.active { + border: 1px solid rgba(78, 116, 248, 0.4); + background: rgba(78, 116, 248, 0.2); + } + } + + .add-another-member-button { + font-size: 12px; + height: 32px; + } + + .onboarding-questionaire-other-input { + .ant-input-group { + .ant-input { + border-top-right-radius: 0px !important; + border-bottom-right-radius: 0px !important; + } + } + } + .tool-grid { grid-template-columns: repeat(4, 1fr); } @@ -275,4 +331,12 @@ padding: 12px 24px; box-sizing: border-box; } + + .invite-team-members-add-another-member-container { + width: 100%; + display: flex; + justify-content: flex-end; + align-items: center; + margin-top: 12px; + } } diff --git a/frontend/src/container/OnboardingQuestionaire/OptimiseSignozNeeds/OptimiseSignozNeeds.tsx b/frontend/src/container/OnboardingQuestionaire/OptimiseSignozNeeds/OptimiseSignozNeeds.tsx index 52f45ca0b5..107b1830ef 100644 --- a/frontend/src/container/OnboardingQuestionaire/OptimiseSignozNeeds/OptimiseSignozNeeds.tsx +++ b/frontend/src/container/OnboardingQuestionaire/OptimiseSignozNeeds/OptimiseSignozNeeds.tsx @@ -1,7 +1,11 @@ import { Button, Slider, SliderSingleProps, Typography } from 'antd'; +import logEvent from 'api/common/logEvent'; import { ArrowLeft, ArrowRight, Minus } from 'lucide-react'; +import { useState } from 'react'; interface OptimiseSignozNeedsProps { + optimiseSignozDetails: Record | null; + setOptimiseSignozDetails: (details: Record | null) => void; onNext: () => void; onBack: () => void; } @@ -32,9 +36,69 @@ const serviceMarks: SliderSingleProps['marks'] = { }; function OptimiseSignozNeeds({ + optimiseSignozDetails, + setOptimiseSignozDetails, onNext, onBack, }: OptimiseSignozNeedsProps): JSX.Element { + const [logsPerDay, setLogsPerDay] = useState( + optimiseSignozDetails?.logsPerDay || 25, + ); + const [hostsPerDay, setHostsPerDay] = useState( + optimiseSignozDetails?.hostsPerDay || 40, + ); + const [services, setServices] = useState( + optimiseSignozDetails?.services || 10, + ); + + const handleOnNext = (): void => { + setOptimiseSignozDetails({ + logsPerDay, + hostsPerDay, + services, + }); + + logEvent('Onboarding: Optimise SigNoz Needs: Next', { + logsPerDay, + hostsPerDay, + services, + }); + + onNext(); + }; + + const handleOnBack = (): void => { + setOptimiseSignozDetails({ + logsPerDay, + hostsPerDay, + services, + }); + + logEvent('Onboarding: Optimise SigNoz Needs: Back', { + logsPerDay, + hostsPerDay, + services, + }); + + onBack(); + }; + + const handleWillDoLater = (): void => { + setOptimiseSignozDetails({ + logsPerDay: 0, + hostsPerDay: 0, + services: 0, + }); + + logEvent('Onboarding: Optimise SigNoz Needs: Will do later', { + logsPerDay: 0, + hostsPerDay: 0, + services: 0, + }); + + onNext(); + }; + return (
@@ -57,7 +121,8 @@ function OptimiseSignozNeeds({
setLogsPerDay(value)} styles={{ track: { background: '#4E74F8', @@ -69,12 +134,13 @@ function OptimiseSignozNeeds({
setHostsPerDay(value)} styles={{ track: { background: '#4E74F8', @@ -91,7 +157,8 @@ function OptimiseSignozNeeds({
setServices(value)} styles={{ track: { background: '#4E74F8', @@ -103,19 +170,19 @@ function OptimiseSignozNeeds({
- -
-
diff --git a/frontend/src/container/OnboardingQuestionaire/OrgQuestions/OrgQuestions.tsx b/frontend/src/container/OnboardingQuestionaire/OrgQuestions/OrgQuestions.tsx index 9988c9e97c..6cd679c5da 100644 --- a/frontend/src/container/OnboardingQuestionaire/OrgQuestions/OrgQuestions.tsx +++ b/frontend/src/container/OnboardingQuestionaire/OrgQuestions/OrgQuestions.tsx @@ -1,27 +1,58 @@ /* eslint-disable sonarjs/cognitive-complexity */ import '../OnboardingQuestionaire.styles.scss'; -import { Button, Typography } from 'antd'; -import { ArrowRight } from 'lucide-react'; +import { Color } from '@signozhq/design-tokens'; +import { Button, Input, Typography } from 'antd'; +import logEvent from 'api/common/logEvent'; +import { ArrowRight, CheckCircle } from 'lucide-react'; import { useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; import AppReducer from 'types/reducer/app'; interface OrgQuestionsProps { + orgDetails: any; + setOrgDetails: (details: any) => void; onNext: () => void; } -function OrgQuestions({ onNext }: OrgQuestionsProps): JSX.Element { - const [organisationName, setOrganisationName] = useState(''); +const observabilityTools = [ + 'AWS Cloudwatch', + 'DataDog', + 'New Relic', + 'Grafana / Prometheus', + 'Azure App Monitor', + 'GCP-native o11y tools', + 'Honeycomb', +]; + +const o11yFamiliarityOptions: Record = { + new: "I'm completely new", + builtStack: "I've built a stack before", + experienced: 'I have some experience', + dontKnow: "I don't know what it is", +}; + +function OrgQuestions({ + orgDetails, + setOrgDetails, + onNext, +}: OrgQuestionsProps): JSX.Element { + const [organisationName, setOrganisationName] = useState( + orgDetails?.organisationName || '', + ); const [usesObservability, setUsesObservability] = useState( - null, + orgDetails?.usesObservability || null, ); const [observabilityTool, setObservabilityTool] = useState( - null, + orgDetails?.observabilityTool || null, + ); + const [otherTool, setOtherTool] = useState( + orgDetails?.otherTool || '', + ); + const [familiarity, setFamiliarity] = useState( + orgDetails?.familiarity || null, ); - const [otherTool, setOtherTool] = useState(''); - const [familiarity, setFamiliarity] = useState(null); const [isNextDisabled, setIsNextDisabled] = useState(true); const { user } = useSelector((state) => state.app); @@ -45,6 +76,26 @@ function OrgQuestions({ onNext }: OrgQuestionsProps): JSX.Element { otherTool, ]); + const handleOnNext = (): void => { + setOrgDetails({ + organisationName, + usesObservability, + observabilityTool, + otherTool, + familiarity, + }); + + logEvent('Onboarding: Org Questions: Next', { + organisationName, + usesObservability, + observabilityTool, + otherTool, + familiarity, + }); + + onNext(); + }; + return (
@@ -77,20 +128,25 @@ function OrgQuestions({ onNext }: OrgQuestionsProps): JSX.Element { Do you currently use any observability/monitoring tool? -
- - + + No{' '} + {usesObservability === false && ( + + )} +
@@ -109,83 +168,44 @@ function OrgQuestions({ onNext }: OrgQuestionsProps): JSX.Element { -
- - - - - - - +
+ {observabilityTools.map((tool) => ( + + ))} {observabilityTool === 'Others' ? ( - + ) : ( + '' + ) + } onChange={(e): void => setOtherTool(e.target.value)} /> ) : ( - - - +
+ {Object.keys(o11yFamiliarityOptions).map((option: string) => ( + + ))}
@@ -242,7 +245,7 @@ function OrgQuestions({ onNext }: OrgQuestionsProps): JSX.Element { ))} - {interestedSignoz === 'Others' ? ( + {interestInSignoz === 'Others' ? ( ) : ( '' ) } - onChange={(e): void => setOtherInterest(e.target.value)} + onChange={(e): void => setOtherInterestInSignoz(e.target.value)} /> ) : ( diff --git a/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.styles.scss b/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.styles.scss new file mode 100644 index 0000000000..aae10276f6 --- /dev/null +++ b/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.styles.scss @@ -0,0 +1,46 @@ +.team-member-container { + display: flex; + align-items: center; + + .team-member-role-select { + width: 20%; + + .ant-select-selector { + border: 1px solid #1d212d; + border-top-right-radius: 0px; + border-bottom-right-radius: 0px; + + border-right: none; + } + } + + .team-member-email-input { + width: 80%; + + border: 1px solid #1d212d; + border-top-left-radius: 0px; + border-bottom-left-radius: 0px; + } +} + +.questions-form-container { + .error-message-container { + padding: 16px; + margin-top: 16px; + border-radius: 4px; + border: 1px solid var(--bg-slate-500, #161922); + background: var(--bg-ink-400, #121317); + width: 100%; + display: flex; + align-items: center; + + .error-message { + font-size: 12px; + font-weight: 400; + + display: flex; + align-items: center; + gap: 8px; + } + } +} diff --git a/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.tsx b/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.tsx index 6eeb2c5e6f..cd5f0d53c4 100644 --- a/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.tsx +++ b/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.tsx @@ -1,5 +1,8 @@ +import './InviteTeamMembers.styles.scss'; + import { Color } from '@signozhq/design-tokens'; import { Button, Input, Select, Typography } from 'antd'; +import { cloneDeep, debounce, isEmpty } from 'lodash-es'; import { ArrowLeft, ArrowRight, @@ -7,55 +10,123 @@ import { Plus, TriangleAlert, } from 'lucide-react'; -import { useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; +import { v4 as uuid } from 'uuid'; + +interface TeamMember { + email: string; + role: string; + name: string; + frontendBaseUrl: string; + id: string; +} interface InviteTeamMembersProps { - teamMembers: string[]; - setTeamMembers: (teamMembers: string[]) => void; + teamMembers: TeamMember[] | null; + setTeamMembers: (teamMembers: TeamMember[]) => void; onNext: () => void; onBack: () => void; } -const userRolesOptions = ( - -); - function InviteTeamMembers({ teamMembers, setTeamMembers, onNext, onBack, }: InviteTeamMembersProps): JSX.Element { - const [teamMembersToInvite, setTeamMembersToInvite] = useState( - teamMembers || [''], + const [teamMembersToInvite, setTeamMembersToInvite] = useState< + TeamMember[] | null + >(teamMembers); + + const [emailValidity, setEmailValidity] = useState>( + {}, ); + const [hasInvalidEmails, setHasInvalidEmails] = useState(false); + + const defaultTeamMember: TeamMember = { + email: '', + role: 'EDITOR', + name: '', + frontendBaseUrl: '', + id: '', + }; + + useEffect(() => { + if (isEmpty(teamMembers)) { + const teamMember = { + ...defaultTeamMember, + id: uuid(), + }; + + setTeamMembersToInvite([teamMember]); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [teamMembers]); + const handleAddTeamMember = (): void => { - setTeamMembersToInvite([...teamMembersToInvite, '']); + const newTeamMember = { ...defaultTeamMember, id: uuid() }; + setTeamMembersToInvite((prev) => [...(prev || []), newTeamMember]); + }; + + // Validation function to check all users + const validateAllUsers = (): boolean => { + let isValid = true; + + const updatedValidity: Record = {}; + + teamMembersToInvite?.forEach((member) => { + const emailValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(member.email); + if (!emailValid || !member.email) { + isValid = false; + setHasInvalidEmails(true); + } + updatedValidity[member.id!] = emailValid; + }); + + setEmailValidity(updatedValidity); + + return isValid; }; const handleNext = (): void => { - console.log(teamMembersToInvite); - setTeamMembers(teamMembersToInvite); - onNext(); + if (validateAllUsers()) { + setTeamMembers(teamMembersToInvite || []); + onNext(); + } }; - const handleOnChange = ( + // eslint-disable-next-line react-hooks/exhaustive-deps + const debouncedValidateEmail = useCallback( + debounce((email: string, memberId: string) => { + const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); + setEmailValidity((prev) => ({ ...prev, [memberId]: isValid })); + }, 500), + [], + ); + + const handleEmailChange = ( e: React.ChangeEvent, - index: number, + member: TeamMember, ): void => { - const newTeamMembers = [...teamMembersToInvite]; - newTeamMembers[index] = e.target.value; - setTeamMembersToInvite(newTeamMembers); + const { value } = e.target; + const updatedMembers = cloneDeep(teamMembersToInvite || []); + + const memberToUpdate = updatedMembers.find((m) => m.id === member.id); + if (memberToUpdate) { + memberToUpdate.email = value; + setTeamMembersToInvite(updatedMembers); + debouncedValidateEmail(value, member.id!); + } }; - const isValidEmail = (email: string): boolean => { - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - return emailRegex.test(email); + const handleRoleChange = (role: string, member: TeamMember): void => { + const updatedMembers = cloneDeep(teamMembersToInvite || []); + const memberToUpdate = updatedMembers.find((m) => m.id === member.id); + if (memberToUpdate) { + memberToUpdate.role = role; + setTeamMembersToInvite(updatedMembers); + } }; return ( @@ -79,29 +150,37 @@ function InviteTeamMembers({
- {teamMembersToInvite.map((member, index) => ( - // eslint-disable-next-line react/no-array-index-key -
+ {teamMembersToInvite?.map((member) => ( +
+ 0 ? ( - isValidEmail(member) ? ( - - ) : ( - - ) - ) : null - } placeholder="your-teammate@org.com" - value={member} + value={member.email} type="email" required autoFocus autoComplete="off" + className="team-member-email-input" onChange={(e: React.ChangeEvent): void => - handleOnChange(e, index) + handleEmailChange(e, member) + } + addonAfter={ + // eslint-disable-next-line no-nested-ternary + emailValidity[member.id!] === undefined ? null : emailValidity[ + member.id! + ] ? ( + + ) : ( + + ) } />
@@ -121,6 +200,15 @@ function InviteTeamMembers({
+ {hasInvalidEmails && ( +
+ + Please enter valid emails for all team + members + +
+ )} +
- -
diff --git a/frontend/src/container/OnboardingQuestionaire/OrgQuestions/OrgQuestions.tsx b/frontend/src/container/OnboardingQuestionaire/OrgQuestions/OrgQuestions.tsx index 6cd679c5da..a1fea0aaf8 100644 --- a/frontend/src/container/OnboardingQuestionaire/OrgQuestions/OrgQuestions.tsx +++ b/frontend/src/container/OnboardingQuestionaire/OrgQuestions/OrgQuestions.tsx @@ -4,27 +4,42 @@ import '../OnboardingQuestionaire.styles.scss'; import { Color } from '@signozhq/design-tokens'; import { Button, Input, Typography } from 'antd'; import logEvent from 'api/common/logEvent'; -import { ArrowRight, CheckCircle } from 'lucide-react'; +import { ArrowRight, CheckCircle, Loader2 } from 'lucide-react'; import { useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; import AppReducer from 'types/reducer/app'; +export interface OrgData { + id: string; + isAnonymous: boolean; + name: string; +} + +export interface OrgDetails { + organisationName: string; + usesObservability: boolean | null; + observabilityTool: string | null; + otherTool: string | null; + familiarity: string | null; +} + interface OrgQuestionsProps { - orgDetails: any; - setOrgDetails: (details: any) => void; + isLoading: boolean; + orgDetails: OrgDetails; + setOrgDetails: (details: OrgDetails) => void; onNext: () => void; } -const observabilityTools = [ - 'AWS Cloudwatch', - 'DataDog', - 'New Relic', - 'Grafana / Prometheus', - 'Azure App Monitor', - 'GCP-native o11y tools', - 'Honeycomb', -]; +const observabilityTools = { + AWSCloudwatch: 'AWS Cloudwatch', + DataDog: 'DataDog', + NewRelic: 'New Relic', + GrafanaPrometheus: 'Grafana / Prometheus', + AzureAppMonitor: 'Azure App Monitor', + GCPNativeO11yTools: 'GCP-native o11y tools', + Honeycomb: 'Honeycomb', +}; const o11yFamiliarityOptions: Record = { new: "I'm completely new", @@ -34,10 +49,13 @@ const o11yFamiliarityOptions: Record = { }; function OrgQuestions({ + isLoading, orgDetails, setOrgDetails, onNext, }: OrgQuestionsProps): JSX.Element { + const { user } = useSelector((state) => state.app); + const [organisationName, setOrganisationName] = useState( orgDetails?.organisationName || '', ); @@ -55,19 +73,36 @@ function OrgQuestions({ ); const [isNextDisabled, setIsNextDisabled] = useState(true); - const { user } = useSelector((state) => state.app); + useEffect(() => { + setOrganisationName(orgDetails.organisationName); + }, [orgDetails.organisationName]); + + const isValidUsesObservability = (): boolean => { + if (usesObservability === null) { + return false; + } + + if (usesObservability && (!observabilityTool || observabilityTool === '')) { + return false; + } + + // eslint-disable-next-line sonarjs/prefer-single-boolean-return + if (usesObservability && observabilityTool === 'Others' && otherTool === '') { + return false; + } + + return true; + }; useEffect(() => { - if ( - organisationName !== '' && - usesObservability !== null && - familiarity !== null && - (observabilityTool !== 'Others' || (usesObservability && otherTool !== '')) - ) { + const isValidObservability = isValidUsesObservability(); + + if (organisationName !== '' && familiarity !== null && isValidObservability) { setIsNextDisabled(false); } else { setIsNextDisabled(true); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ organisationName, usesObservability, @@ -115,7 +150,7 @@ function OrgQuestions({
- {observabilityTools.map((tool) => ( + {Object.keys(observabilityTools).map((tool) => (
diff --git a/frontend/src/container/OnboardingQuestionaire/index.tsx b/frontend/src/container/OnboardingQuestionaire/index.tsx index bb9e88d0d4..39e34e1f82 100644 --- a/frontend/src/container/OnboardingQuestionaire/index.tsx +++ b/frontend/src/container/OnboardingQuestionaire/index.tsx @@ -1,30 +1,194 @@ import './OnboardingQuestionaire.styles.scss'; -import { useState } from 'react'; +import { NotificationInstance } from 'antd/es/notification/interface'; +import updateProfileAPI from 'api/onboarding/updateProfile'; +import editOrg from 'api/user/editOrg'; +import { AxiosError } from 'axios'; +import { SOMETHING_WENT_WRONG } from 'constants/api'; +import { InviteTeamMembersProps } from 'container/OrganizationSettings/PendingInvitesContainer'; +import { useNotifications } from 'hooks/useNotifications'; +import history from 'lib/history'; +import { Dispatch, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useMutation } from 'react-query'; +import { useDispatch, useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import AppActions from 'types/actions'; +import { UPDATE_ORG_NAME } from 'types/actions/app'; +import AppReducer from 'types/reducer/app'; -import { AboutSigNozQuestions } from './AboutSigNozQuestions/AboutSigNozQuestions'; +import { + AboutSigNozQuestions, + SignozDetails, +} from './AboutSigNozQuestions/AboutSigNozQuestions'; import InviteTeamMembers from './InviteTeamMembers/InviteTeamMembers'; import { OnboardingFooter } from './OnboardingFooter/OnboardingFooter'; import { OnboardingHeader } from './OnboardingHeader/OnboardingHeader'; -import OptimiseSignozNeeds from './OptimiseSignozNeeds/OptimiseSignozNeeds'; -import OrgQuestions from './OrgQuestions/OrgQuestions'; +import OptimiseSignozNeeds, { + OptimiseSignozDetails, +} from './OptimiseSignozNeeds/OptimiseSignozNeeds'; +import OrgQuestions, { OrgData, OrgDetails } from './OrgQuestions/OrgQuestions'; + +export const showErrorNotification = ( + notifications: NotificationInstance, + err: Error, +): void => { + notifications.error({ + message: err.message || SOMETHING_WENT_WRONG, + }); +}; + +const INITIAL_ORG_DETAILS: OrgDetails = { + organisationName: '', + usesObservability: true, + observabilityTool: '', + otherTool: '', + familiarity: '', +}; + +const INITIAL_SIGNOZ_DETAILS: SignozDetails = { + hearAboutSignoz: '', + interestInSignoz: '', + otherInterestInSignoz: '', + otherAboutSignoz: '', +}; + +const INITIAL_OPTIMISE_SIGNOZ_DETAILS: OptimiseSignozDetails = { + logsPerDay: 25, + hostsPerDay: 40, + services: 10, +}; function OnboardingQuestionaire(): JSX.Element { + const { notifications } = useNotifications(); + const [currentStep, setCurrentStep] = useState(1); + const [orgDetails, setOrgDetails] = useState(INITIAL_ORG_DETAILS); + const [signozDetails, setSignozDetails] = useState( + INITIAL_SIGNOZ_DETAILS, + ); + const [ + optimiseSignozDetails, + setOptimiseSignozDetails, + ] = useState(INITIAL_OPTIMISE_SIGNOZ_DETAILS); + const [teamMembers, setTeamMembers] = useState< + InviteTeamMembersProps[] | null + >(null); + + const { t } = useTranslation(['organizationsettings', 'common']); + const { org } = useSelector((state) => state.app); + const dispatch = useDispatch>(); + const [orgData, setOrgData] = useState(null); + + useEffect(() => { + if (org) { + setOrgData(org[0]); + + setOrgDetails({ + ...orgDetails, + organisationName: org[0].name, + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [org]); + + const [isLoading, setIsLoading] = useState(false); - const [orgDetails, setOrgDetails] = useState | null>( - null, + const handleOrgNameUpdate = async (): Promise => { + /* Early bailout if orgData is not set or if the organisation name is not set or if the organisation name is empty or if the organisation name is the same as the one in the orgData */ + if ( + !orgData || + !orgDetails.organisationName || + orgDetails.organisationName === '' || + orgData.name === orgDetails.organisationName + ) { + setCurrentStep(2); + + return; + } + + try { + setIsLoading(true); + const { statusCode, error } = await editOrg({ + isAnonymous: orgData?.isAnonymous, + name: orgDetails.organisationName, + orgId: orgData?.id, + }); + if (statusCode === 200) { + dispatch({ + type: UPDATE_ORG_NAME, + payload: { + orgId: orgData?.id, + name: orgDetails.organisationName, + }, + }); + + setCurrentStep(2); + } else { + notifications.error({ + message: + error || + t('something_went_wrong', { + ns: 'common', + }), + }); + } + setIsLoading(false); + } catch (error) { + setIsLoading(false); + notifications.error({ + message: t('something_went_wrong', { + ns: 'common', + }), + }); + } + }; + + const handleOrgDetailsUpdate = (): void => { + handleOrgNameUpdate(); + }; + + const { mutate: updateProfile, isLoading: isUpdatingProfile } = useMutation( + updateProfileAPI, + { + onSuccess: (data) => { + console.log('data', data); + + setCurrentStep(4); + }, + onError: (error) => { + showErrorNotification(notifications, error as AxiosError); + }, + }, ); - const [signozDetails, setSignozDetails] = useState | null>(null); - const [optimiseSignozDetails, setOptimiseSignozDetails] = useState | null>(null); - const [teamMembers, setTeamMembers] = useState(['']); + const handleUpdateProfile = (): void => { + updateProfile({ + familiarity_with_observability: orgDetails?.familiarity as string, + has_existing_observability_tool: orgDetails?.usesObservability as boolean, + existing_observability_tool: + orgDetails?.observabilityTool === 'Others' + ? (orgDetails?.otherTool as string) + : (orgDetails?.observabilityTool as string), + + reasons_for_interest_in_signoz: + signozDetails?.interestInSignoz === 'Others' + ? (signozDetails?.otherInterestInSignoz as string) + : (signozDetails?.interestInSignoz as string), + where_did_you_hear_about_signoz: + signozDetails?.hearAboutSignoz === 'Others' + ? (signozDetails?.otherAboutSignoz as string) + : (signozDetails?.hearAboutSignoz as string), + + logs_scale_per_day_in_gb: optimiseSignozDetails?.logsPerDay as number, + number_of_hosts: optimiseSignozDetails?.hostsPerDay as number, + number_of_services: optimiseSignozDetails?.services as number, + }); + }; + + const handleOnboardingComplete = (): void => { + history.push('/'); + }; return (
@@ -35,9 +199,10 @@ function OnboardingQuestionaire(): JSX.Element {
{currentStep === 1 && ( setCurrentStep(2)} + onNext={handleOrgDetailsUpdate} /> )} @@ -52,10 +217,11 @@ function OnboardingQuestionaire(): JSX.Element { {currentStep === 3 && ( setCurrentStep(2)} - onNext={(): void => setCurrentStep(4)} + onNext={handleUpdateProfile} /> )} @@ -64,7 +230,7 @@ function OnboardingQuestionaire(): JSX.Element { teamMembers={teamMembers} setTeamMembers={setTeamMembers} onBack={(): void => setCurrentStep(3)} - onNext={(): void => setCurrentStep(5)} + onNext={handleOnboardingComplete} /> )}
diff --git a/frontend/src/container/OrganizationSettings/PendingInvitesContainer/index.tsx b/frontend/src/container/OrganizationSettings/PendingInvitesContainer/index.tsx index 7c4909ab8d..07c1d8f219 100644 --- a/frontend/src/container/OrganizationSettings/PendingInvitesContainer/index.tsx +++ b/frontend/src/container/OrganizationSettings/PendingInvitesContainer/index.tsx @@ -236,7 +236,9 @@ function PendingInvitesContainer(): JSX.Element { export interface InviteTeamMembersProps { email: string; name: string; - role: ROLES; + role: string; + id: string; + frontendBaseUrl: string; } interface DataProps { diff --git a/frontend/src/types/api/onboarding/types.ts b/frontend/src/types/api/onboarding/types.ts new file mode 100644 index 0000000000..046b7df18d --- /dev/null +++ b/frontend/src/types/api/onboarding/types.ts @@ -0,0 +1,10 @@ +export interface UpdateProfileProps { + reasons_for_interest_in_signoz: string; + familiarity_with_observability: string; + has_existing_observability_tool: boolean; + existing_observability_tool: string; + logs_scale_per_day_in_gb: number; + number_of_services: number; + number_of_hosts: number; + where_did_you_hear_about_signoz: string; +} diff --git a/frontend/src/types/api/user/inviteUsers.ts b/frontend/src/types/api/user/inviteUsers.ts new file mode 100644 index 0000000000..cf12269e73 --- /dev/null +++ b/frontend/src/types/api/user/inviteUsers.ts @@ -0,0 +1,17 @@ +import { User } from 'types/reducer/app'; +import { ROLES } from 'types/roles'; + +export interface UserProps { + name: User['name']; + email: User['email']; + role: ROLES; + frontendBaseUrl: string; +} + +export interface UsersProps { + users: UserProps[]; +} + +export interface PayloadProps { + data: string; +} From fa0715572240ea6a387bd9a5c89f0e4469841267 Mon Sep 17 00:00:00 2001 From: Yunus M Date: Wed, 23 Oct 2024 20:08:23 +0530 Subject: [PATCH 05/15] feat: handle errors for profiles and invite users api --- frontend/public/locales/en/titles.json | 1 + frontend/src/api/onboarding/updateProfile.ts | 24 +++----- frontend/src/api/user/inviteUsers.ts | 23 +++---- .../InviteTeamMembers.styles.scss | 1 + .../InviteTeamMembers/InviteTeamMembers.tsx | 61 ++++++++++++++++--- .../OptimiseSignozNeeds.tsx | 4 ++ .../OnboardingQuestionaire/index.tsx | 4 +- frontend/src/types/api/user/inviteUsers.ts | 3 +- frontend/src/utils/error.ts | 12 ++++ 9 files changed, 93 insertions(+), 40 deletions(-) create mode 100644 frontend/src/utils/error.ts diff --git a/frontend/public/locales/en/titles.json b/frontend/public/locales/en/titles.json index 126b8a7ac1..aa59005c3f 100644 --- a/frontend/public/locales/en/titles.json +++ b/frontend/public/locales/en/titles.json @@ -4,6 +4,7 @@ "SERVICE_METRICS": "SigNoz | Service Metrics", "SERVICE_MAP": "SigNoz | Service Map", "GET_STARTED": "SigNoz | Get Started", + "GET_STARTED_V2": "SigNoz | Get Started", "GET_STARTED_APPLICATION_MONITORING": "SigNoz | Get Started | APM", "GET_STARTED_LOGS_MANAGEMENT": "SigNoz | Get Started | Logs", "GET_STARTED_INFRASTRUCTURE_MONITORING": "SigNoz | Get Started | Infrastructure", diff --git a/frontend/src/api/onboarding/updateProfile.ts b/frontend/src/api/onboarding/updateProfile.ts index f7f5ffe22c..a9e2d44653 100644 --- a/frontend/src/api/onboarding/updateProfile.ts +++ b/frontend/src/api/onboarding/updateProfile.ts @@ -1,26 +1,20 @@ import { GatewayApiV2Instance } from 'api'; -import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; -import { AxiosError } from 'axios'; import { ErrorResponse, SuccessResponse } from 'types/api'; import { UpdateProfileProps } from 'types/api/onboarding/types'; const updateProfile = async ( props: UpdateProfileProps, ): Promise | ErrorResponse> => { - try { - const response = await GatewayApiV2Instance.put('/profiles/me', { - ...props, - }); + const response = await GatewayApiV2Instance.put('/profiles/me', { + ...props, + }); - return { - statusCode: 200, - error: null, - message: response.data.status, - payload: response.data.data, - }; - } catch (error) { - return ErrorResponseHandler(error as AxiosError); - } + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data.data, + }; }; export default updateProfile; diff --git a/frontend/src/api/user/inviteUsers.ts b/frontend/src/api/user/inviteUsers.ts index 6722b26593..28189159ff 100644 --- a/frontend/src/api/user/inviteUsers.ts +++ b/frontend/src/api/user/inviteUsers.ts @@ -1,25 +1,18 @@ -import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; -import axios, { AxiosError } from 'axios'; +import axios from 'api'; import { ErrorResponse, SuccessResponse } from 'types/api'; import { PayloadProps, UsersProps } from 'types/api/user/inviteUsers'; const inviteUsers = async ( users: UsersProps, ): Promise | ErrorResponse> => { - try { - const response = await axios.post(`/invite/bulk`, { - ...users, - }); + const response = await axios.post(`/invite/bulk`, users); - return { - statusCode: 200, - error: null, - message: response.data.status, - payload: response.data, - }; - } catch (error) { - return ErrorResponseHandler(error as AxiosError); - } + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data, + }; }; export default inviteUsers; diff --git a/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.styles.scss b/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.styles.scss index aae10276f6..263e1d4e80 100644 --- a/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.styles.scss +++ b/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.styles.scss @@ -41,6 +41,7 @@ display: flex; align-items: center; gap: 8px; + text-transform: capitalize; } } } diff --git a/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.tsx b/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.tsx index cd5f0d53c4..e4d2a64bc7 100644 --- a/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.tsx +++ b/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.tsx @@ -2,6 +2,8 @@ import './InviteTeamMembers.styles.scss'; import { Color } from '@signozhq/design-tokens'; import { Button, Input, Select, Typography } from 'antd'; +import inviteUsers from 'api/user/inviteUsers'; +import { AxiosError } from 'axios'; import { cloneDeep, debounce, isEmpty } from 'lodash-es'; import { ArrowLeft, @@ -11,6 +13,8 @@ import { TriangleAlert, } from 'lucide-react'; import { useCallback, useEffect, useState } from 'react'; +import { useMutation } from 'react-query'; +import { ErrorResponse } from 'types/api'; import { v4 as uuid } from 'uuid'; interface TeamMember { @@ -37,18 +41,17 @@ function InviteTeamMembers({ const [teamMembersToInvite, setTeamMembersToInvite] = useState< TeamMember[] | null >(teamMembers); - const [emailValidity, setEmailValidity] = useState>( {}, ); - const [hasInvalidEmails, setHasInvalidEmails] = useState(false); + const [error, setError] = useState(null); const defaultTeamMember: TeamMember = { email: '', role: 'EDITOR', name: '', - frontendBaseUrl: '', + frontendBaseUrl: window.location.origin, id: '', }; @@ -65,7 +68,10 @@ function InviteTeamMembers({ }, [teamMembers]); const handleAddTeamMember = (): void => { - const newTeamMember = { ...defaultTeamMember, id: uuid() }; + const newTeamMember = { + ...defaultTeamMember, + id: uuid(), + }; setTeamMembersToInvite((prev) => [...(prev || []), newTeamMember]); }; @@ -89,10 +95,34 @@ function InviteTeamMembers({ return isValid; }; + const handleError = (error: AxiosError): void => { + const errorMessage = error.response?.data as ErrorResponse; + + setError(errorMessage.error); + }; + + const { mutate: sendInvites, isLoading: isSendingInvites } = useMutation( + inviteUsers, + { + onSuccess: (): void => { + onNext(); + }, + onError: (error): void => { + handleError(error as AxiosError); + }, + }, + ); + const handleNext = (): void => { if (validateAllUsers()) { setTeamMembers(teamMembersToInvite || []); - onNext(); + + setError(null); + setHasInvalidEmails(false); + + sendInvites({ + users: teamMembersToInvite || [], + }); } }; @@ -129,6 +159,10 @@ function InviteTeamMembers({ } }; + const handleDoLater = (): void => { + onNext(); + }; + return (
@@ -209,20 +243,33 @@ function InviteTeamMembers({
)} + {error && ( +
+ + {error} + +
+ )} +
-
-
diff --git a/frontend/src/container/OnboardingQuestionaire/OptimiseSignozNeeds/OptimiseSignozNeeds.tsx b/frontend/src/container/OnboardingQuestionaire/OptimiseSignozNeeds/OptimiseSignozNeeds.tsx index f5d4d410c1..d564158059 100644 --- a/frontend/src/container/OnboardingQuestionaire/OptimiseSignozNeeds/OptimiseSignozNeeds.tsx +++ b/frontend/src/container/OnboardingQuestionaire/OptimiseSignozNeeds/OptimiseSignozNeeds.tsx @@ -14,6 +14,7 @@ interface OptimiseSignozNeedsProps { setOptimiseSignozDetails: (details: OptimiseSignozDetails) => void; onNext: () => void; onBack: () => void; + onWillDoLater: () => void; isUpdatingProfile: boolean; } @@ -48,6 +49,7 @@ function OptimiseSignozNeeds({ setOptimiseSignozDetails, onNext, onBack, + onWillDoLater, }: OptimiseSignozNeedsProps): JSX.Element { const [logsPerDay, setLogsPerDay] = useState( optimiseSignozDetails?.logsPerDay || 25, @@ -95,6 +97,8 @@ function OptimiseSignozNeeds({ services: 0, }); + onWillDoLater(); + logEvent('Onboarding: Optimise SigNoz Needs: Will do later', { logsPerDay: 0, hostsPerDay: 0, diff --git a/frontend/src/container/OnboardingQuestionaire/index.tsx b/frontend/src/container/OnboardingQuestionaire/index.tsx index 39e34e1f82..974070b944 100644 --- a/frontend/src/container/OnboardingQuestionaire/index.tsx +++ b/frontend/src/container/OnboardingQuestionaire/index.tsx @@ -5,6 +5,7 @@ import updateProfileAPI from 'api/onboarding/updateProfile'; import editOrg from 'api/user/editOrg'; import { AxiosError } from 'axios'; import { SOMETHING_WENT_WRONG } from 'constants/api'; +import ROUTES from 'constants/routes'; import { InviteTeamMembersProps } from 'container/OrganizationSettings/PendingInvitesContainer'; import { useNotifications } from 'hooks/useNotifications'; import history from 'lib/history'; @@ -187,7 +188,7 @@ function OnboardingQuestionaire(): JSX.Element { }; const handleOnboardingComplete = (): void => { - history.push('/'); + history.push(ROUTES.APPLICATION); }; return ( @@ -222,6 +223,7 @@ function OnboardingQuestionaire(): JSX.Element { setOptimiseSignozDetails={setOptimiseSignozDetails} onBack={(): void => setCurrentStep(2)} onNext={handleUpdateProfile} + onWillDoLater={(): void => setCurrentStep(4)} // This is temporary, only to skip gateway api call as it's not setup on staging yet /> )} diff --git a/frontend/src/types/api/user/inviteUsers.ts b/frontend/src/types/api/user/inviteUsers.ts index cf12269e73..8491e31cc4 100644 --- a/frontend/src/types/api/user/inviteUsers.ts +++ b/frontend/src/types/api/user/inviteUsers.ts @@ -1,10 +1,9 @@ import { User } from 'types/reducer/app'; -import { ROLES } from 'types/roles'; export interface UserProps { name: User['name']; email: User['email']; - role: ROLES; + role: string; frontendBaseUrl: string; } diff --git a/frontend/src/utils/error.ts b/frontend/src/utils/error.ts new file mode 100644 index 0000000000..7a0318a3ab --- /dev/null +++ b/frontend/src/utils/error.ts @@ -0,0 +1,12 @@ +import { NotificationInstance } from 'antd/es/notification/interface'; +import axios from 'axios'; +import { SOMETHING_WENT_WRONG } from 'constants/api'; + +export const showErrorNotification = ( + notifications: NotificationInstance, + err: Error, +): void => { + notifications.error({ + message: axios.isAxiosError(err) ? err.message : SOMETHING_WENT_WRONG, + }); +}; From 53b04b9bf12e0ccebd3186af1005002c02b863a5 Mon Sep 17 00:00:00 2001 From: Yunus M Date: Wed, 23 Oct 2024 23:06:28 +0530 Subject: [PATCH 06/15] feat: handle redirection after onboarding --- frontend/public/locales/en/titles.json | 2 +- frontend/src/AppRoutes/routes.ts | 4 +- frontend/src/constants/routes.ts | 2 +- frontend/src/container/AppLayout/index.tsx | 2 +- .../InviteTeamMembers.styles.scss | 21 +++++--- .../InviteTeamMembers/InviteTeamMembers.tsx | 34 +++++++++---- .../OnboardingQuestionaire.styles.scss | 50 ++++++++++++++++++- .../OnboardingQuestionaire/index.tsx | 16 +++++- frontend/src/container/SideNav/config.ts | 2 +- frontend/src/container/SideNav/menuItems.tsx | 11 ---- .../container/TopNav/Breadcrumbs/index.tsx | 1 - frontend/src/utils/permission/index.ts | 2 +- 12 files changed, 106 insertions(+), 41 deletions(-) diff --git a/frontend/public/locales/en/titles.json b/frontend/public/locales/en/titles.json index aa59005c3f..4d3e899d76 100644 --- a/frontend/public/locales/en/titles.json +++ b/frontend/public/locales/en/titles.json @@ -4,7 +4,7 @@ "SERVICE_METRICS": "SigNoz | Service Metrics", "SERVICE_MAP": "SigNoz | Service Map", "GET_STARTED": "SigNoz | Get Started", - "GET_STARTED_V2": "SigNoz | Get Started", + "ONBOARDING": "SigNoz | Get Started", "GET_STARTED_APPLICATION_MONITORING": "SigNoz | Get Started | APM", "GET_STARTED_LOGS_MANAGEMENT": "SigNoz | Get Started | Logs", "GET_STARTED_INFRASTRUCTURE_MONITORING": "SigNoz | Get Started | Infrastructure", diff --git a/frontend/src/AppRoutes/routes.ts b/frontend/src/AppRoutes/routes.ts index fc59fef1e3..1901095a67 100644 --- a/frontend/src/AppRoutes/routes.ts +++ b/frontend/src/AppRoutes/routes.ts @@ -70,11 +70,11 @@ const routes: AppRoutes[] = [ key: 'GET_STARTED', }, { - path: ROUTES.GET_STARTED_V2, + path: ROUTES.ONBOARDING, exact: false, component: OnboardingV2, isPrivate: true, - key: 'GET_STARTED_V2', + key: 'ONBOARDING', }, { component: LogsIndexToFields, diff --git a/frontend/src/constants/routes.ts b/frontend/src/constants/routes.ts index 50d189f4a8..3c17336889 100644 --- a/frontend/src/constants/routes.ts +++ b/frontend/src/constants/routes.ts @@ -8,7 +8,7 @@ const ROUTES = { TRACE_DETAIL: '/trace/:id', TRACES_EXPLORER: '/traces-explorer', GET_STARTED: '/get-started', - GET_STARTED_V2: '/get-started-v2', + ONBOARDING: '/onboarding', GET_STARTED_APPLICATION_MONITORING: '/get-started/application-monitoring', GET_STARTED_LOGS_MANAGEMENT: '/get-started/logs-management', GET_STARTED_INFRASTRUCTURE_MONITORING: diff --git a/frontend/src/container/AppLayout/index.tsx b/frontend/src/container/AppLayout/index.tsx index 898481e2a5..700914cc6a 100644 --- a/frontend/src/container/AppLayout/index.tsx +++ b/frontend/src/container/AppLayout/index.tsx @@ -191,7 +191,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element { const pageTitle = t(routeKey); const renderFullScreen = pathname === ROUTES.GET_STARTED || - pathname === ROUTES.GET_STARTED_V2 || + pathname === ROUTES.ONBOARDING || pathname === ROUTES.WORKSPACE_LOCKED || pathname === ROUTES.GET_STARTED_APPLICATION_MONITORING || pathname === ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING || diff --git a/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.styles.scss b/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.styles.scss index 263e1d4e80..ae26562b3a 100644 --- a/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.styles.scss +++ b/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.styles.scss @@ -7,19 +7,24 @@ .ant-select-selector { border: 1px solid #1d212d; - border-top-right-radius: 0px; - border-bottom-right-radius: 0px; - - border-right: none; + border-top-left-radius: 0px; + border-bottom-left-radius: 0px; } } .team-member-email-input { width: 80%; - - border: 1px solid #1d212d; - border-top-left-radius: 0px; - border-bottom-left-radius: 0px; + background-color: #121317; + border-top-right-radius: 0px; + border-bottom-right-radius: 0px; + + .ant-input, + .ant-input-group-addon { + background-color: #121317 !important; + border-right: 0px; + border-top-right-radius: 0px; + border-bottom-right-radius: 0px; + } } } diff --git a/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.tsx b/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.tsx index e4d2a64bc7..4adfd92c6e 100644 --- a/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.tsx +++ b/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.tsx @@ -11,6 +11,7 @@ import { CheckCircle, Plus, TriangleAlert, + X, } from 'lucide-react'; import { useCallback, useEffect, useState } from 'react'; import { useMutation } from 'react-query'; @@ -75,6 +76,10 @@ function InviteTeamMembers({ setTeamMembersToInvite((prev) => [...(prev || []), newTeamMember]); }; + const handleRemoveTeamMember = (id: string): void => { + setTeamMembersToInvite((prev) => (prev || []).filter((m) => m.id !== id)); + }; + // Validation function to check all users const validateAllUsers = (): boolean => { let isValid = true; @@ -174,7 +179,7 @@ function InviteTeamMembers({
-
+
Collaborate with your team @@ -186,15 +191,6 @@ function InviteTeamMembers({
{teamMembersToInvite?.map((member) => (
- + + + {teamMembersToInvite?.length > 1 && ( +
))}
diff --git a/frontend/src/container/OnboardingQuestionaire/OnboardingQuestionaire.styles.scss b/frontend/src/container/OnboardingQuestionaire/OnboardingQuestionaire.styles.scss index 8be4dc608c..00b091d1d8 100644 --- a/frontend/src/container/OnboardingQuestionaire/OnboardingQuestionaire.styles.scss +++ b/frontend/src/container/OnboardingQuestionaire/OnboardingQuestionaire.styles.scss @@ -83,6 +83,31 @@ } } } + + &.invite-team-members-form { + min-height: calc(420px - 24px); + max-height: calc(420px - 24px); + + .invite-team-members-container { + max-height: 260px; + padding-right: 8px; + overflow-y: auto; + + &::-webkit-scrollbar { + width: 0.1rem; + } + &::-webkit-scrollbar-corner { + background: transparent; + } + &::-webkit-scrollbar-thumb { + background: rgb(136, 136, 136); + border-radius: 0.625rem; + } + &::-webkit-scrollbar-track { + background: transparent; + } + } + } } .invite-team-members-container { @@ -173,6 +198,10 @@ font-style: normal; font-weight: 500; line-height: 20px; + + display: flex; + align-items: center; + gap: 8px; } input[type='text'] { @@ -237,7 +266,8 @@ } .onboarding-questionaire-button, - .add-another-member-button { + .add-another-member-button, + .remove-team-member-button { display: flex; align-items: center; justify-content: space-between; @@ -270,11 +300,27 @@ } } - .add-another-member-button { + .add-another-member-button, + .remove-team-member-button { font-size: 12px; height: 32px; } + .remove-team-member-button { + display: flex; + align-items: center; + justify-content: center; + + border: 1px solid #1d212d; + border-top-left-radius: 0px; + border-bottom-left-radius: 0px; + background-color: #121317; + + border-left: 0px; + border-top-left-radius: 0px; + border-bottom-left-radius: 0px; + } + .onboarding-questionaire-other-input { .ant-input-group { .ant-input { diff --git a/frontend/src/container/OnboardingQuestionaire/index.tsx b/frontend/src/container/OnboardingQuestionaire/index.tsx index 974070b944..a5d1f83254 100644 --- a/frontend/src/container/OnboardingQuestionaire/index.tsx +++ b/frontend/src/container/OnboardingQuestionaire/index.tsx @@ -3,6 +3,7 @@ import './OnboardingQuestionaire.styles.scss'; import { NotificationInstance } from 'antd/es/notification/interface'; import updateProfileAPI from 'api/onboarding/updateProfile'; import editOrg from 'api/user/editOrg'; +import getOrgUser from 'api/user/getOrgUser'; import { AxiosError } from 'axios'; import { SOMETHING_WENT_WRONG } from 'constants/api'; import ROUTES from 'constants/routes'; @@ -11,7 +12,7 @@ import { useNotifications } from 'hooks/useNotifications'; import history from 'lib/history'; import { Dispatch, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useMutation } from 'react-query'; +import { useMutation, useQuery } from 'react-query'; import { useDispatch, useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; import AppActions from 'types/actions'; @@ -78,6 +79,17 @@ function OnboardingQuestionaire(): JSX.Element { const { t } = useTranslation(['organizationsettings', 'common']); const { org } = useSelector((state) => state.app); + + const { data: orgUsers, isLoading: isLoadingOrgUsers } = useQuery({ + queryFn: () => + getOrgUser({ + orgId: (org || [])[0].id, + }), + queryKey: ['getOrgUser', org?.[0].id], + }); + + console.log('orgUsers', orgUsers, isLoadingOrgUsers); + const dispatch = useDispatch>(); const [orgData, setOrgData] = useState(null); @@ -188,7 +200,7 @@ function OnboardingQuestionaire(): JSX.Element { }; const handleOnboardingComplete = (): void => { - history.push(ROUTES.APPLICATION); + history.push(ROUTES.GET_STARTED); }; return ( diff --git a/frontend/src/container/SideNav/config.ts b/frontend/src/container/SideNav/config.ts index 7b37f64bb7..7fdd462a52 100644 --- a/frontend/src/container/SideNav/config.ts +++ b/frontend/src/container/SideNav/config.ts @@ -27,7 +27,7 @@ export const routeConfig: Record = { [ROUTES.ERROR_DETAIL]: [QueryParams.resourceAttributes], [ROUTES.HOME_PAGE]: [QueryParams.resourceAttributes], [ROUTES.GET_STARTED]: [QueryParams.resourceAttributes], - [ROUTES.GET_STARTED_V2]: [QueryParams.resourceAttributes], + [ROUTES.ONBOARDING]: [QueryParams.resourceAttributes], [ROUTES.LIST_ALL_ALERT]: [QueryParams.resourceAttributes], [ROUTES.LIST_LICENSES]: [QueryParams.resourceAttributes], [ROUTES.LOGIN]: [QueryParams.resourceAttributes], diff --git a/frontend/src/container/SideNav/menuItems.tsx b/frontend/src/container/SideNav/menuItems.tsx index a1c41ddea8..6d24b74c53 100644 --- a/frontend/src/container/SideNav/menuItems.tsx +++ b/frontend/src/container/SideNav/menuItems.tsx @@ -29,12 +29,6 @@ export const getStartedMenuItem = { icon: , }; -export const getStartedV2MenuItem = { - key: ROUTES.GET_STARTED_V2, - label: 'Get Started V2', - icon: , -}; - export const inviteMemberMenuItem = { key: `${ROUTES.ORG_SETTINGS}#invite-team-members`, label: 'Invite Team Member', @@ -72,11 +66,6 @@ export const trySignozCloudMenuItem: SidebarItem = { }; const menuItems: SidebarItem[] = [ - { - key: ROUTES.GET_STARTED_V2, - label: 'Get Started V2', - icon: , - }, { key: ROUTES.APPLICATION, label: 'Services', diff --git a/frontend/src/container/TopNav/Breadcrumbs/index.tsx b/frontend/src/container/TopNav/Breadcrumbs/index.tsx index c98f4d05e2..9efd50d2c3 100644 --- a/frontend/src/container/TopNav/Breadcrumbs/index.tsx +++ b/frontend/src/container/TopNav/Breadcrumbs/index.tsx @@ -9,7 +9,6 @@ const breadcrumbNameMap: Record = { [ROUTES.SERVICE_MAP]: 'Service Map', [ROUTES.USAGE_EXPLORER]: 'Usage Explorer', [ROUTES.GET_STARTED]: 'Get Started', - [ROUTES.GET_STARTED_V2]: 'Get Started V2', [ROUTES.ALL_CHANNELS]: 'Channels', [ROUTES.SETTINGS]: 'Settings', [ROUTES.DASHBOARD]: 'Dashboard', diff --git a/frontend/src/utils/permission/index.ts b/frontend/src/utils/permission/index.ts index 6052900a05..e58843232a 100644 --- a/frontend/src/utils/permission/index.ts +++ b/frontend/src/utils/permission/index.ts @@ -86,7 +86,7 @@ export const routePermission: Record = { LOGS_PIPELINES: ['ADMIN', 'EDITOR', 'VIEWER'], TRACE_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'], GET_STARTED: ['ADMIN', 'EDITOR', 'VIEWER'], - GET_STARTED_V2: ['ADMIN', 'EDITOR', 'VIEWER'], + ONBOARDING: ['ADMIN', 'EDITOR', 'VIEWER'], GET_STARTED_APPLICATION_MONITORING: ['ADMIN', 'EDITOR', 'VIEWER'], GET_STARTED_INFRASTRUCTURE_MONITORING: ['ADMIN', 'EDITOR', 'VIEWER'], GET_STARTED_LOGS_MANAGEMENT: ['ADMIN', 'EDITOR', 'VIEWER'], From 887d46e6262f02d0d007442e9f78d85a4e20852b Mon Sep 17 00:00:00 2001 From: Yunus M Date: Mon, 28 Oct 2024 17:30:11 +0530 Subject: [PATCH 07/15] feat: feedback updates --- .../api/preferences/updateOrgPreference.ts | 9 +- .../AboutSigNozQuestions.tsx | 8 +- .../InviteTeamMembers/InviteTeamMembers.tsx | 2 +- .../OnboardingFooter.styles.scss | 3 + .../OnboardingFooter/OnboardingFooter.tsx | 26 +-- .../OnboardingHeader.styles.scss | 1 + .../OnboardingHeader/OnboardingHeader.tsx | 11 +- .../OnboardingQuestionaire.styles.scss | 10 ++ .../OptimiseSignozNeeds.tsx | 14 +- .../OrgQuestions/OrgQuestions.tsx | 10 +- .../OnboardingQuestionaire/index.tsx | 150 +++++++++++++----- frontend/src/container/SideNav/SideNav.tsx | 4 +- frontend/src/pages/SignUp/SignUp.tsx | 2 +- .../api/preferences/userOrgPreferences.ts | 4 +- 14 files changed, 162 insertions(+), 92 deletions(-) diff --git a/frontend/src/api/preferences/updateOrgPreference.ts b/frontend/src/api/preferences/updateOrgPreference.ts index 76e5a68640..aae4d83ddf 100644 --- a/frontend/src/api/preferences/updateOrgPreference.ts +++ b/frontend/src/api/preferences/updateOrgPreference.ts @@ -10,9 +10,12 @@ const updateOrgPreference = async ( ): Promise< SuccessResponse | ErrorResponse > => { - const response = await axios.put(`/org/preferences`, { - preference_value: preferencePayload.value, - }); + const response = await axios.put( + `/org/preferences/${preferencePayload.preferenceID}`, + { + preference_value: preferencePayload.value, + }, + ); return { statusCode: 200, diff --git a/frontend/src/container/OnboardingQuestionaire/AboutSigNozQuestions/AboutSigNozQuestions.tsx b/frontend/src/container/OnboardingQuestionaire/AboutSigNozQuestions/AboutSigNozQuestions.tsx index df8066b358..c6f0320d1a 100644 --- a/frontend/src/container/OnboardingQuestionaire/AboutSigNozQuestions/AboutSigNozQuestions.tsx +++ b/frontend/src/container/OnboardingQuestionaire/AboutSigNozQuestions/AboutSigNozQuestions.tsx @@ -22,7 +22,7 @@ interface AboutSigNozQuestionsProps { } const hearAboutSignozOptions: Record = { - blog: 'Blog', + search: 'Google / Search', hackerNews: 'Hacker News', linkedin: 'LinkedIn', twitter: 'Twitter', @@ -144,7 +144,7 @@ export function AboutSigNozQuestions({
-
- What are you interested in doing with SigNoz? -
+
What got you interested in SigNoz?
{Object.keys(interestedInOptions).map((option: string) => (
); diff --git a/frontend/src/container/OnboardingQuestionaire/OnboardingHeader/OnboardingHeader.styles.scss b/frontend/src/container/OnboardingQuestionaire/OnboardingHeader/OnboardingHeader.styles.scss index 0b2cecaac6..984b89828b 100644 --- a/frontend/src/container/OnboardingQuestionaire/OnboardingHeader/OnboardingHeader.styles.scss +++ b/frontend/src/container/OnboardingQuestionaire/OnboardingHeader/OnboardingHeader.styles.scss @@ -40,6 +40,7 @@ border-radius: 2px; border: 1px solid var(--Greyscale-Slate-400, #1d212d); background: var(--Ink-300, #16181d); + box-shadow: none; } .header-container .get-help-container img { diff --git a/frontend/src/container/OnboardingQuestionaire/OnboardingHeader/OnboardingHeader.tsx b/frontend/src/container/OnboardingQuestionaire/OnboardingHeader/OnboardingHeader.tsx index 76801d7620..f1b54d50dd 100644 --- a/frontend/src/container/OnboardingQuestionaire/OnboardingHeader/OnboardingHeader.tsx +++ b/frontend/src/container/OnboardingQuestionaire/OnboardingHeader/OnboardingHeader.tsx @@ -1,19 +1,26 @@ import './OnboardingHeader.styles.scss'; import { Color } from '@signozhq/design-tokens'; +import { Button } from 'antd'; import { LifeBuoy } from 'lucide-react'; export function OnboardingHeader(): JSX.Element { + const handleGetHelpClick = (): void => { + if (window.Intercom) { + window.Intercom('showNewMessage', ''); + } + }; + return (
SigNoz SigNoz
-
+
+
); } diff --git a/frontend/src/container/OnboardingQuestionaire/OnboardingQuestionaire.styles.scss b/frontend/src/container/OnboardingQuestionaire/OnboardingQuestionaire.styles.scss index 00b091d1d8..d50ca325b7 100644 --- a/frontend/src/container/OnboardingQuestionaire/OnboardingQuestionaire.styles.scss +++ b/frontend/src/container/OnboardingQuestionaire/OnboardingQuestionaire.styles.scss @@ -386,3 +386,13 @@ margin-top: 12px; } } + +.onboarding-questionaire-loading-container { + width: 100%; + display: flex; + height: 100vh; + max-width: 600px; + justify-content: center; + align-items: center; + margin: 0 auto; +} diff --git a/frontend/src/container/OnboardingQuestionaire/OptimiseSignozNeeds/OptimiseSignozNeeds.tsx b/frontend/src/container/OnboardingQuestionaire/OptimiseSignozNeeds/OptimiseSignozNeeds.tsx index d564158059..a801c2784e 100644 --- a/frontend/src/container/OnboardingQuestionaire/OptimiseSignozNeeds/OptimiseSignozNeeds.tsx +++ b/frontend/src/container/OnboardingQuestionaire/OptimiseSignozNeeds/OptimiseSignozNeeds.tsx @@ -16,10 +16,11 @@ interface OptimiseSignozNeedsProps { onBack: () => void; onWillDoLater: () => void; isUpdatingProfile: boolean; + isNextDisabled: boolean; } const logMarks: SliderSingleProps['marks'] = { - 0: '2 GB', + 0: '0 GB', 25: '25 GB', 50: '50 GB', 100: '100 GB', @@ -50,15 +51,16 @@ function OptimiseSignozNeeds({ onNext, onBack, onWillDoLater, + isNextDisabled, }: OptimiseSignozNeedsProps): JSX.Element { const [logsPerDay, setLogsPerDay] = useState( - optimiseSignozDetails?.logsPerDay || 25, + optimiseSignozDetails?.logsPerDay || 0, ); const [hostsPerDay, setHostsPerDay] = useState( - optimiseSignozDetails?.hostsPerDay || 40, + optimiseSignozDetails?.hostsPerDay || 0, ); const [services, setServices] = useState( - optimiseSignozDetails?.services || 10, + optimiseSignozDetails?.services || 0, ); useEffect(() => { @@ -191,7 +193,7 @@ function OptimiseSignozNeeds({ type="primary" className="next-button" onClick={handleOnNext} - disabled={isUpdatingProfile} + disabled={isUpdatingProfile || isNextDisabled} > Next{' '} {isUpdatingProfile ? ( @@ -204,7 +206,7 @@ function OptimiseSignozNeeds({
diff --git a/frontend/src/container/OnboardingQuestionaire/OrgQuestions/OrgQuestions.tsx b/frontend/src/container/OnboardingQuestionaire/OrgQuestions/OrgQuestions.tsx index a1fea0aaf8..ea6bad53d6 100644 --- a/frontend/src/container/OnboardingQuestionaire/OrgQuestions/OrgQuestions.tsx +++ b/frontend/src/container/OnboardingQuestionaire/OrgQuestions/OrgQuestions.tsx @@ -42,10 +42,10 @@ const observabilityTools = { }; const o11yFamiliarityOptions: Record = { - new: "I'm completely new", - builtStack: "I've built a stack before", - experienced: 'I have some experience', - dontKnow: "I don't know what it is", + beginner: 'Basic Understanding', + intermediate: 'Somewhat Familiar', + expert: 'Very Familiar', + notFamiliar: "I'm not familiar with it", }; function OrgQuestions({ @@ -254,7 +254,7 @@ function OrgQuestions({
- Are you familiar with observability (o11y)? + Are you familiar with setting upobservability (o11y)?
{Object.keys(o11yFamiliarityOptions).map((option: string) => ( diff --git a/frontend/src/container/OnboardingQuestionaire/index.tsx b/frontend/src/container/OnboardingQuestionaire/index.tsx index a5d1f83254..77bde17848 100644 --- a/frontend/src/container/OnboardingQuestionaire/index.tsx +++ b/frontend/src/container/OnboardingQuestionaire/index.tsx @@ -1,7 +1,10 @@ import './OnboardingQuestionaire.styles.scss'; +import { Skeleton } from 'antd'; import { NotificationInstance } from 'antd/es/notification/interface'; import updateProfileAPI from 'api/onboarding/updateProfile'; +import getOrgPreference from 'api/preferences/getOrgPreference'; +import updateOrgPreferenceAPI from 'api/preferences/updateOrgPreference'; import editOrg from 'api/user/editOrg'; import getOrgUser from 'api/user/getOrgUser'; import { AxiosError } from 'axios'; @@ -10,6 +13,7 @@ import ROUTES from 'constants/routes'; import { InviteTeamMembersProps } from 'container/OrganizationSettings/PendingInvitesContainer'; import { useNotifications } from 'hooks/useNotifications'; import history from 'lib/history'; +import { isEmpty } from 'lodash-es'; import { Dispatch, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useMutation, useQuery } from 'react-query'; @@ -56,9 +60,9 @@ const INITIAL_SIGNOZ_DETAILS: SignozDetails = { }; const INITIAL_OPTIMISE_SIGNOZ_DETAILS: OptimiseSignozDetails = { - logsPerDay: 25, - hostsPerDay: 40, - services: 10, + logsPerDay: 0, + hostsPerDay: 0, + services: 0, }; function OnboardingQuestionaire(): JSX.Element { @@ -92,6 +96,44 @@ function OnboardingQuestionaire(): JSX.Element { const dispatch = useDispatch>(); const [orgData, setOrgData] = useState(null); + const [isOnboardingComplete, setIsOnboardingComplete] = useState( + false, + ); + + const { data: orgPreferences, isLoading: isLoadingOrgPreferences } = useQuery({ + queryFn: () => getOrgPreference({ preferenceID: 'ORG_ONBOARDING' }), + queryKey: ['getOrgPreferences', 'ORG_ONBOARDING'], + }); + + useEffect(() => { + if (!isLoadingOrgPreferences && !isEmpty(orgPreferences?.payload?.data)) { + const preferenceId = orgPreferences?.payload?.data?.preference_id; + const preferenceValue = orgPreferences?.payload?.data?.preference_value; + + if (preferenceId === 'ORG_ONBOARDING') { + setIsOnboardingComplete(preferenceValue as boolean); + } + } + }, [orgPreferences, isLoadingOrgPreferences]); + + const checkFirstTimeUser = (): boolean => { + const users = orgUsers?.payload || []; + + const remainingUsers = users.filter( + (user) => user.email !== 'admin@signoz.cloud', + ); + + return remainingUsers.length === 1; + }; + + useEffect(() => { + const isFirstUser = checkFirstTimeUser(); + + if (isOnboardingComplete || !isFirstUser) { + history.push(ROUTES.GET_STARTED); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isOnboardingComplete, orgUsers]); useEffect(() => { if (org) { @@ -105,6 +147,11 @@ function OnboardingQuestionaire(): JSX.Element { // eslint-disable-next-line react-hooks/exhaustive-deps }, [org]); + const isNextDisabled = + optimiseSignozDetails.logsPerDay === 0 || + optimiseSignozDetails.hostsPerDay === 0 || + optimiseSignozDetails.services === 0; + const [isLoading, setIsLoading] = useState(false); const handleOrgNameUpdate = async (): Promise => { @@ -164,9 +211,7 @@ function OnboardingQuestionaire(): JSX.Element { const { mutate: updateProfile, isLoading: isUpdatingProfile } = useMutation( updateProfileAPI, { - onSuccess: (data) => { - console.log('data', data); - + onSuccess: () => { setCurrentStep(4); }, onError: (error) => { @@ -175,6 +220,15 @@ function OnboardingQuestionaire(): JSX.Element { }, ); + const { mutate: updateOrgPreference } = useMutation(updateOrgPreferenceAPI, { + onSuccess: () => { + setIsOnboardingComplete(true); + }, + onError: (error) => { + showErrorNotification(notifications, error as AxiosError); + }, + }); + const handleUpdateProfile = (): void => { updateProfile({ familiarity_with_observability: orgDetails?.familiarity as string, @@ -200,7 +254,10 @@ function OnboardingQuestionaire(): JSX.Element { }; const handleOnboardingComplete = (): void => { - history.push(ROUTES.GET_STARTED); + updateOrgPreference({ + preferenceID: 'ORG_ONBOARDING', + value: true, + }); }; return ( @@ -210,42 +267,53 @@ function OnboardingQuestionaire(): JSX.Element {
- {currentStep === 1 && ( - - )} - - {currentStep === 2 && ( - setCurrentStep(1)} - onNext={(): void => setCurrentStep(3)} - /> - )} - - {currentStep === 3 && ( - setCurrentStep(2)} - onNext={handleUpdateProfile} - onWillDoLater={(): void => setCurrentStep(4)} // This is temporary, only to skip gateway api call as it's not setup on staging yet - /> + {(isLoadingOrgPreferences || isLoadingOrgUsers) && ( +
+ +
)} - {currentStep === 4 && ( - setCurrentStep(3)} - onNext={handleOnboardingComplete} - /> + {!isLoadingOrgPreferences && !isLoadingOrgUsers && ( + <> + {currentStep === 1 && ( + + )} + + {currentStep === 2 && ( + setCurrentStep(1)} + onNext={(): void => setCurrentStep(3)} + /> + )} + + {currentStep === 3 && ( + setCurrentStep(2)} + onNext={handleUpdateProfile} + onWillDoLater={(): void => setCurrentStep(4)} // This is temporary, only to skip gateway api call as it's not setup on staging yet + /> + )} + + {currentStep === 4 && ( + setCurrentStep(3)} + onNext={handleOnboardingComplete} + /> + )} + )}
diff --git a/frontend/src/container/SideNav/SideNav.tsx b/frontend/src/container/SideNav/SideNav.tsx index 16787bc3d8..8ccafacb0a 100644 --- a/frontend/src/container/SideNav/SideNav.tsx +++ b/frontend/src/container/SideNav/SideNav.tsx @@ -113,7 +113,9 @@ function SideNav({ if (!isOnboardingEnabled || !isCloudUser()) { let items = [...menuItems]; - items = items.filter((item) => item.key !== ROUTES.GET_STARTED); + items = items.filter( + (item) => item.key !== ROUTES.GET_STARTED && item.key !== ROUTES.ONBOARDING, + ); setMenuItems(items); } diff --git a/frontend/src/pages/SignUp/SignUp.tsx b/frontend/src/pages/SignUp/SignUp.tsx index 4917b0fe2d..68f9c19dd1 100644 --- a/frontend/src/pages/SignUp/SignUp.tsx +++ b/frontend/src/pages/SignUp/SignUp.tsx @@ -261,7 +261,7 @@ function SignUp({ version }: SignUpProps): JSX.Element { values, async (): Promise => { if (isOnboardingEnabled && isCloudUser()) { - history.push(ROUTES.GET_STARTED); + history.push(ROUTES.ONBOARDING); } else { history.push(ROUTES.APPLICATION); } diff --git a/frontend/src/types/api/preferences/userOrgPreferences.ts b/frontend/src/types/api/preferences/userOrgPreferences.ts index 6faa75d5f1..2c77090769 100644 --- a/frontend/src/types/api/preferences/userOrgPreferences.ts +++ b/frontend/src/types/api/preferences/userOrgPreferences.ts @@ -19,12 +19,12 @@ export interface GetAllUserPreferencesResponseProps { } export interface UpdateOrgPreferenceProps { - key: string; + preferenceID: string; value: unknown; } export interface UpdateUserPreferenceProps { - key: string; + preferenceID: string; value: unknown; } From 8592c8f6cb0a3e9538a95dec04e52b7bbab4a9c2 Mon Sep 17 00:00:00 2001 From: Yunus M Date: Mon, 28 Oct 2024 18:33:56 +0530 Subject: [PATCH 08/15] feat: feedback updates --- .../AboutSigNozQuestions.tsx | 4 +- .../OnboardingHeader/OnboardingHeader.tsx | 14 -- .../OptimiseSignozNeeds.tsx | 194 +++++++++++++----- .../OrgQuestions/OrgQuestions.tsx | 8 +- .../OnboardingQuestionaire/index.tsx | 9 +- 5 files changed, 152 insertions(+), 77 deletions(-) diff --git a/frontend/src/container/OnboardingQuestionaire/AboutSigNozQuestions/AboutSigNozQuestions.tsx b/frontend/src/container/OnboardingQuestionaire/AboutSigNozQuestions/AboutSigNozQuestions.tsx index c6f0320d1a..8ebddd3430 100644 --- a/frontend/src/container/OnboardingQuestionaire/AboutSigNozQuestions/AboutSigNozQuestions.tsx +++ b/frontend/src/container/OnboardingQuestionaire/AboutSigNozQuestions/AboutSigNozQuestions.tsx @@ -33,7 +33,7 @@ const hearAboutSignozOptions: Record = { const interestedInOptions: Record = { savingCosts: 'Saving costs', otelNativeStack: 'Interested in Otel-native stack', - allInOne: 'All in one', + allInOne: 'All in one (Logs, Metrics & Traces)', }; export function AboutSigNozQuestions({ @@ -144,7 +144,7 @@ export function AboutSigNozQuestions({ { - if (window.Intercom) { - window.Intercom('showNewMessage', ''); - } - }; - return (
SigNoz SigNoz
-
); } diff --git a/frontend/src/container/OnboardingQuestionaire/OptimiseSignozNeeds/OptimiseSignozNeeds.tsx b/frontend/src/container/OnboardingQuestionaire/OptimiseSignozNeeds/OptimiseSignozNeeds.tsx index a801c2784e..b3fdb9f4e7 100644 --- a/frontend/src/container/OnboardingQuestionaire/OptimiseSignozNeeds/OptimiseSignozNeeds.tsx +++ b/frontend/src/container/OnboardingQuestionaire/OptimiseSignozNeeds/OptimiseSignozNeeds.tsx @@ -1,4 +1,4 @@ -import { Button, Slider, SliderSingleProps, Typography } from 'antd'; +import { Button, Slider, Typography } from 'antd'; import logEvent from 'api/common/logEvent'; import { ArrowLeft, ArrowRight, Loader2, Minus } from 'lucide-react'; import { useEffect, useState } from 'react'; @@ -9,6 +9,28 @@ export interface OptimiseSignozDetails { services: number; } +// Define exponential range +const logsMin = 1; // Set to your minimum value in the exponential range +const logsMax = 10000; // Set to your maximum value in the exponential range + +const hostsMin = 1; +const hostsMax = 10000; + +const servicesMin = 1; +const servicesMax = 5000; + +// Function to convert linear slider value to exponential scale +const linearToExponential = ( + value: number, + min: number, + max: number, +): number => { + const expMin = Math.log10(min); + const expMax = Math.log10(max); + const expValue = 10 ** (expMin + ((expMax - expMin) * value) / 100); + return Math.round(expValue); +}; + interface OptimiseSignozNeedsProps { optimiseSignozDetails: OptimiseSignozDetails; setOptimiseSignozDetails: (details: OptimiseSignozDetails) => void; @@ -19,29 +41,28 @@ interface OptimiseSignozNeedsProps { isNextDisabled: boolean; } -const logMarks: SliderSingleProps['marks'] = { - 0: '0 GB', - 25: '25 GB', - 50: '50 GB', - 100: '100 GB', +const marks = { + 0: `${linearToExponential(0, logsMin, logsMax).toLocaleString()} GB`, + 25: `${linearToExponential(25, logsMin, logsMax).toLocaleString()} GB`, + 50: `${linearToExponential(50, logsMin, logsMax).toLocaleString()} GB`, + 75: `${linearToExponential(75, logsMin, logsMax).toLocaleString()} GB`, + 100: `${linearToExponential(100, logsMin, logsMax).toLocaleString()} GB`, }; -const hostMarks: SliderSingleProps['marks'] = { - 0: '0', - 20: '20', - 40: '40', - 60: '60', - 80: '80', - 100: '100', +const hostMarks = { + 0: `${linearToExponential(0, hostsMin, hostsMax).toLocaleString()}`, + 25: `${linearToExponential(25, hostsMin, hostsMax).toLocaleString()}`, + 50: `${linearToExponential(50, hostsMin, hostsMax).toLocaleString()}`, + 75: `${linearToExponential(75, hostsMin, hostsMax).toLocaleString()}`, + 100: `${linearToExponential(100, hostsMin, hostsMax).toLocaleString()}`, }; -const serviceMarks: SliderSingleProps['marks'] = { - 0: '0', - 20: '20', - 40: '40', - 60: '60', - 80: '80', - 100: '100', +const serviceMarks = { + 0: `${linearToExponential(0, servicesMin, servicesMax).toLocaleString()}`, + 25: `${linearToExponential(25, servicesMin, servicesMax).toLocaleString()}`, + 50: `${linearToExponential(50, servicesMin, servicesMax).toLocaleString()}`, + 75: `${linearToExponential(75, servicesMin, servicesMax).toLocaleString()}`, + 100: `${linearToExponential(100, servicesMin, servicesMax).toLocaleString()}`, }; function OptimiseSignozNeeds({ @@ -108,6 +129,52 @@ function OptimiseSignozNeeds({ }); }; + // Internal state for the linear slider + const [sliderValues, setSliderValues] = useState({ + logsPerDay: 0, + hostsPerDay: 0, + services: 0, + }); + + const handleSliderChange = (key: string, value: number): void => { + console.log('value', value); + setSliderValues({ + ...sliderValues, + [key]: value, + }); + + switch (key) { + case 'logsPerDay': + setLogsPerDay(value); + break; + case 'hostsPerDay': + setHostsPerDay(value); + break; + case 'services': + setServices(value); + break; + default: + break; + } + }; + + // Calculate the exponential value based on the current slider position + const logsPerDayValue = linearToExponential( + sliderValues.logsPerDay, + logsMin, + logsMax, + ); + const hostsPerDayValue = linearToExponential( + sliderValues.hostsPerDay, + hostsMin, + hostsMax, + ); + const servicesValue = linearToExponential( + sliderValues.services, + servicesMin, + servicesMax, + ); + return (
@@ -128,16 +195,25 @@ function OptimiseSignozNeeds({ Logs / Day
- setLogsPerDay(value)} - styles={{ - track: { - background: '#4E74F8', - }, - }} - /> +
+ + handleSliderChange('logsPerDay', value) + } + styles={{ + track: { + background: '#4E74F8', + }, + }} + tooltip={{ + formatter: (): string => `${logsPerDayValue.toLocaleString()} GB`, // Show whole number + }} + /> +
@@ -146,16 +222,25 @@ function OptimiseSignozNeeds({ Metrics Number of Hosts
- setHostsPerDay(value)} - styles={{ - track: { - background: '#4E74F8', - }, - }} - /> +
+ + handleSliderChange('hostsPerDay', value) + } + styles={{ + track: { + background: '#4E74F8', + }, + }} + tooltip={{ + formatter: (): string => `${hostsPerDayValue.toLocaleString()}`, // Show whole number + }} + /> +
@@ -164,16 +249,25 @@ function OptimiseSignozNeeds({ Number of services
- setServices(value)} - styles={{ - track: { - background: '#4E74F8', - }, - }} - /> +
+ + handleSliderChange('services', value) + } + styles={{ + track: { + background: '#4E74F8', + }, + }} + tooltip={{ + formatter: (): string => `${servicesValue.toLocaleString()}`, // Show whole number + }} + /> +
diff --git a/frontend/src/container/OnboardingQuestionaire/OrgQuestions/OrgQuestions.tsx b/frontend/src/container/OnboardingQuestionaire/OrgQuestions/OrgQuestions.tsx index ea6bad53d6..17822c2342 100644 --- a/frontend/src/container/OnboardingQuestionaire/OrgQuestions/OrgQuestions.tsx +++ b/frontend/src/container/OnboardingQuestionaire/OrgQuestions/OrgQuestions.tsx @@ -42,9 +42,9 @@ const observabilityTools = { }; const o11yFamiliarityOptions: Record = { - beginner: 'Basic Understanding', - intermediate: 'Somewhat Familiar', - expert: 'Very Familiar', + beginner: 'Beginner', + intermediate: 'Intermediate', + expert: 'Expert', notFamiliar: "I'm not familiar with it", }; @@ -254,7 +254,7 @@ function OrgQuestions({
- Are you familiar with setting upobservability (o11y)? + Are you familiar with setting up observability (o11y)?
{Object.keys(o11yFamiliarityOptions).map((option: string) => ( diff --git a/frontend/src/container/OnboardingQuestionaire/index.tsx b/frontend/src/container/OnboardingQuestionaire/index.tsx index 77bde17848..ade99c099f 100644 --- a/frontend/src/container/OnboardingQuestionaire/index.tsx +++ b/frontend/src/container/OnboardingQuestionaire/index.tsx @@ -28,7 +28,6 @@ import { SignozDetails, } from './AboutSigNozQuestions/AboutSigNozQuestions'; import InviteTeamMembers from './InviteTeamMembers/InviteTeamMembers'; -import { OnboardingFooter } from './OnboardingFooter/OnboardingFooter'; import { OnboardingHeader } from './OnboardingHeader/OnboardingHeader'; import OptimiseSignozNeeds, { OptimiseSignozDetails, @@ -148,8 +147,8 @@ function OnboardingQuestionaire(): JSX.Element { }, [org]); const isNextDisabled = - optimiseSignozDetails.logsPerDay === 0 || - optimiseSignozDetails.hostsPerDay === 0 || + optimiseSignozDetails.logsPerDay === 0 && + optimiseSignozDetails.hostsPerDay === 0 && optimiseSignozDetails.services === 0; const [isLoading, setIsLoading] = useState(false); @@ -316,10 +315,6 @@ function OnboardingQuestionaire(): JSX.Element { )}
- -
- -
); } From 896e1ccf5d4d8dbc96ba67179d38003a7510fa65 Mon Sep 17 00:00:00 2001 From: Yunus M Date: Mon, 28 Oct 2024 20:09:08 +0530 Subject: [PATCH 09/15] feat: update to use v2 instance --- frontend/src/api/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index a340f03ff1..b3a810e2ef 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -175,12 +175,12 @@ export const GatewayApiV2Instance = axios.create({ baseURL: `${ENVIRONMENT.baseURL}${gatewayApiV2}`, }); -GatewayApiV1Instance.interceptors.response.use( +GatewayApiV2Instance.interceptors.response.use( interceptorsResponse, interceptorRejected, ); -GatewayApiV1Instance.interceptors.request.use(interceptorsRequestResponse); +GatewayApiV2Instance.interceptors.request.use(interceptorsRequestResponse); // AxiosAlertManagerInstance.interceptors.response.use( From 716261b99a9da148fb2f80b837fc1003137a073f Mon Sep 17 00:00:00 2001 From: Yunus M Date: Mon, 28 Oct 2024 21:01:01 +0530 Subject: [PATCH 10/15] feat: handle linear to exponential conversion for logs, services and hosts --- .../OptimiseSignozNeeds.tsx | 42 ++++++++++++++----- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/frontend/src/container/OnboardingQuestionaire/OptimiseSignozNeeds/OptimiseSignozNeeds.tsx b/frontend/src/container/OnboardingQuestionaire/OptimiseSignozNeeds/OptimiseSignozNeeds.tsx index b3fdb9f4e7..d54b40b702 100644 --- a/frontend/src/container/OnboardingQuestionaire/OptimiseSignozNeeds/OptimiseSignozNeeds.tsx +++ b/frontend/src/container/OnboardingQuestionaire/OptimiseSignozNeeds/OptimiseSignozNeeds.tsx @@ -31,6 +31,18 @@ const linearToExponential = ( return Math.round(expValue); }; +const exponentialToLinear = ( + expValue: number, + min: number, + max: number, +): number => { + const expMin = Math.log10(min); + const expMax = Math.log10(max); + const linearValue = + ((Math.log10(expValue) - expMin) / (expMax - expMin)) * 100; + return Math.round(linearValue); // Round to get a whole number within the 0-100 range +}; + interface OptimiseSignozNeedsProps { optimiseSignozDetails: OptimiseSignozDetails; setOptimiseSignozDetails: (details: OptimiseSignozDetails) => void; @@ -84,6 +96,22 @@ function OptimiseSignozNeeds({ optimiseSignozDetails?.services || 0, ); + // Internal state for the linear slider + const [sliderValues, setSliderValues] = useState({ + logsPerDay: 0, + hostsPerDay: 0, + services: 0, + }); + + useEffect(() => { + setSliderValues({ + logsPerDay: exponentialToLinear(logsPerDay, logsMin, logsMax), + hostsPerDay: exponentialToLinear(hostsPerDay, hostsMin, hostsMax), + services: exponentialToLinear(services, servicesMin, servicesMax), + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + useEffect(() => { setOptimiseSignozDetails({ logsPerDay, @@ -129,15 +157,7 @@ function OptimiseSignozNeeds({ }); }; - // Internal state for the linear slider - const [sliderValues, setSliderValues] = useState({ - logsPerDay: 0, - hostsPerDay: 0, - services: 0, - }); - const handleSliderChange = (key: string, value: number): void => { - console.log('value', value); setSliderValues({ ...sliderValues, [key]: value, @@ -145,13 +165,13 @@ function OptimiseSignozNeeds({ switch (key) { case 'logsPerDay': - setLogsPerDay(value); + setLogsPerDay(linearToExponential(value, logsMin, logsMax)); break; case 'hostsPerDay': - setHostsPerDay(value); + setHostsPerDay(linearToExponential(value, hostsMin, hostsMax)); break; case 'services': - setServices(value); + setServices(linearToExponential(value, servicesMin, servicesMax)); break; default: break; From 6afe43cd3f657e324a439828192d9089576ede49 Mon Sep 17 00:00:00 2001 From: Yunus M Date: Tue, 29 Oct 2024 14:20:18 +0530 Subject: [PATCH 11/15] feat: handle invite user flows --- frontend/src/api/user/inviteUsers.ts | 6 +- .../AboutSigNozQuestions.tsx | 9 +- .../InviteTeamMembers.styles.scss | 47 ++++- .../InviteTeamMembers/InviteTeamMembers.tsx | 195 +++++++++++++++--- .../OnboardingQuestionaire.styles.scss | 1 - .../OptimiseSignozNeeds.tsx | 10 +- .../OrgQuestions/OrgQuestions.tsx | 2 +- .../OnboardingQuestionaire/index.tsx | 37 +++- frontend/src/types/api/user/inviteUsers.ts | 24 +++ 9 files changed, 269 insertions(+), 62 deletions(-) diff --git a/frontend/src/api/user/inviteUsers.ts b/frontend/src/api/user/inviteUsers.ts index 28189159ff..d7afb7ff53 100644 --- a/frontend/src/api/user/inviteUsers.ts +++ b/frontend/src/api/user/inviteUsers.ts @@ -1,10 +1,10 @@ import axios from 'api'; -import { ErrorResponse, SuccessResponse } from 'types/api'; -import { PayloadProps, UsersProps } from 'types/api/user/inviteUsers'; +import { SuccessResponse } from 'types/api'; +import { InviteUsersResponse, UsersProps } from 'types/api/user/inviteUsers'; const inviteUsers = async ( users: UsersProps, -): Promise | ErrorResponse> => { +): Promise> => { const response = await axios.post(`/invite/bulk`, users); return { diff --git a/frontend/src/container/OnboardingQuestionaire/AboutSigNozQuestions/AboutSigNozQuestions.tsx b/frontend/src/container/OnboardingQuestionaire/AboutSigNozQuestions/AboutSigNozQuestions.tsx index 8ebddd3430..ee7606ff3f 100644 --- a/frontend/src/container/OnboardingQuestionaire/AboutSigNozQuestions/AboutSigNozQuestions.tsx +++ b/frontend/src/container/OnboardingQuestionaire/AboutSigNozQuestions/AboutSigNozQuestions.tsx @@ -82,7 +82,7 @@ export function AboutSigNozQuestions({ otherInterestInSignoz, }); - logEvent('Onboarding: SigNoz Questions: Next', { + logEvent('User Onboarding: About SigNoz Questions Answered', { hearAboutSignoz, otherAboutSignoz, interestInSignoz, @@ -100,13 +100,6 @@ export function AboutSigNozQuestions({ otherInterestInSignoz, }); - logEvent('Onboarding: SigNoz Questions: Back', { - hearAboutSignoz, - otherAboutSignoz, - interestInSignoz, - otherInterestInSignoz, - }); - onBack(); }; diff --git a/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.styles.scss b/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.styles.scss index ae26562b3a..1d1be4b0df 100644 --- a/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.styles.scss +++ b/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.styles.scss @@ -29,24 +29,57 @@ } .questions-form-container { - .error-message-container { - padding: 16px; - margin-top: 16px; + .error-message-container, + .success-message-container, + .partially-sent-invites-container { border-radius: 4px; - border: 1px solid var(--bg-slate-500, #161922); - background: var(--bg-ink-400, #121317); width: 100%; display: flex; align-items: center; - .error-message { + .error-message, + .success-message { + font-size: 12px; + font-weight: 400; + + display: flex; + align-items: center; + gap: 8px; + } + } + + .invite-users-error-message-container, + .invite-users-success-message-container { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + + .success-message { + color: var(--bg-success-500, #00b37e); + } + } + + .partially-sent-invites-container { + margin-top: 16px; + padding: 8px; + border: 1px solid #1d212d; + background-color: #121317; + + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + + .partially-sent-invites-message { + color: var(--bg-warning-500, #fbbd23); + font-size: 12px; font-weight: 400; display: flex; align-items: center; gap: 8px; - text-transform: capitalize; } } } diff --git a/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.tsx b/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.tsx index f9d7fd3ae0..a316ee34d2 100644 --- a/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.tsx +++ b/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.tsx @@ -2,6 +2,7 @@ import './InviteTeamMembers.styles.scss'; import { Color } from '@signozhq/design-tokens'; import { Button, Input, Select, Typography } from 'antd'; +import logEvent from 'api/common/logEvent'; import inviteUsers from 'api/user/inviteUsers'; import { AxiosError } from 'axios'; import { cloneDeep, debounce, isEmpty } from 'lodash-es'; @@ -15,7 +16,12 @@ import { } from 'lucide-react'; import { useCallback, useEffect, useState } from 'react'; import { useMutation } from 'react-query'; -import { ErrorResponse } from 'types/api'; +import { SuccessResponse } from 'types/api'; +import { + FailedInvite, + InviteUsersResponse, + SuccessfulInvite, +} from 'types/api/user/inviteUsers'; import { v4 as uuid } from 'uuid'; interface TeamMember { @@ -46,8 +52,23 @@ function InviteTeamMembers({ {}, ); const [hasInvalidEmails, setHasInvalidEmails] = useState(false); + + const [hasErrors, setHasErrors] = useState(true); + const [error, setError] = useState(null); + const [inviteUsersErrorResponse, setInviteUsersErrorResponse] = useState< + string[] | null + >(null); + + const [inviteUsersSuccessResponse, setInviteUsersSuccessResponse] = useState< + string[] | null + >(null); + + const [disableNextButton, setDisableNextButton] = useState(false); + + const [allInvitesSent, setAllInvitesSent] = useState(false); + const defaultTeamMember: TeamMember = { email: '', role: 'EDITOR', @@ -100,30 +121,100 @@ function InviteTeamMembers({ return isValid; }; + const parseInviteUsersSuccessResponse = ( + response: SuccessfulInvite[], + ): string[] => response.map((invite) => `${invite.email} - Invite Sent`); + + const parseInviteUsersErrorResponse = (response: FailedInvite[]): string[] => + response.map((invite) => `${invite.email} - ${invite.error}`); + const handleError = (error: AxiosError): void => { - const errorMessage = error.response?.data as ErrorResponse; + const errorMessage = error.response?.data as InviteUsersResponse; - setError(errorMessage.error); + if (errorMessage?.status === 'failure') { + setHasErrors(true); + + const failedInvitesErrorResponse = parseInviteUsersErrorResponse( + errorMessage.failed_invites, + ); + + setInviteUsersErrorResponse(failedInvitesErrorResponse); + } }; - const { mutate: sendInvites, isLoading: isSendingInvites } = useMutation( - inviteUsers, - { - onSuccess: (): void => { + const handleInviteUsersSuccess = ( + response: SuccessResponse, + ): void => { + const inviteUsersResponse = response.payload as InviteUsersResponse; + + if (inviteUsersResponse?.status === 'success') { + const successfulInvites = parseInviteUsersSuccessResponse( + inviteUsersResponse.successful_invites, + ); + + setDisableNextButton(true); + + setError(null); + setHasErrors(false); + setInviteUsersErrorResponse(null); + setAllInvitesSent(true); + + setInviteUsersSuccessResponse(successfulInvites); + + setTimeout(() => { + setDisableNextButton(false); onNext(); - }, - onError: (error): void => { - handleError(error as AxiosError); - }, + }, 1000); + } else if (inviteUsersResponse?.status === 'partial_success') { + const successfulInvites = parseInviteUsersSuccessResponse( + inviteUsersResponse.successful_invites, + ); + + setInviteUsersSuccessResponse(successfulInvites); + + if (inviteUsersResponse.failed_invites.length > 0) { + setHasErrors(true); + + setInviteUsersErrorResponse( + parseInviteUsersErrorResponse(inviteUsersResponse.failed_invites), + ); + } + } + }; + + const { + mutate: sendInvites, + isLoading: isSendingInvites, + data: inviteUsersApiResponseData, + } = useMutation(inviteUsers, { + onSuccess: (response: SuccessResponse): void => { + logEvent('User Onboarding: Invite Team Members Sent', { + teamMembers: teamMembersToInvite, + }); + + handleInviteUsersSuccess(response); }, - ); + onError: (error: AxiosError): void => { + console.log('error', error); + + logEvent('User Onboarding: Invite Team Members Failed', { + teamMembers: teamMembersToInvite, + error, + }); + + handleError(error); + }, + }); const handleNext = (): void => { if (validateAllUsers()) { setTeamMembers(teamMembersToInvite || []); - setError(null); setHasInvalidEmails(false); + setError(null); + setHasErrors(false); + setInviteUsersErrorResponse(null); + setInviteUsersSuccessResponse(null); sendInvites({ users: teamMembersToInvite || [], @@ -165,6 +256,11 @@ function InviteTeamMembers({ }; const handleDoLater = (): void => { + logEvent('User Onboarding: Invite Team Members Skipped', { + teamMembers: teamMembersToInvite, + apiResponse: inviteUsersApiResponseData, + }); + onNext(); }; @@ -246,21 +342,65 @@ function InviteTeamMembers({
+ + {hasInvalidEmails && ( +
+ + Please enter valid emails for all team + members + +
+ )} + + {error && ( +
+ + {error} + +
+ )} + + {inviteUsersSuccessResponse && ( +
+ {inviteUsersSuccessResponse?.map((success, index) => ( + + {success} + + ))} +
+ )} + + {hasErrors && ( +
+ {inviteUsersErrorResponse?.map((error, index) => ( + + {error} + + ))} +
+ )}
- {hasInvalidEmails && ( -
- - Please enter valid emails for all team - members + {/* Partially sent invites */} + {inviteUsersSuccessResponse && inviteUsersErrorResponse && ( +
+ + + Some invites were sent successfully. Please fix the errors above and + resend invites. -
- )} - {error && ( -
- - {error} + + You can click on I'll do this later to go to next step.
)} @@ -275,10 +415,11 @@ function InviteTeamMembers({ type="primary" className="next-button" onClick={handleNext} - loading={isSendingInvites} + loading={isSendingInvites || disableNextButton} > - Send Invites - + {allInvitesSent ? 'Invites Sent' : 'Send Invites'} + + {allInvitesSent ? : }
diff --git a/frontend/src/container/OnboardingQuestionaire/OnboardingQuestionaire.styles.scss b/frontend/src/container/OnboardingQuestionaire/OnboardingQuestionaire.styles.scss index d50ca325b7..5737379d40 100644 --- a/frontend/src/container/OnboardingQuestionaire/OnboardingQuestionaire.styles.scss +++ b/frontend/src/container/OnboardingQuestionaire/OnboardingQuestionaire.styles.scss @@ -90,7 +90,6 @@ .invite-team-members-container { max-height: 260px; - padding-right: 8px; overflow-y: auto; &::-webkit-scrollbar { diff --git a/frontend/src/container/OnboardingQuestionaire/OptimiseSignozNeeds/OptimiseSignozNeeds.tsx b/frontend/src/container/OnboardingQuestionaire/OptimiseSignozNeeds/OptimiseSignozNeeds.tsx index d54b40b702..08177c1e27 100644 --- a/frontend/src/container/OnboardingQuestionaire/OptimiseSignozNeeds/OptimiseSignozNeeds.tsx +++ b/frontend/src/container/OnboardingQuestionaire/OptimiseSignozNeeds/OptimiseSignozNeeds.tsx @@ -122,7 +122,7 @@ function OptimiseSignozNeeds({ }, [services, hostsPerDay, logsPerDay]); const handleOnNext = (): void => { - logEvent('Onboarding: Optimise SigNoz Needs: Next', { + logEvent('User Onboarding: Optimise SigNoz Needs Answered', { logsPerDay, hostsPerDay, services, @@ -132,12 +132,6 @@ function OptimiseSignozNeeds({ }; const handleOnBack = (): void => { - logEvent('Onboarding: Optimise SigNoz Needs: Back', { - logsPerDay, - hostsPerDay, - services, - }); - onBack(); }; @@ -150,7 +144,7 @@ function OptimiseSignozNeeds({ onWillDoLater(); - logEvent('Onboarding: Optimise SigNoz Needs: Will do later', { + logEvent('User Onboarding: Optimise SigNoz Needs Skipped', { logsPerDay: 0, hostsPerDay: 0, services: 0, diff --git a/frontend/src/container/OnboardingQuestionaire/OrgQuestions/OrgQuestions.tsx b/frontend/src/container/OnboardingQuestionaire/OrgQuestions/OrgQuestions.tsx index 17822c2342..ab8f7d4a5c 100644 --- a/frontend/src/container/OnboardingQuestionaire/OrgQuestions/OrgQuestions.tsx +++ b/frontend/src/container/OnboardingQuestionaire/OrgQuestions/OrgQuestions.tsx @@ -120,7 +120,7 @@ function OrgQuestions({ familiarity, }); - logEvent('Onboarding: Org Questions: Next', { + logEvent('User Onboarding: Org Questions Answered', { organisationName, usesObservability, observabilityTool, diff --git a/frontend/src/container/OnboardingQuestionaire/index.tsx b/frontend/src/container/OnboardingQuestionaire/index.tsx index ade99c099f..97ebc33269 100644 --- a/frontend/src/container/OnboardingQuestionaire/index.tsx +++ b/frontend/src/container/OnboardingQuestionaire/index.tsx @@ -2,6 +2,7 @@ import './OnboardingQuestionaire.styles.scss'; import { Skeleton } from 'antd'; import { NotificationInstance } from 'antd/es/notification/interface'; +import logEvent from 'api/common/logEvent'; import updateProfileAPI from 'api/onboarding/updateProfile'; import getOrgPreference from 'api/preferences/getOrgPreference'; import updateOrgPreferenceAPI from 'api/preferences/updateOrgPreference'; @@ -67,7 +68,7 @@ const INITIAL_OPTIMISE_SIGNOZ_DETAILS: OptimiseSignozDetails = { function OnboardingQuestionaire(): JSX.Element { const { notifications } = useNotifications(); - const [currentStep, setCurrentStep] = useState(1); + const [currentStep, setCurrentStep] = useState(4); const [orgDetails, setOrgDetails] = useState(INITIAL_ORG_DETAILS); const [signozDetails, setSignozDetails] = useState( INITIAL_SIGNOZ_DETAILS, @@ -91,8 +92,6 @@ function OnboardingQuestionaire(): JSX.Element { queryKey: ['getOrgUser', org?.[0].id], }); - console.log('orgUsers', orgUsers, isLoadingOrgUsers); - const dispatch = useDispatch>(); const [orgData, setOrgData] = useState(null); const [isOnboardingComplete, setIsOnboardingComplete] = useState( @@ -126,13 +125,29 @@ function OnboardingQuestionaire(): JSX.Element { }; useEffect(() => { - const isFirstUser = checkFirstTimeUser(); + // Only run this effect if the org users and preferences are loaded + if (!isLoadingOrgUsers && !isLoadingOrgPreferences) { + const isFirstUser = checkFirstTimeUser(); + + // Redirect to get started if it's not the first user or if the onboarding is complete + if (!isFirstUser || isOnboardingComplete) { + history.push(ROUTES.GET_STARTED); - if (isOnboardingComplete || !isFirstUser) { - history.push(ROUTES.GET_STARTED); + logEvent('User Onboarding: Redirected to Get Started', { + isFirstUser, + isOnboardingComplete, + }); + } else { + logEvent('User Onboarding: Started', {}); + } } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isOnboardingComplete, orgUsers]); + }, [ + isLoadingOrgUsers, + isLoadingOrgPreferences, + isOnboardingComplete, + orgUsers, + ]); useEffect(() => { if (org) { @@ -182,8 +197,16 @@ function OnboardingQuestionaire(): JSX.Element { }, }); + logEvent('User Onboarding: Org Name Updated', { + organisationName: orgDetails.organisationName, + }); + setCurrentStep(2); } else { + logEvent('User Onboarding: Org Name Update Failed', { + organisationName: orgDetails.organisationName, + }); + notifications.error({ message: error || diff --git a/frontend/src/types/api/user/inviteUsers.ts b/frontend/src/types/api/user/inviteUsers.ts index 8491e31cc4..e173ca8f8c 100644 --- a/frontend/src/types/api/user/inviteUsers.ts +++ b/frontend/src/types/api/user/inviteUsers.ts @@ -1,5 +1,7 @@ import { User } from 'types/reducer/app'; +import { ErrorResponse } from '..'; + export interface UserProps { name: User['name']; email: User['email']; @@ -14,3 +16,25 @@ export interface UsersProps { export interface PayloadProps { data: string; } + +export interface FailedInvite { + email: string; + error: string; +} + +export interface SuccessfulInvite { + email: string; + invite_link: string; + status: string; +} + +export interface InviteUsersResponse extends ErrorResponse { + status: string; + summary: { + total_invites: number; + successful_invites: number; + failed_invites: number; + }; + successful_invites: SuccessfulInvite[]; + failed_invites: FailedInvite[]; +} From 537c8b1ca468a59a3cbe9772e7bed68adc873e67 Mon Sep 17 00:00:00 2001 From: Yunus M Date: Tue, 29 Oct 2024 19:57:00 +0530 Subject: [PATCH 12/15] feat: handle onboarding visibility --- frontend/src/AppRoutes/Private.tsx | 111 +++++++++++++- frontend/src/AppRoutes/index.tsx | 43 +++++- .../OrgQuestions/OrgQuestions.tsx | 111 +++++++++++--- .../OnboardingQuestionaire/index.tsx | 140 +++++++----------- frontend/src/store/reducers/app.ts | 15 ++ frontend/src/styles.scss | 45 ++++-- frontend/src/types/actions/app.ts | 21 ++- .../api/preferences/userOrgPreferences.ts | 4 +- frontend/src/types/reducer/app.ts | 14 ++ frontend/src/utils/permission/index.ts | 2 +- 10 files changed, 372 insertions(+), 134 deletions(-) diff --git a/frontend/src/AppRoutes/Private.tsx b/frontend/src/AppRoutes/Private.tsx index 43402fdbb2..3956676ec7 100644 --- a/frontend/src/AppRoutes/Private.tsx +++ b/frontend/src/AppRoutes/Private.tsx @@ -1,5 +1,6 @@ /* eslint-disable react-hooks/exhaustive-deps */ import getLocalStorageApi from 'api/browser/localstorage/get'; +import getOrgUser from 'api/user/getOrgUser'; import loginApi from 'api/user/login'; import { Logout } from 'api/utils'; import Spinner from 'components/Spinner'; @@ -8,8 +9,10 @@ import ROUTES from 'constants/routes'; import useLicense from 'hooks/useLicense'; import { useNotifications } from 'hooks/useNotifications'; import history from 'lib/history'; -import { ReactChild, useEffect, useMemo } from 'react'; +import { isEmpty } from 'lodash-es'; +import { ReactChild, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { useQuery } from 'react-query'; import { useDispatch, useSelector } from 'react-redux'; import { matchPath, Redirect, useLocation } from 'react-router-dom'; import { Dispatch } from 'redux'; @@ -17,6 +20,7 @@ import { AppState } from 'store/reducers'; import { getInitialUserTokenRefreshToken } from 'store/utils'; import AppActions from 'types/actions'; import { UPDATE_USER_IS_FETCH } from 'types/actions/app'; +import { Organization } from 'types/api/user/getOrganization'; import AppReducer from 'types/reducer/app'; import { routePermission } from 'utils/permission'; @@ -31,6 +35,14 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element { const location = useLocation(); const { pathname } = location; + const { org, orgPreferences } = useSelector( + (state) => state.app, + ); + + const [isOnboardingComplete, setIsOnboardingComplete] = useState< + boolean | null + >(null); + const mapRoutes = useMemo( () => new Map( @@ -44,6 +56,18 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element { [pathname], ); + useEffect(() => { + if (orgPreferences && !isEmpty(orgPreferences)) { + const onboardingPreference = orgPreferences?.find( + (preference: Record) => preference.key === 'ORG_ONBOARDING', + ); + + if (onboardingPreference) { + setIsOnboardingComplete(onboardingPreference.value); + } + } + }, [orgPreferences]); + const { data: licensesData, isFetching: isFetchingLicensesData, @@ -53,9 +77,11 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element { isUserFetching, isUserFetchingError, isLoggedIn: isLoggedInState, + isFetchingOrgPreferences, } = useSelector((state) => state.app); const { t } = useTranslation(['common']); + const localStorageUserAuthToken = getInitialUserTokenRefreshToken(); const dispatch = useDispatch>(); @@ -66,6 +92,8 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element { const isOldRoute = oldRoutes.indexOf(pathname) > -1; + const [orgData, setOrgData] = useState(undefined); + const isLocalStorageLoggedIn = getLocalStorageApi(LOCALSTORAGE.IS_LOGGED_IN) === 'true'; @@ -81,6 +109,42 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element { } }; + const { data: orgUsers, isLoading: isLoadingOrgUsers } = useQuery({ + queryFn: () => { + if (orgData && orgData.id !== undefined) { + return getOrgUser({ + orgId: orgData.id, + }); + } + return undefined; + }, + queryKey: ['getOrgUser'], + enabled: !isEmpty(orgData), + }); + + const checkFirstTimeUser = (): boolean => { + const users = orgUsers?.payload || []; + + const remainingUsers = users.filter( + (user) => user.email !== 'admin@signoz.cloud', + ); + + return remainingUsers.length === 1; + }; + + // Check if the onboarding should be shown based on the org users and onboarding completion status, wait for org users and preferences to load + const shouldShowOnboarding = (): boolean => { + // Only run this effect if the org users and preferences are loaded + if (!isLoadingOrgUsers) { + const isFirstUser = checkFirstTimeUser(); + + // Redirect to get started if it's not the first user or if the onboarding is complete + return isFirstUser && !isOnboardingComplete; + } + + return false; + }; + const handleUserLoginIfTokenPresent = async ( key: keyof typeof ROUTES, ): Promise => { @@ -102,6 +166,16 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element { response.payload.refreshJwt, ); + const showOnboarding = shouldShowOnboarding(); + + if ( + userResponse && + showOnboarding && + userResponse.payload.role === 'ADMIN' + ) { + history.push(ROUTES.ONBOARDING); + } + if ( userResponse && route && @@ -160,6 +234,35 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element { } }, [isFetchingLicensesData]); + useEffect(() => { + if (org && org.length > 0 && org[0].id !== undefined) { + setOrgData(org[0]); + } + }, [org]); + + const handleRouting = (): void => { + const showOnboarding = shouldShowOnboarding(); + + if (showOnboarding) { + history.push(ROUTES.ONBOARDING); + } else { + history.push(ROUTES.APPLICATION); + } + }; + + useEffect(() => { + // Only run this effect if the org users and preferences are loaded + if (!isLoadingOrgUsers && isOnboardingComplete !== null) { + const isFirstUser = checkFirstTimeUser(); + + // Redirect to get started if it's not the first user or if the onboarding is complete + if (isFirstUser && !isOnboardingComplete) { + history.push(ROUTES.ONBOARDING); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isLoadingOrgUsers, isOnboardingComplete, orgUsers]); + // eslint-disable-next-line sonarjs/cognitive-complexity useEffect(() => { (async (): Promise => { @@ -183,7 +286,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element { // no need to fetch the user and make user fetching false if (getLocalStorageApi(LOCALSTORAGE.IS_LOGGED_IN) === 'true') { - history.push(ROUTES.APPLICATION); + handleRouting(); } dispatch({ type: UPDATE_USER_IS_FETCH, @@ -195,7 +298,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element { } else if (pathname === ROUTES.HOME_PAGE) { // routing to application page over root page if (isLoggedInState) { - history.push(ROUTES.APPLICATION); + handleRouting(); } else { navigateToLoginIfNotLoggedIn(); } @@ -214,7 +317,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element { return ; } - if (isUserFetching) { + if (isUserFetching || (isLoggedInState && isFetchingOrgPreferences)) { return ; } diff --git a/frontend/src/AppRoutes/index.tsx b/frontend/src/AppRoutes/index.tsx index 8cb2bf0a8b..cf17fd4db3 100644 --- a/frontend/src/AppRoutes/index.tsx +++ b/frontend/src/AppRoutes/index.tsx @@ -2,6 +2,7 @@ import { ConfigProvider } from 'antd'; import getLocalStorageApi from 'api/browser/localstorage/get'; import setLocalStorageApi from 'api/browser/localstorage/set'; import logEvent from 'api/common/logEvent'; +import getAllOrgPreferences from 'api/preferences/getAllOrgPreferences'; import NotFound from 'components/NotFound'; import Spinner from 'components/Spinner'; import { FeatureKeys } from 'constants/features'; @@ -24,12 +25,17 @@ import AlertRuleProvider from 'providers/Alert'; import { DashboardProvider } from 'providers/Dashboard/Dashboard'; import { QueryBuilderProvider } from 'providers/QueryBuilder'; import { Suspense, useEffect, useState } from 'react'; +import { useQuery } from 'react-query'; import { useDispatch, useSelector } from 'react-redux'; import { Route, Router, Switch } from 'react-router-dom'; import { Dispatch } from 'redux'; import { AppState } from 'store/reducers'; import AppActions from 'types/actions'; -import { UPDATE_FEATURE_FLAG_RESPONSE } from 'types/actions/app'; +import { + UPDATE_FEATURE_FLAG_RESPONSE, + UPDATE_IS_FETCHING_ORG_PREFERENCES, + UPDATE_ORG_PREFERENCES, +} from 'types/actions/app'; import AppReducer, { User } from 'types/reducer/app'; import { extractDomain, isCloudUser, isEECloudUser } from 'utils/app'; @@ -65,6 +71,30 @@ function App(): JSX.Element { const isPremiumSupportEnabled = useFeatureFlags(FeatureKeys.PREMIUM_SUPPORT)?.active || false; + const { data: orgPreferences, isLoading: isLoadingOrgPreferences } = useQuery({ + queryFn: () => getAllOrgPreferences(), + queryKey: ['getOrgPreferences'], + enabled: isLoggedInState, + }); + + useEffect(() => { + if (orgPreferences && !isLoadingOrgPreferences) { + dispatch({ + type: UPDATE_IS_FETCHING_ORG_PREFERENCES, + payload: { + isFetchingOrgPreferences: false, + }, + }); + + dispatch({ + type: UPDATE_ORG_PREFERENCES, + payload: { + orgPreferences: orgPreferences.payload?.data || null, + }, + }); + } + }, [orgPreferences, dispatch, isLoadingOrgPreferences]); + const featureResponse = useGetFeatureFlag((allFlags) => { dispatch({ type: UPDATE_FEATURE_FLAG_RESPONSE, @@ -182,6 +212,16 @@ function App(): JSX.Element { }, [isLoggedInState, isOnBasicPlan, user]); useEffect(() => { + if (pathname === ROUTES.ONBOARDING) { + window.Intercom('update', { + hide_default_launcher: true, + }); + } else { + window.Intercom('update', { + hide_default_launcher: false, + }); + } + trackPageView(pathname); // eslint-disable-next-line react-hooks/exhaustive-deps }, [pathname]); @@ -204,6 +244,7 @@ function App(): JSX.Element { user, licenseData, isPremiumSupportEnabled, + pathname, ]); useEffect(() => { diff --git a/frontend/src/container/OnboardingQuestionaire/OrgQuestions/OrgQuestions.tsx b/frontend/src/container/OnboardingQuestionaire/OrgQuestions/OrgQuestions.tsx index ab8f7d4a5c..e0376a6559 100644 --- a/frontend/src/container/OnboardingQuestionaire/OrgQuestions/OrgQuestions.tsx +++ b/frontend/src/container/OnboardingQuestionaire/OrgQuestions/OrgQuestions.tsx @@ -4,10 +4,15 @@ import '../OnboardingQuestionaire.styles.scss'; import { Color } from '@signozhq/design-tokens'; import { Button, Input, Typography } from 'antd'; import logEvent from 'api/common/logEvent'; +import editOrg from 'api/user/editOrg'; +import { useNotifications } from 'hooks/useNotifications'; import { ArrowRight, CheckCircle, Loader2 } from 'lucide-react'; -import { useEffect, useState } from 'react'; -import { useSelector } from 'react-redux'; +import { Dispatch, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useDispatch, useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; +import AppActions from 'types/actions'; +import { UPDATE_ORG_NAME } from 'types/actions/app'; import AppReducer from 'types/reducer/app'; export interface OrgData { @@ -25,10 +30,9 @@ export interface OrgDetails { } interface OrgQuestionsProps { - isLoading: boolean; + currentOrgData: OrgData | null; orgDetails: OrgDetails; - setOrgDetails: (details: OrgDetails) => void; - onNext: () => void; + onNext: (details: OrgDetails) => void; } const observabilityTools = { @@ -49,12 +53,15 @@ const o11yFamiliarityOptions: Record = { }; function OrgQuestions({ - isLoading, + currentOrgData, orgDetails, - setOrgDetails, onNext, }: OrgQuestionsProps): JSX.Element { const { user } = useSelector((state) => state.app); + const { notifications } = useNotifications(); + const dispatch = useDispatch>(); + + const { t } = useTranslation(['organizationsettings', 'common']); const [organisationName, setOrganisationName] = useState( orgDetails?.organisationName || '', @@ -77,6 +84,78 @@ function OrgQuestions({ setOrganisationName(orgDetails.organisationName); }, [orgDetails.organisationName]); + const [isLoading, setIsLoading] = useState(false); + + const handleOrgNameUpdate = async (): Promise => { + /* Early bailout if orgData is not set or if the organisation name is not set or if the organisation name is empty or if the organisation name is the same as the one in the orgData */ + if ( + !currentOrgData || + !organisationName || + organisationName === '' || + orgDetails.organisationName === organisationName + ) { + onNext({ + organisationName, + usesObservability, + observabilityTool, + otherTool, + familiarity, + }); + + return; + } + + try { + setIsLoading(true); + const { statusCode, error } = await editOrg({ + isAnonymous: currentOrgData.isAnonymous, + name: organisationName, + orgId: currentOrgData.id, + }); + if (statusCode === 200) { + dispatch({ + type: UPDATE_ORG_NAME, + payload: { + orgId: currentOrgData?.id, + name: orgDetails.organisationName, + }, + }); + + logEvent('User Onboarding: Org Name Updated', { + organisationName: orgDetails.organisationName, + }); + + onNext({ + organisationName, + usesObservability, + observabilityTool, + otherTool, + familiarity, + }); + } else { + logEvent('User Onboarding: Org Name Update Failed', { + organisationName: orgDetails.organisationName, + }); + + notifications.error({ + message: + error || + t('something_went_wrong', { + ns: 'common', + }), + }); + } + setIsLoading(false); + } catch (error) { + setIsLoading(false); + notifications.error({ + message: t('something_went_wrong', { + ns: 'common', + }), + }); + } + }; + const isValidUsesObservability = (): boolean => { if (usesObservability === null) { return false; @@ -112,23 +191,7 @@ function OrgQuestions({ ]); const handleOnNext = (): void => { - setOrgDetails({ - organisationName, - usesObservability, - observabilityTool, - otherTool, - familiarity, - }); - - logEvent('User Onboarding: Org Questions Answered', { - organisationName, - usesObservability, - observabilityTool, - otherTool, - familiarity, - }); - - onNext(); + handleOrgNameUpdate(); }; return ( diff --git a/frontend/src/container/OnboardingQuestionaire/index.tsx b/frontend/src/container/OnboardingQuestionaire/index.tsx index 97ebc33269..1917cd7a55 100644 --- a/frontend/src/container/OnboardingQuestionaire/index.tsx +++ b/frontend/src/container/OnboardingQuestionaire/index.tsx @@ -4,9 +4,9 @@ import { Skeleton } from 'antd'; import { NotificationInstance } from 'antd/es/notification/interface'; import logEvent from 'api/common/logEvent'; import updateProfileAPI from 'api/onboarding/updateProfile'; +import getAllOrgPreferences from 'api/preferences/getAllOrgPreferences'; import getOrgPreference from 'api/preferences/getOrgPreference'; import updateOrgPreferenceAPI from 'api/preferences/updateOrgPreference'; -import editOrg from 'api/user/editOrg'; import getOrgUser from 'api/user/getOrgUser'; import { AxiosError } from 'axios'; import { SOMETHING_WENT_WRONG } from 'constants/api'; @@ -16,12 +16,14 @@ import { useNotifications } from 'hooks/useNotifications'; import history from 'lib/history'; import { isEmpty } from 'lodash-es'; import { Dispatch, useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; import { useMutation, useQuery } from 'react-query'; import { useDispatch, useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; import AppActions from 'types/actions'; -import { UPDATE_ORG_NAME } from 'types/actions/app'; +import { + UPDATE_IS_FETCHING_ORG_PREFERENCES, + UPDATE_ORG_PREFERENCES, +} from 'types/actions/app'; import AppReducer from 'types/reducer/app'; import { @@ -67,8 +69,8 @@ const INITIAL_OPTIMISE_SIGNOZ_DETAILS: OptimiseSignozDetails = { function OnboardingQuestionaire(): JSX.Element { const { notifications } = useNotifications(); - - const [currentStep, setCurrentStep] = useState(4); + const { org } = useSelector((state) => state.app); + const [currentStep, setCurrentStep] = useState(1); const [orgDetails, setOrgDetails] = useState(INITIAL_ORG_DETAILS); const [signozDetails, setSignozDetails] = useState( INITIAL_SIGNOZ_DETAILS, @@ -81,9 +83,6 @@ function OnboardingQuestionaire(): JSX.Element { InviteTeamMembersProps[] | null >(null); - const { t } = useTranslation(['organizationsettings', 'common']); - const { org } = useSelector((state) => state.app); - const { data: orgUsers, isLoading: isLoadingOrgUsers } = useQuery({ queryFn: () => getOrgUser({ @@ -93,26 +92,57 @@ function OnboardingQuestionaire(): JSX.Element { }); const dispatch = useDispatch>(); - const [orgData, setOrgData] = useState(null); + const [currentOrgData, setCurrentOrgData] = useState(null); const [isOnboardingComplete, setIsOnboardingComplete] = useState( false, ); - const { data: orgPreferences, isLoading: isLoadingOrgPreferences } = useQuery({ + const { + data: onboardingPreferenceData, + isLoading: isLoadingOnboardingPreference, + } = useQuery({ queryFn: () => getOrgPreference({ preferenceID: 'ORG_ONBOARDING' }), queryKey: ['getOrgPreferences', 'ORG_ONBOARDING'], }); + const { data: orgPreferences, isLoading: isLoadingOrgPreferences } = useQuery({ + queryFn: () => getAllOrgPreferences(), + queryKey: ['getOrgPreferences'], + enabled: isOnboardingComplete, + }); + + useEffect(() => { + if (orgPreferences && !isLoadingOrgPreferences) { + dispatch({ + type: UPDATE_IS_FETCHING_ORG_PREFERENCES, + payload: { + isFetchingOrgPreferences: false, + }, + }); + + dispatch({ + type: UPDATE_ORG_PREFERENCES, + payload: { + orgPreferences: orgPreferences.payload?.data || null, + }, + }); + } + }, [orgPreferences, dispatch, isLoadingOrgPreferences]); + useEffect(() => { - if (!isLoadingOrgPreferences && !isEmpty(orgPreferences?.payload?.data)) { - const preferenceId = orgPreferences?.payload?.data?.preference_id; - const preferenceValue = orgPreferences?.payload?.data?.preference_value; + if ( + !isLoadingOnboardingPreference && + !isEmpty(onboardingPreferenceData?.payload?.data) + ) { + const preferenceId = onboardingPreferenceData?.payload?.data?.preference_id; + const preferenceValue = + onboardingPreferenceData?.payload?.data?.preference_value; if (preferenceId === 'ORG_ONBOARDING') { setIsOnboardingComplete(preferenceValue as boolean); } } - }, [orgPreferences, isLoadingOrgPreferences]); + }, [onboardingPreferenceData, isLoadingOnboardingPreference]); const checkFirstTimeUser = (): boolean => { const users = orgUsers?.payload || []; @@ -126,7 +156,7 @@ function OnboardingQuestionaire(): JSX.Element { useEffect(() => { // Only run this effect if the org users and preferences are loaded - if (!isLoadingOrgUsers && !isLoadingOrgPreferences) { + if (!isLoadingOrgUsers && !isLoadingOnboardingPreference) { const isFirstUser = checkFirstTimeUser(); // Redirect to get started if it's not the first user or if the onboarding is complete @@ -144,14 +174,14 @@ function OnboardingQuestionaire(): JSX.Element { // eslint-disable-next-line react-hooks/exhaustive-deps }, [ isLoadingOrgUsers, - isLoadingOrgPreferences, + isLoadingOnboardingPreference, isOnboardingComplete, orgUsers, ]); useEffect(() => { if (org) { - setOrgData(org[0]); + setCurrentOrgData(org[0]); setOrgDetails({ ...orgDetails, @@ -166,70 +196,6 @@ function OnboardingQuestionaire(): JSX.Element { optimiseSignozDetails.hostsPerDay === 0 && optimiseSignozDetails.services === 0; - const [isLoading, setIsLoading] = useState(false); - - const handleOrgNameUpdate = async (): Promise => { - /* Early bailout if orgData is not set or if the organisation name is not set or if the organisation name is empty or if the organisation name is the same as the one in the orgData */ - if ( - !orgData || - !orgDetails.organisationName || - orgDetails.organisationName === '' || - orgData.name === orgDetails.organisationName - ) { - setCurrentStep(2); - - return; - } - - try { - setIsLoading(true); - const { statusCode, error } = await editOrg({ - isAnonymous: orgData?.isAnonymous, - name: orgDetails.organisationName, - orgId: orgData?.id, - }); - if (statusCode === 200) { - dispatch({ - type: UPDATE_ORG_NAME, - payload: { - orgId: orgData?.id, - name: orgDetails.organisationName, - }, - }); - - logEvent('User Onboarding: Org Name Updated', { - organisationName: orgDetails.organisationName, - }); - - setCurrentStep(2); - } else { - logEvent('User Onboarding: Org Name Update Failed', { - organisationName: orgDetails.organisationName, - }); - - notifications.error({ - message: - error || - t('something_went_wrong', { - ns: 'common', - }), - }); - } - setIsLoading(false); - } catch (error) { - setIsLoading(false); - notifications.error({ - message: t('something_went_wrong', { - ns: 'common', - }), - }); - } - }; - - const handleOrgDetailsUpdate = (): void => { - handleOrgNameUpdate(); - }; - const { mutate: updateProfile, isLoading: isUpdatingProfile } = useMutation( updateProfileAPI, { @@ -289,20 +255,22 @@ function OnboardingQuestionaire(): JSX.Element {
- {(isLoadingOrgPreferences || isLoadingOrgUsers) && ( + {(isLoadingOnboardingPreference || isLoadingOrgUsers) && (
)} - {!isLoadingOrgPreferences && !isLoadingOrgUsers && ( + {!isLoadingOnboardingPreference && !isLoadingOrgUsers && ( <> {currentStep === 1 && ( { + setOrgDetails(orgDetails); + setCurrentStep(2); + }} /> )} diff --git a/frontend/src/store/reducers/app.ts b/frontend/src/store/reducers/app.ts index bdb80d2565..3ae5df9699 100644 --- a/frontend/src/store/reducers/app.ts +++ b/frontend/src/store/reducers/app.ts @@ -8,10 +8,12 @@ import { UPDATE_CURRENT_ERROR, UPDATE_CURRENT_VERSION, UPDATE_FEATURE_FLAG_RESPONSE, + UPDATE_IS_FETCHING_ORG_PREFERENCES, UPDATE_LATEST_VERSION, UPDATE_LATEST_VERSION_ERROR, UPDATE_ORG, UPDATE_ORG_NAME, + UPDATE_ORG_PREFERENCES, UPDATE_USER, UPDATE_USER_ACCESS_REFRESH_ACCESS_TOKEN, UPDATE_USER_FLAG, @@ -59,6 +61,8 @@ const InitialValue: InitialValueTypes = { userFlags: {}, ee: 'Y', setupCompleted: true, + orgPreferences: null, + isFetchingOrgPreferences: true, }; const appReducer = ( @@ -73,6 +77,17 @@ const appReducer = ( }; } + case UPDATE_ORG_PREFERENCES: { + return { ...state, orgPreferences: action.payload.orgPreferences }; + } + + case UPDATE_IS_FETCHING_ORG_PREFERENCES: { + return { + ...state, + isFetchingOrgPreferences: action.payload.isFetchingOrgPreferences, + }; + } + case UPDATE_FEATURE_FLAG_RESPONSE: { return { ...state, diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index 130464a980..18db88688f 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -276,26 +276,39 @@ notifications - 2050 } @font-face { - font-family: 'Inter'; - src: url('../public/fonts/Inter-VariableFont_opsz,wght.ttf') format('truetype'); - font-weight: 300 700; - font-style: normal; + font-family: 'Inter'; + src: url('../public/fonts/Inter-VariableFont_opsz,wght.ttf') format('truetype'); + font-weight: 300 700; + font-style: normal; } @font-face { - font-family: 'Work Sans'; - src: url('../public/fonts/WorkSans-VariableFont_wght.ttf') format('truetype'); - font-weight: 500; - font-style: normal; + font-family: 'Work Sans'; + src: url('../public/fonts/WorkSans-VariableFont_wght.ttf') format('truetype'); + font-weight: 500; + font-style: normal; } @font-face { - font-family: 'Space Mono'; - src: url('../public/fonts/SpaceMono-Regular.ttf') format('truetype'); - font-weight: normal; - font-style: normal; + font-family: 'Space Mono'; + src: url('../public/fonts/SpaceMono-Regular.ttf') format('truetype'); + font-weight: normal; + font-style: normal; } @font-face { - font-family: 'Fira Code'; - src: url('../public/fonts/FiraCode-VariableFont_wght.ttf') format('truetype'); - font-weight: 300 700; - font-style: normal; + font-family: 'Fira Code'; + src: url('../public/fonts/FiraCode-VariableFont_wght.ttf') format('truetype'); + font-weight: 300 700; + font-style: normal; +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +.animate-spin { + animation: spin 1s linear infinite; } diff --git a/frontend/src/types/actions/app.ts b/frontend/src/types/actions/app.ts index 54b1992af2..93a9dac5df 100644 --- a/frontend/src/types/actions/app.ts +++ b/frontend/src/types/actions/app.ts @@ -25,7 +25,10 @@ export const UPDATE_ORG_NAME = 'UPDATE_ORG_NAME'; export const UPDATE_ORG = 'UPDATE_ORG'; export const UPDATE_CONFIGS = 'UPDATE_CONFIGS'; export const UPDATE_USER_FLAG = 'UPDATE_USER_FLAG'; +export const UPDATE_ORG_PREFERENCES = 'UPDATE_ORG_PREFERENCES'; export const UPDATE_FEATURE_FLAG_RESPONSE = 'UPDATE_FEATURE_FLAG_RESPONSE'; +export const UPDATE_IS_FETCHING_ORG_PREFERENCES = + 'UPDATE_IS_FETCHING_ORG_PREFERENCES'; export interface LoggedInUser { type: typeof LOGGED_IN; @@ -130,6 +133,20 @@ export interface UpdateFeatureFlag { }; } +export interface UpdateOrgPreferences { + type: typeof UPDATE_ORG_PREFERENCES; + payload: { + orgPreferences: AppReducer['orgPreferences']; + }; +} + +export interface UpdateIsFetchingOrgPreferences { + type: typeof UPDATE_IS_FETCHING_ORG_PREFERENCES; + payload: { + isFetchingOrgPreferences: AppReducer['isFetchingOrgPreferences']; + }; +} + export type AppAction = | LoggedInUser | UpdateAppVersion @@ -143,4 +160,6 @@ export type AppAction = | UpdateOrg | UpdateConfigs | UpdateUserFlag - | UpdateFeatureFlag; + | UpdateFeatureFlag + | UpdateOrgPreferences + | UpdateIsFetchingOrgPreferences; diff --git a/frontend/src/types/api/preferences/userOrgPreferences.ts b/frontend/src/types/api/preferences/userOrgPreferences.ts index 2c77090769..ab9292596f 100644 --- a/frontend/src/types/api/preferences/userOrgPreferences.ts +++ b/frontend/src/types/api/preferences/userOrgPreferences.ts @@ -1,3 +1,5 @@ +import { OrgPreference } from 'types/reducer/app'; + export interface GetOrgPreferenceResponseProps { status: string; data: Record; @@ -10,7 +12,7 @@ export interface GetUserPreferenceResponseProps { export interface GetAllOrgPreferencesResponseProps { status: string; - data: Record; + data: OrgPreference[]; } export interface GetAllUserPreferencesResponseProps { diff --git a/frontend/src/types/reducer/app.ts b/frontend/src/types/reducer/app.ts index c51defcfb0..cfc431ab03 100644 --- a/frontend/src/types/reducer/app.ts +++ b/frontend/src/types/reducer/app.ts @@ -15,6 +15,18 @@ export interface User { profilePictureURL: UserPayload['profilePictureURL']; } +export interface OrgPreference { + key: string; + name: string; + description: string; + valueType: string; + defaultValue: boolean; + allowedValues: any[]; + isDiscreteValues: boolean; + allowedScopes: string[]; + value: boolean; +} + export default interface AppReducer { isLoggedIn: boolean; currentVersion: string; @@ -30,6 +42,8 @@ export default interface AppReducer { userFlags: null | UserFlags; ee: 'Y' | 'N'; setupCompleted: boolean; + orgPreferences: OrgPreference[] | null; + isFetchingOrgPreferences: boolean; featureResponse: { data: FeatureFlagPayload[] | null; refetch: QueryObserverBaseResult['refetch']; diff --git a/frontend/src/utils/permission/index.ts b/frontend/src/utils/permission/index.ts index e58843232a..bd27d04934 100644 --- a/frontend/src/utils/permission/index.ts +++ b/frontend/src/utils/permission/index.ts @@ -86,7 +86,7 @@ export const routePermission: Record = { LOGS_PIPELINES: ['ADMIN', 'EDITOR', 'VIEWER'], TRACE_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'], GET_STARTED: ['ADMIN', 'EDITOR', 'VIEWER'], - ONBOARDING: ['ADMIN', 'EDITOR', 'VIEWER'], + ONBOARDING: ['ADMIN'], GET_STARTED_APPLICATION_MONITORING: ['ADMIN', 'EDITOR', 'VIEWER'], GET_STARTED_INFRASTRUCTURE_MONITORING: ['ADMIN', 'EDITOR', 'VIEWER'], GET_STARTED_LOGS_MANAGEMENT: ['ADMIN', 'EDITOR', 'VIEWER'], From 4c573753215e39b5921c45d7aa93aa7bfc76ef97 Mon Sep 17 00:00:00 2001 From: Yunus M Date: Wed, 30 Oct 2024 12:32:56 +0530 Subject: [PATCH 13/15] chore: update org onboarding package --- frontend/src/AppRoutes/pageComponents.ts | 4 ++-- frontend/src/AppRoutes/routes.ts | 4 ++-- frontend/src/pages/OnboardingPageV2/index.tsx | 3 --- .../OnboardingPageV2.tsx => OrgOnboarding/OrgOnboarding.tsx} | 4 ++-- frontend/src/pages/OrgOnboarding/index.tsx | 3 +++ 5 files changed, 9 insertions(+), 9 deletions(-) delete mode 100644 frontend/src/pages/OnboardingPageV2/index.tsx rename frontend/src/pages/{OnboardingPageV2/OnboardingPageV2.tsx => OrgOnboarding/OrgOnboarding.tsx} (68%) create mode 100644 frontend/src/pages/OrgOnboarding/index.tsx diff --git a/frontend/src/AppRoutes/pageComponents.ts b/frontend/src/AppRoutes/pageComponents.ts index c267304e6b..2def2ac11f 100644 --- a/frontend/src/AppRoutes/pageComponents.ts +++ b/frontend/src/AppRoutes/pageComponents.ts @@ -66,8 +66,8 @@ export const Onboarding = Loadable( () => import(/* webpackChunkName: "Onboarding" */ 'pages/OnboardingPage'), ); -export const OnboardingV2 = Loadable( - () => import(/* webpackChunkName: "Onboarding" */ 'pages/OnboardingPageV2'), +export const OrgOnboarding = Loadable( + () => import(/* webpackChunkName: "OrgOnboarding" */ 'pages/OrgOnboarding'), ); export const DashboardPage = Loadable( diff --git a/frontend/src/AppRoutes/routes.ts b/frontend/src/AppRoutes/routes.ts index 1901095a67..ccbb700387 100644 --- a/frontend/src/AppRoutes/routes.ts +++ b/frontend/src/AppRoutes/routes.ts @@ -31,8 +31,8 @@ import { NewDashboardPage, OldLogsExplorer, Onboarding, - OnboardingV2, OrganizationSettings, + OrgOnboarding, PasswordReset, PipelinePage, ServiceMapPage, @@ -72,7 +72,7 @@ const routes: AppRoutes[] = [ { path: ROUTES.ONBOARDING, exact: false, - component: OnboardingV2, + component: OrgOnboarding, isPrivate: true, key: 'ONBOARDING', }, diff --git a/frontend/src/pages/OnboardingPageV2/index.tsx b/frontend/src/pages/OnboardingPageV2/index.tsx deleted file mode 100644 index 9fad953dfe..0000000000 --- a/frontend/src/pages/OnboardingPageV2/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import OnboardingPage from './OnboardingPageV2'; - -export default OnboardingPage; diff --git a/frontend/src/pages/OnboardingPageV2/OnboardingPageV2.tsx b/frontend/src/pages/OrgOnboarding/OrgOnboarding.tsx similarity index 68% rename from frontend/src/pages/OnboardingPageV2/OnboardingPageV2.tsx rename to frontend/src/pages/OrgOnboarding/OrgOnboarding.tsx index 7100894a41..33e1523577 100644 --- a/frontend/src/pages/OnboardingPageV2/OnboardingPageV2.tsx +++ b/frontend/src/pages/OrgOnboarding/OrgOnboarding.tsx @@ -1,6 +1,6 @@ import OnboardingQuestionaire from 'container/OnboardingQuestionaire'; -function OnboardingPageV2(): JSX.Element { +function OrgOnboarding(): JSX.Element { return (
@@ -8,4 +8,4 @@ function OnboardingPageV2(): JSX.Element { ); } -export default OnboardingPageV2; +export default OrgOnboarding; diff --git a/frontend/src/pages/OrgOnboarding/index.tsx b/frontend/src/pages/OrgOnboarding/index.tsx new file mode 100644 index 0000000000..64e413ce30 --- /dev/null +++ b/frontend/src/pages/OrgOnboarding/index.tsx @@ -0,0 +1,3 @@ +import OnboardingPage from './OrgOnboarding'; + +export default OnboardingPage; From addddf3df14b5808d00d800822b48c5aab221158 Mon Sep 17 00:00:00 2001 From: Yunus M Date: Wed, 30 Oct 2024 13:08:43 +0530 Subject: [PATCH 14/15] chore: remove workspace from fullscreen --- frontend/src/container/AppLayout/index.tsx | 1 - .../InviteTeamMembers/InviteTeamMembers.tsx | 2 -- 2 files changed, 3 deletions(-) diff --git a/frontend/src/container/AppLayout/index.tsx b/frontend/src/container/AppLayout/index.tsx index 700914cc6a..d9beb2d5d3 100644 --- a/frontend/src/container/AppLayout/index.tsx +++ b/frontend/src/container/AppLayout/index.tsx @@ -192,7 +192,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element { const renderFullScreen = pathname === ROUTES.GET_STARTED || pathname === ROUTES.ONBOARDING || - pathname === ROUTES.WORKSPACE_LOCKED || pathname === ROUTES.GET_STARTED_APPLICATION_MONITORING || pathname === ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING || pathname === ROUTES.GET_STARTED_LOGS_MANAGEMENT || diff --git a/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.tsx b/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.tsx index a316ee34d2..0f99dd315b 100644 --- a/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.tsx +++ b/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.tsx @@ -195,8 +195,6 @@ function InviteTeamMembers({ handleInviteUsersSuccess(response); }, onError: (error: AxiosError): void => { - console.log('error', error); - logEvent('User Onboarding: Invite Team Members Failed', { teamMembers: teamMembersToInvite, error, From 5ae754cdd946a8a5564a7d94bc01f6c8e0dba0cc Mon Sep 17 00:00:00 2001 From: Yunus M Date: Wed, 30 Oct 2024 13:40:13 +0530 Subject: [PATCH 15/15] feat: handle light mode and remove unnecessary logos --- frontend/public/Logos/hippa.svg | 10 - .../public/Logos/signoz-brand-logo-new.svg | 30 --- frontend/public/Logos/soc2.svg | 72 ----- frontend/public/fonts/Satoshi-Regular.woff2 | Bin 25516 -> 0 bytes .../InviteTeamMembers.styles.scss | 37 +++ .../OnboardingHeader.styles.scss | 16 +- .../OnboardingHeader/OnboardingHeader.tsx | 2 +- .../OnboardingQuestionaire.styles.scss | 245 ++++++++++++++++-- 8 files changed, 267 insertions(+), 145 deletions(-) delete mode 100644 frontend/public/Logos/hippa.svg delete mode 100644 frontend/public/Logos/signoz-brand-logo-new.svg delete mode 100644 frontend/public/Logos/soc2.svg delete mode 100644 frontend/public/fonts/Satoshi-Regular.woff2 diff --git a/frontend/public/Logos/hippa.svg b/frontend/public/Logos/hippa.svg deleted file mode 100644 index 2a1b74b818..0000000000 --- a/frontend/public/Logos/hippa.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/frontend/public/Logos/signoz-brand-logo-new.svg b/frontend/public/Logos/signoz-brand-logo-new.svg deleted file mode 100644 index f57d3c5def..0000000000 --- a/frontend/public/Logos/signoz-brand-logo-new.svg +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/frontend/public/Logos/soc2.svg b/frontend/public/Logos/soc2.svg deleted file mode 100644 index 234321c95c..0000000000 --- a/frontend/public/Logos/soc2.svg +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/frontend/public/fonts/Satoshi-Regular.woff2 b/frontend/public/fonts/Satoshi-Regular.woff2 deleted file mode 100644 index 81c40ab08a87d975b0e61e0d8497fe4dd3c11da4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25516 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 diff --git a/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.styles.scss b/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.styles.scss index 1d1be4b0df..2f47e1d3e3 100644 --- a/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.styles.scss +++ b/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.styles.scss @@ -83,3 +83,40 @@ } } } + +.lightMode { + .team-member-container { + .team-member-role-select { + .ant-select-selector { + border: 1px solid var(--bg-vanilla-300); + } + } + + .team-member-email-input { + background-color: var(--bg-vanilla-100); + + .ant-input, + .ant-input-group-addon { + background-color: var(--bg-vanilla-100) !important; + } + } + } + + .questions-form-container { + .invite-users-error-message-container, + .invite-users-success-message-container { + .success-message { + color: var(--bg-success-500, #00b37e); + } + } + + .partially-sent-invites-container { + border: 1px solid var(--bg-vanilla-300); + background-color: var(--bg-vanilla-100); + + .partially-sent-invites-message { + color: var(--bg-warning-500, #fbbd23); + } + } + } +} diff --git a/frontend/src/container/OnboardingQuestionaire/OnboardingHeader/OnboardingHeader.styles.scss b/frontend/src/container/OnboardingQuestionaire/OnboardingHeader/OnboardingHeader.styles.scss index 984b89828b..0b37c654be 100644 --- a/frontend/src/container/OnboardingQuestionaire/OnboardingHeader/OnboardingHeader.styles.scss +++ b/frontend/src/container/OnboardingQuestionaire/OnboardingHeader/OnboardingHeader.styles.scss @@ -20,8 +20,8 @@ } .header-container .logo-text { - font-family: 'Satoshi', sans-serif; - color: #fff; + font-family: 'Work Sans', sans-serif; + color: var(--bg-vanilla-100); font-size: 15.4px; font-style: normal; font-weight: 500; @@ -38,8 +38,8 @@ gap: 6px; flex-shrink: 0; border-radius: 2px; - border: 1px solid var(--Greyscale-Slate-400, #1d212d); - background: var(--Ink-300, #16181d); + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); box-shadow: none; } @@ -50,10 +50,16 @@ } .header-container .get-help-text { - color: var(--Vanilla-400, var(--Greyscale-Vanilla-400, #c0c1c3)); + color: var(--bg-vanilla-400); font-size: 12px; font-style: normal; font-weight: 400; line-height: 10px; letter-spacing: 0.12px; } + +.lightMode { + .header-container .logo-text { + color: var(--bg-slate-300); + } +} diff --git a/frontend/src/container/OnboardingQuestionaire/OnboardingHeader/OnboardingHeader.tsx b/frontend/src/container/OnboardingQuestionaire/OnboardingHeader/OnboardingHeader.tsx index b8982baeb1..9d4df97676 100644 --- a/frontend/src/container/OnboardingQuestionaire/OnboardingHeader/OnboardingHeader.tsx +++ b/frontend/src/container/OnboardingQuestionaire/OnboardingHeader/OnboardingHeader.tsx @@ -4,7 +4,7 @@ export function OnboardingHeader(): JSX.Element { return (
- SigNoz + SigNoz SigNoz
diff --git a/frontend/src/container/OnboardingQuestionaire/OnboardingQuestionaire.styles.scss b/frontend/src/container/OnboardingQuestionaire/OnboardingQuestionaire.styles.scss index 5737379d40..22cf4c6b2a 100644 --- a/frontend/src/container/OnboardingQuestionaire/OnboardingQuestionaire.styles.scss +++ b/frontend/src/container/OnboardingQuestionaire/OnboardingQuestionaire.styles.scss @@ -24,7 +24,7 @@ overflow-y: auto; .questions-container { - color: var(--Vanilla-100, #fff); + color: var(--bg-vanilla-100, #fff); font-family: Inter; font-size: 24px; font-style: normal; @@ -37,14 +37,14 @@ } .title { - color: var(--Vanilla-100, #fff) !important; + color: var(--bg-vanilla-100) !important; font-size: 24px !important; line-height: 32px !important; margin-bottom: 8px !important; } .sub-title { - color: var(--Vanilla-400, var(--Greyscale-Vanilla-400, #c0c1c3)) !important; + color: var(--bg-vanilla-400) !important; font-size: 14px !important; font-style: normal; font-weight: 400 !important; @@ -68,15 +68,15 @@ align-items: center; gap: 24px; border-radius: 4px; - border: 1px solid var(--Greyscale-Slate-500, #161922); - background: var(--Ink-400, #121317); + border: 1px solid var(--bg-slate-500); + background: var(--bg-ink-400); .ant-form-item { margin-bottom: 0px !important; .ant-form-item-label { label { - color: var(--Vanilla-100, #fff) !important; + color: var(--bg-vanilla-100) !important; font-size: 13px !important; font-weight: 500; line-height: 20px; @@ -123,8 +123,8 @@ height: 32px; background: var(--Ink-300, #16181d); - border: 1px solid var(--Greyscale-Slate-400, #1d212d); - color: var(--Vanilla-400, var(--Greyscale-Vanilla-400, #c0c1c3)); + border: 1px solid var(--bg-slate-400); + color: var(--bg-vanilla-400); } .ant-input-group-addon { @@ -134,13 +134,13 @@ background: var(--Ink-300, #16181d); border: 1px solid var(--Greyscale-Slate-400, #1d212d); border-left: 0px; - color: var(--Vanilla-400, var(--Greyscale-Vanilla-400, #c0c1c3)); + color: var(--bg-vanilla-400); } } } .question-label { - color: var(--Vanilla-100, #fff); + color: var(--bg-vanilla-100); font-size: 13px; font-style: normal; font-weight: 500; @@ -148,7 +148,7 @@ } .question-sub-label { - color: var(--Vanilla-400, var(--Greyscale-Vanilla-400, #c0c1c3)); + color: var(--bg-vanilla-400); font-size: 11px; font-style: normal; font-weight: 400; @@ -192,7 +192,7 @@ } .question { - color: var(--Vanilla-100, #fff); + color: var(--bg-vanilla-100); font-size: 14px; font-style: normal; font-weight: 500; @@ -209,9 +209,9 @@ border-radius: 2px; font-size: 14px; height: 40px; - border: 1px solid var(--Greyscale-Slate-400, #1d212d); - background: var(--Ink-300, #16181d); - color: #fff; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + color: var(--bg-vanilla-100); &:focus-visible { outline: none; @@ -233,10 +233,10 @@ .grid-button, .tool-button { border-radius: 4px; - border: 1px solid var(--Greyscale-Slate-400, #1d212d); - background: var(--Ink-300, #16181d); + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); padding: 12px; - color: var(--Vanilla-400, var(--Greyscale-Vanilla-400, #c0c1c3)); + color: var(--bg-vanilla-400); font-size: 14px; font-style: normal; text-align: left; @@ -271,9 +271,9 @@ align-items: center; justify-content: space-between; border-radius: 2px; - border: 1px solid var(--Greyscale-Slate-400, #1d212d); - background: var(--Ink-300, #16181d); - color: var(--Vanilla-400, var(--Greyscale-Vanilla-400, #c0c1c3)); + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + color: var(--bg-vanilla-400); box-shadow: none; font-size: 14px; font-style: normal; @@ -310,10 +310,10 @@ align-items: center; justify-content: center; - border: 1px solid #1d212d; + border: 1px solid var(--bg-slate-400); border-top-left-radius: 0px; border-bottom-left-radius: 0px; - background-color: #121317; + background-color: var(--bg-ink-300); border-left: 0px; border-top-left-radius: 0px; @@ -336,9 +336,9 @@ .input-field { flex: 0; padding: 12px; - border: 1px solid var(--Greyscale-Slate-400, #1d212d); - background: var(--Ink-300, #16181d); - color: #fff; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + color: var(--bg-vanilla-100); border-radius: 4px; font-size: 14px; min-width: 258px; @@ -366,7 +366,7 @@ .arrow { font-size: 18px; - color: #fff; + color: var(--bg-vanilla-100); } } @@ -395,3 +395,194 @@ align-items: center; margin: 0 auto; } + +.lightMode { + .onboarding-questionaire-container { + .onboarding-questionaire-content { + .questions-container { + color: var(--bg-slate-300); + } + + .title { + color: var(--bg-slate-300) !important; + } + + .sub-title { + color: var(--bg-slate-400) !important; + } + + .questions-form { + width: 100%; + display: flex; + min-height: 420px; + padding: 20px 24px 24px 24px; + flex-direction: column; + align-items: center; + gap: 24px; + border-radius: 4px; + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100); + + .ant-form-item { + margin-bottom: 0px !important; + + .ant-form-item-label { + label { + color: var(--bg-slate-300) !important; + font-size: 13px; + font-weight: 500; + line-height: 20px; + } + } + } + + &.invite-team-members-form { + .invite-team-members-container { + max-height: 260px; + overflow-y: auto; + + &::-webkit-scrollbar { + width: 0.1rem; + } + &::-webkit-scrollbar-corner { + background: transparent; + } + &::-webkit-scrollbar-thumb { + background: rgb(136, 136, 136); + border-radius: 0.625rem; + } + &::-webkit-scrollbar-track { + background: transparent; + } + } + } + } + + .invite-team-members-container { + .ant-input-group { + .ant-input { + background: var(--bg-vanilla-100); + border: 1px solid var(--bg-vanilla-300); + color: var(--bg-slate-300); + } + + .ant-input-group-addon { + font-size: 11px; + height: 32px; + min-width: 80px; + background: var(--bg-vanilla-100); + border: 1px solid var(--bg-vanilla-300); + border-left: 0px; + color: var(--bg-slate-300); + } + } + } + + .question-label { + color: var(--bg-slate-300); + } + + .question-sub-label { + color: var(--bg-slate-400); + } + + .question { + color: var(--bg-slate-300); + } + + input[type='text'] { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100); + color: var(--text-ink-300); + } + + .radio-button, + .grid-button, + .tool-button { + border-radius: 4px; + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100); + padding: 12px; + color: var(--bg-slate-300); + font-size: 14px; + font-style: normal; + text-align: left; + font-weight: 400; + transition: background-color 0.3s ease; + min-width: 258px; + cursor: pointer; + box-sizing: border-box; + } + + .radio-button.active, + .grid-button.active, + .tool-button.active, + .radio-button:hover, + .grid-button:hover, + .tool-button:hover { + border: 1px solid rgba(78, 116, 248, 0.4); + background: rgba(78, 116, 248, 0.2); + } + + .onboarding-questionaire-button, + .add-another-member-button, + .remove-team-member-button { + display: flex; + align-items: center; + justify-content: space-between; + border-radius: 2px; + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100); + color: var(--bg-ink-300); + box-shadow: none; + font-size: 14px; + font-style: normal; + text-align: left; + font-weight: 400; + transition: background-color 0.3s ease; + cursor: pointer; + height: 40px; + box-sizing: border-box; + + &:hover { + border: 1px solid rgba(78, 116, 248, 0.4); + background: rgba(78, 116, 248, 0.2); + } + + &:focus-visible { + outline: none; + } + + &.active { + border: 1px solid rgba(78, 116, 248, 0.4); + background: rgba(78, 116, 248, 0.2); + } + } + + .remove-team-member-button { + display: flex; + align-items: center; + justify-content: center; + + border: 1px solid var(--bg-vanilla-300); + border-top-left-radius: 0px; + border-bottom-left-radius: 0px; + background-color: var(--bg-vanilla-100); + + border-left: 0px; + border-top-left-radius: 0px; + border-bottom-left-radius: 0px; + } + + .input-field { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-100); + color: var(--text-ink-300); + } + + .arrow { + color: var(--bg-slate-300); + } + } + } +}