diff --git a/src/assets/icons/ic-onboarding.svg b/src/assets/icons/ic-onboarding.svg new file mode 100644 index 0000000000..91bad8534f --- /dev/null +++ b/src/assets/icons/ic-onboarding.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/img/guide-create-app.png b/src/assets/img/guide-create-app.png new file mode 100644 index 0000000000..e0f6a13fdf Binary files /dev/null and b/src/assets/img/guide-create-app.png differ diff --git a/src/assets/img/guide-onboard.png b/src/assets/img/guide-onboard.png new file mode 100644 index 0000000000..2dabd036e5 Binary files /dev/null and b/src/assets/img/guide-onboard.png differ diff --git a/src/assets/img/guided-chart-repository.png b/src/assets/img/guided-chart-repository.png new file mode 100644 index 0000000000..2ece6ae284 Binary files /dev/null and b/src/assets/img/guided-chart-repository.png differ diff --git a/src/assets/img/guided-helm-cluster.png b/src/assets/img/guided-helm-cluster.png new file mode 100644 index 0000000000..3fe6df7226 Binary files /dev/null and b/src/assets/img/guided-helm-cluster.png differ diff --git a/src/assets/img/guided-helm-collage.png b/src/assets/img/guided-helm-collage.png new file mode 100644 index 0000000000..59c173d333 Binary files /dev/null and b/src/assets/img/guided-helm-collage.png differ diff --git a/src/assets/img/guided-helm-search.png b/src/assets/img/guided-helm-search.png new file mode 100644 index 0000000000..0188a61a9c Binary files /dev/null and b/src/assets/img/guided-helm-search.png differ diff --git a/src/assets/img/helm-collage.png b/src/assets/img/helm-collage.png new file mode 100644 index 0000000000..f79f448e60 Binary files /dev/null and b/src/assets/img/helm-collage.png differ diff --git a/src/assets/img/ic-preview.png b/src/assets/img/ic-preview.png new file mode 100644 index 0000000000..b92a99f52a Binary files /dev/null and b/src/assets/img/ic-preview.png differ diff --git a/src/assets/img/lifebuoy.png b/src/assets/img/lifebuoy.png new file mode 100644 index 0000000000..1a71e76f62 Binary files /dev/null and b/src/assets/img/lifebuoy.png differ diff --git a/src/components/app/create/createApp.css b/src/components/app/create/createApp.css index de00fa9531..8ed0cce9b9 100644 --- a/src/components/app/create/createApp.css +++ b/src/components/app/create/createApp.css @@ -1,7 +1,9 @@ .create-app-modal { width: 450px; max-height: 90vh; - margin-top: 40px; + margin-top: 0px; + border-top-left-radius: 0px; + border-top-right-radius: 0px; } .footer { diff --git a/src/components/app/details/main.tsx b/src/components/app/details/main.tsx index 022b93f7c7..a743b854cb 100644 --- a/src/components/app/details/main.tsx +++ b/src/components/app/details/main.tsx @@ -19,6 +19,7 @@ import { ReactComponent as Settings } from '../../../assets/icons/ic-settings.sv import { ReactComponent as Info } from '../../../assets/icons/ic-info-outlined.svg' import { EnvType } from '../../v2/appDetails/appDetails.type' import PageHeader from '../../common/header/PageHeader' +import { AppDetailsProps } from './triggerView/types' const TriggerView = lazy(() => import('./triggerView/TriggerView')) const DeploymentMetrics = lazy(() => import('./metrics/DeploymentMetrics')) @@ -29,7 +30,7 @@ const IndexComponent = lazy(() => import('../../v2/index')) const CDDetails = lazy(() => import('./cdDetails/CDDetails')) const TestRunList = lazy(() => import('./testViewer/TestRunList')) -export default function AppDetailsPage({ isV2 }) { +export default function AppDetailsPage({ isV2 }: AppDetailsProps) { const { path } = useRouteMatch() const { appId } = useParams<{ appId }>() diff --git a/src/components/app/details/triggerView/types.ts b/src/components/app/details/triggerView/types.ts index 703c563b98..2add5cf1cd 100644 --- a/src/components/app/details/triggerView/types.ts +++ b/src/components/app/details/triggerView/types.ts @@ -469,3 +469,6 @@ export interface BranchRegexModalProps { regexValue onCloseBranchRegexModal } +export interface AppDetailsProps { + isV2: boolean +} diff --git a/src/components/app/list-new/AppList.tsx b/src/components/app/list-new/AppList.tsx index c3052f3df7..a5f25fe4d6 100644 --- a/src/components/app/list-new/AppList.tsx +++ b/src/components/app/list-new/AppList.tsx @@ -14,8 +14,6 @@ import { import { ReactComponent as Search } from '../../../assets/icons/ic-search.svg' import { ReactComponent as ChartIcon } from '../../../assets/icons/ic-charts.svg' import { ReactComponent as AddIcon } from '../../../assets/icons/ic-add.svg' -import InstallDevtronFullImage from '../../../assets/img/install-devtron-full@2x.png' -import EmptyState from '../../EmptyState/EmptyState' import { getInitData, buildClusterVsNamespace, getNamespaces } from './AppListService' import { ServerErrors } from '../../../modals/commonTypes' import { AppListViewType } from '../config' @@ -24,7 +22,7 @@ import { ReactComponent as Clear } from '../../../assets/icons/ic-error.svg' import DevtronAppListContainer from '../list/DevtronAppListContainer' import HelmAppList from './HelmAppList' import * as queryString from 'query-string' -import { OrderBy, SortBy } from '../list/types' +import { AppListPropType, OrderBy, SortBy } from '../list/types' import { AddNewApp } from '../create/CreateApp' import { mainContext } from '../../common/navigation/NavigationRoutes' import '../list/list.css' @@ -37,9 +35,8 @@ import { FILE_NAMES } from '../../common/ExportToCsv/constants' import { getAppList } from '../service' import moment from 'moment' import { getUserRole } from '../../userGroups/userGroup.service' -import Tippy from '@tippyjs/react' -export default function AppList() { +export default function AppList({isSuperAdmin, appListCount} : AppListPropType) { const location = useLocation() const history = useHistory() const params = useParams<{ appType: string }>() @@ -50,7 +47,6 @@ export default function AppList() { const [lastDataSync, setLastDataSync] = useState(false) const [fetchingNamespaces, setFetchingNamespaces] = useState(false) const [fetchingNamespacesErrored, setFetchingNamespacesErrored] = useState(false) - const [parsedPayloadOnUrlChange, setParsedPayloadOnUrlChange] = useState({}) const [currentTab, setCurrentTab] = useState(undefined) const [showCreateNewAppSelectionModal, setShowCreateNewAppSelectionModal] = useState(false) @@ -79,7 +75,7 @@ export default function AppList() { // on page load useEffect(() => { let _currentTab = - params.appType == AppListConstants.AppType.DEVTRON_APPS + params.appType === AppListConstants.AppType.DEVTRON_APPS ? AppListConstants.AppTabs.DEVTRON_APPS : AppListConstants.AppTabs.HELM_APPS setCurrentTab(_currentTab) @@ -104,7 +100,7 @@ export default function AppList() { setEnvironmentListRes(initData.environmentListRes) setMasterFilters(initData.filters) setDataStateType(AppListViewType.LIST) - if (serverMode == SERVER_MODE.EA_ONLY) { + if (serverMode === SERVER_MODE.EA_ONLY) { applyClusterSelectionFilterOnPageLoadIfSingle(initData.filters.clusters, _currentTab) } }) @@ -1014,65 +1010,70 @@ export default function AppList() { return (
- {dataStateType == AppListViewType.LOADING && ( + {dataStateType === AppListViewType.LOADING && (
)} - {dataStateType == AppListViewType.ERROR && ( + {dataStateType === AppListViewType.ERROR && (
)} - {dataStateType == AppListViewType.LIST && ( + {dataStateType === AppListViewType.LIST && ( <> {renderPageHeader()} {renderMasterFilters()} {renderAppliedFilters()} {renderAppTabs()} - {serverMode == SERVER_MODE.FULL && renderAppCreateRouter()} - {params.appType == AppListConstants.AppType.DEVTRON_APPS && serverMode == SERVER_MODE.FULL && ( - - )} - {params.appType == AppListConstants.AppType.DEVTRON_APPS && serverMode == SERVER_MODE.EA_ONLY && ( -
- -
- )} - {params.appType == AppListConstants.AppType.HELM_APPS && ( + {serverMode === SERVER_MODE.FULL && renderAppCreateRouter()} <> - - {fetchingExternalApps && ( -
- -
+ {params.appType === AppListConstants.AppType.DEVTRON_APPS && + serverMode === SERVER_MODE.FULL && ( + + )} + {params.appType === AppListConstants.AppType.DEVTRON_APPS && + serverMode === SERVER_MODE.EA_ONLY && ( +
+ +
+ )} + {params.appType === AppListConstants.AppType.HELM_APPS && ( + <> + + {fetchingExternalApps && ( +
+ +
+ )} + )} - )} )}
diff --git a/src/components/app/list-new/HelmAppList.tsx b/src/components/app/list-new/HelmAppList.tsx index 907e56dfcc..dd3f00328e 100644 --- a/src/components/app/list-new/HelmAppList.tsx +++ b/src/components/app/list-new/HelmAppList.tsx @@ -10,7 +10,6 @@ import { Pagination, LazyImage, handleUTCTime, - useEventSource, } from '../../common'; import { Host, SERVER_MODE, URLS, DOCUMENTATION } from '../../../config'; import { AppListViewType } from '../config'; diff --git a/src/components/app/list/AppListView.tsx b/src/components/app/list/AppListView.tsx index d6c018ab2f..9d41ab917a 100644 --- a/src/components/app/list/AppListView.tsx +++ b/src/components/app/list/AppListView.tsx @@ -10,8 +10,7 @@ import { ReactComponent as Edit } from '../../../assets/icons/ic-settings.svg'; import {ReactComponent as DevtronAppIcon} from '../../../assets/icons/ic-devtron-app.svg'; import {ReactComponent as HelpOutlineIcon} from '../../../assets/icons/ic-help-outline.svg'; import Tippy from '@tippyjs/react'; - - +import DevtronAppGuidePage from '../../onboardingGuide/DevtronAppGuidePage'; interface AppListViewProps extends AppListState, RouteComponentProps<{}> { expandRow: (app: App | null) => void; closeExpandedRow: () => void; @@ -21,6 +20,9 @@ interface AppListViewProps extends AppListState, RouteComponentProps<{}> { clearAll: () => void; changePage: (pageNo: number) => void; changePageSize: (size: number) => void; + appListCount: number + isSuperAdmin: boolean + openDevtronAppCreateModel: (event) => void } export class AppListView extends Component{ @@ -129,18 +131,13 @@ export class AppListView extends Component{ } + else if (this.props.view === AppListViewType.EMPTY) { return - + } + else if (this.props.view === AppListViewType.NO_RESULT) { return { clearAll={this.props.clearAllFilters} changePage={this.changePage} changePageSize={this.changePageSize} + isSuperAdmin={this.props.isSuperAdmin} + appListCount={this.props.appListCount} + openDevtronAppCreateModel={this.props.openDevtronAppCreateModel} /> } } diff --git a/src/components/app/list/list.css b/src/components/app/list/list.css index 1fc510e74f..03df3e2e8e 100644 --- a/src/components/app/list/list.css +++ b/src/components/app/list/list.css @@ -482,3 +482,7 @@ background-color: var(--R100); border: solid 1px var(--R200); } + +.first-app-container { + margin-top: 60px; +} \ No newline at end of file diff --git a/src/components/app/list/types.tsx b/src/components/app/list/types.tsx index 496462d15c..4793ba8290 100644 --- a/src/components/app/list/types.tsx +++ b/src/components/app/list/types.tsx @@ -59,6 +59,9 @@ export interface AppListProps extends RouteComponentProps<{ route: string }> { clearAllFilters: () => void; sortApplicationList : (key: string) => void; updateLastDataSync : () => void; + appListCount: number + isSuperAdmin: boolean + openDevtronAppCreateModel: (event) => void setAppCount: React.Dispatch> } @@ -114,3 +117,7 @@ export const SortBy = { ENVIRONMENT: "environmentSort", } +export interface AppListPropType { + isSuperAdmin: boolean + appListCount: number +} \ No newline at end of file diff --git a/src/components/common/HelpNav.tsx b/src/components/common/HelpNav.tsx index de456f4652..8c9991c44f 100644 --- a/src/components/common/HelpNav.tsx +++ b/src/components/common/HelpNav.tsx @@ -6,7 +6,8 @@ import { ReactComponent as Discord } from '../../assets/icons/ic-discord-fill.sv import { ReactComponent as Edit } from '../../assets/icons/ic-pencil.svg' import { ReactComponent as Chat } from '../../assets/icons/ic-chat-circle-dots.svg' import { InstallationType, ServerInfo } from '../v2/devtronStackManager/DevtronStackManager.type' -import { NavLink } from 'react-router-dom' +import { NavLink, useHistory } from 'react-router-dom' +import { ReactComponent as GettingStartedIcon } from '../../assets/icons/ic-onboarding.svg' export interface HelpNavType { className: string @@ -14,9 +15,12 @@ export interface HelpNavType { setShowHelpCard: React.Dispatch> serverInfo: ServerInfo fetchingServerInfo: boolean + setGettingStartedClicked: (isClicked: boolean) => void } -function HelpNav({ className, showHelpCard, setShowHelpCard, serverInfo, fetchingServerInfo }: HelpNavType) { +function HelpNav({ className, showHelpCard, setShowHelpCard, serverInfo, fetchingServerInfo, setGettingStartedClicked }: HelpNavType) { + const history = useHistory() + const HelpOptions = [ { name: 'View documentation', @@ -28,6 +32,7 @@ function HelpNav({ className, showHelpCard, setShowHelpCard, serverInfo, fetchin name: 'Chat with support', link: 'https://discord.devtron.ai/', icon: Chat, + showSeparator: true, }, { name: 'Join discord community', @@ -42,30 +47,42 @@ function HelpNav({ className, showHelpCard, setShowHelpCard, serverInfo, fetchin }, ] + const onClickGettingStarted = () => { + setGettingStartedClicked(true) + } + return (
setShowHelpCard(!showHelpCard)}>
+ + +
Getting started
+
+ {HelpOptions.map((option) => { return ( - + { + ReactGA.event({ + category: 'Main Navigation', + action: `${option.name} Clicked`, + }) + }} + > + +
{option.name}
+
{option.showSeparator &&
} ) diff --git a/src/components/common/gettingStartedCard/GettingStarted.tsx b/src/components/common/gettingStartedCard/GettingStarted.tsx new file mode 100644 index 0000000000..33097600f7 --- /dev/null +++ b/src/components/common/gettingStartedCard/GettingStarted.tsx @@ -0,0 +1,54 @@ +import React from 'react' +import GettingToast from '../../../assets/img/lifebuoy.png' +import { updateLoginCount } from '../../../services/service' +import { handlePostHogEventUpdate, LOGIN_COUNT, MAX_LOGIN_COUNT, POSTHOG_EVENT_ONBOARDING } from '../../onboardingGuide/onboarding.utils' +import { GettingStartedType } from '../../onboardingGuide/OnboardingGuide.type' +import { setActionWithExpiry } from '../helpers/Helpers' +import './gettingStarted.scss' + +function GettingStartedCard({ className, hideGettingStartedCard }: GettingStartedType) { + const onClickedOkay = (e) => { + setActionWithExpiry('clickedOkay', 1) + hideGettingStartedCard() + handlePostHogEventUpdate(e) + } + + const onClickedDontShowAgain = (e) => { + const updatedPayload = { + key: LOGIN_COUNT, + value: `${MAX_LOGIN_COUNT}`, + } + updateLoginCount(updatedPayload) + hideGettingStartedCard(updatedPayload.value) + handlePostHogEventUpdate(e) + } + + return ( +
+
+
+ getting started icon +
Getting started
+
You can always access the Getting Started guide from here.
+
+ + +
+
+
+ ) +} + +export default GettingStartedCard diff --git a/src/components/common/gettingStartedCard/gettingStarted.scss b/src/components/common/gettingStartedCard/gettingStarted.scss new file mode 100644 index 0000000000..6ab064dcab --- /dev/null +++ b/src/components/common/gettingStartedCard/gettingStarted.scss @@ -0,0 +1,23 @@ +.getting-started-card { + background-color: #2d3452; + position: fixed; + z-index: 6; + right: 20px; + top: 54px; + +} + +.arrow-up { + width: 0; + height: 0; + border-left: 12px solid transparent; + border-right: 12px solid transparent; + border-bottom: 12px solid #2d3452; + right: 64px; + position: fixed; + top: 42px +} + +.getting_tippy__position { + position: fixed; +} \ No newline at end of file diff --git a/src/components/common/header/PageHeader.tsx b/src/components/common/header/PageHeader.tsx index e58489adf8..770b0aa2e9 100644 --- a/src/components/common/header/PageHeader.tsx +++ b/src/components/common/header/PageHeader.tsx @@ -5,10 +5,14 @@ import { ReactComponent as Close } from '../../../assets/icons/ic-close.svg' import HelpNav from '../HelpNav' import './pageHeader.css' import LogoutCard from '../LogoutCard' -import { getLoginInfo, getRandomColor } from '../helpers/Helpers' +import { getLoginInfo, getRandomColor, setActionWithExpiry } from '../helpers/Helpers' import { ServerInfo } from '../../v2/devtronStackManager/DevtronStackManager.type' import { getServerInfo } from '../../v2/devtronStackManager/DevtronStackManager.service' +import { useRouteMatch, useHistory, useLocation } from 'react-router' +import GettingStartedCard from '../gettingStartedCard/GettingStarted' +import { mainContext } from '../navigation/NavigationRoutes' import ReactGA from 'react-ga4' +import { handlePostHogEventUpdate, MAX_LOGIN_COUNT, POSTHOG_EVENT_ONBOARDING } from '../../onboardingGuide/onboarding.utils' export interface PageHeaderType { headerName?: string additionalHeaderInfo?: () => JSX.Element @@ -42,6 +46,14 @@ function PageHeader({ showCloseButton = false, onClose, }: PageHeaderType) { + const { + loginCount, + setLoginCount, + showGettingStartedCard, + setShowGettingStartedCard, + isGettingStartedClicked, + setGettingStartedClicked, + } = useContext(mainContext) const [showHelpCard, setShowHelpCard] = useState(false) const [showLogOutCard, setShowLogOutCard] = useState(false) const loginInfo = getLoginInfo() @@ -52,6 +64,7 @@ function PageHeader({ fetchingServerInfo: false, }, ) + const [expiryDate, setExpiryDate] = useState(0) const getCurrentServerInfo = async () => { try { @@ -68,23 +81,44 @@ function PageHeader({ console.error('Error in fetching server info') } } + + useEffect(() => { + setExpiryDate(+localStorage.getItem('clickedOkay')) + }, []) + useEffect(() => { getCurrentServerInfo() }, []) + const onClickLogoutButton = () => { + setShowLogOutCard(!showLogOutCard) + if (showHelpCard) { + setShowHelpCard(false) + } + setActionWithExpiry('clickedOkay', 1) + hideGettingStartedCard() + } + + const onClickHelp = (e) => { + setShowHelpCard(!showHelpCard) + if (showLogOutCard) { + setShowLogOutCard(false) + } + setActionWithExpiry('clickedOkay', 1) + hideGettingStartedCard() + handlePostHogEventUpdate(e, POSTHOG_EVENT_ONBOARDING.HELP) + ReactGA.event({ + category: 'Main Navigation', + action: `Help Clicked`, + }) + } + const renderLogoutHelpSection = () => { return ( <>
{ - setShowHelpCard(!showHelpCard) - showLogOutCard && setShowLogOutCard(false) - ReactGA.event({ - category: 'Main Navigation', - action: `Help Clicked`, - }) - }} + onClick={onClickHelp} > @@ -93,10 +127,7 @@ function PageHeader({
{ - setShowLogOutCard(!showLogOutCard) - showHelpCard && setShowHelpCard(false) - }} + onClick={onClickLogoutButton} style={{ backgroundColor: getRandomColor(email) }} > {email[0]} @@ -105,6 +136,19 @@ function PageHeader({ ) } + const hideGettingStartedCard = (count?: string) => { + setShowGettingStartedCard(false) + if (count) { + setLoginCount(+count) + } + } + + const getExpired = (): boolean => { + // Render Getting started tippy card if the time gets expired + const now = new Date().valueOf() + return now > expiryDate + } + return (
+ )} + {showGettingStartedCard && loginCount >= 0 && loginCount < MAX_LOGIN_COUNT && getExpired() && ( + )} {showLogOutCard && ( diff --git a/src/components/common/helpers/Helpers.tsx b/src/components/common/helpers/Helpers.tsx index f4c7c0f79a..a27acd7d9a 100644 --- a/src/components/common/helpers/Helpers.tsx +++ b/src/components/common/helpers/Helpers.tsx @@ -6,6 +6,7 @@ import YAML from 'yaml'; import { useWindowSize } from './UseWindowSize'; import { useLocation } from 'react-router' import { Link } from 'react-router-dom'; +import { getDateInMilliseconds } from '../../apiTokens/authorization.utils'; const commandLineParser = require('command-line-parser'); export type IntersectionChangeHandler = (entry: IntersectionObserverEntry) => void; @@ -906,7 +907,7 @@ export const sortOptionsByValue = (optionA, optionB) => { return 0 } -// Create instance of MutationObserver & watch for DOM changes until +// Create instance of MutationObserver & watch for DOM changes until // disconnect() is called. export const watchDOMForChanges = (callback: (observer: MutationObserver) => void) => { const observer = new MutationObserver(() => { @@ -937,3 +938,8 @@ export const elementDidMount = (identifier: string): Promise => { }) }) } + +// Setting expiry time in local storage for specified action key +export const setActionWithExpiry = (key: string, days: number): void => { + localStorage.setItem(key, `${getDateInMilliseconds(days)}`) +} diff --git a/src/components/common/navigation/NavigationRoutes.tsx b/src/components/common/navigation/NavigationRoutes.tsx index 4157b9a769..52e30c8d72 100644 --- a/src/components/common/navigation/NavigationRoutes.tsx +++ b/src/components/common/navigation/NavigationRoutes.tsx @@ -7,13 +7,24 @@ import { useRouteMatch, useHistory, useLocation } from 'react-router' import * as Sentry from '@sentry/browser' import ReactGA from 'react-ga4' import { Security } from '../../security/Security' -import { dashboardLoggedIn, getVersionConfig } from '../../../services/service' +import { + dashboardLoggedIn, + getAppListMin, + getLoginData, + getVersionConfig, + updateLoginCount, +} from '../../../services/service' import Reload from '../../Reload/Reload' import { EnvType } from '../../v2/appDetails/appDetails.type' import DevtronStackManager from '../../v2/devtronStackManager/DevtronStackManager' import { ServerInfo } from '../../v2/devtronStackManager/DevtronStackManager.type' import { getServerInfo } from '../../v2/devtronStackManager/DevtronStackManager.service' import ClusterNodeContainer from '../../ClusterNodes/ClusterNodeContainer' +import DeployManageGuide from '../../onboardingGuide/DeployManageGuide' +import { showError } from '../helpers/Helpers' +import { AppRouterType } from '../../../services/service.types' +import { getUserRole } from '../../userGroups/userGroup.service' +import { LOGIN_COUNT, MAX_LOGIN_COUNT } from '../../onboardingGuide/onboarding.utils' const Charts = lazy(() => import('../../charts/Charts')) const ExternalApps = lazy(() => import('../../external-apps/ExternalApps')) @@ -23,6 +34,8 @@ const V2Details = lazy(() => import('../../v2/index')) const GlobalConfig = lazy(() => import('../../globalConfigurations/GlobalConfiguration')) const BulkActions = lazy(() => import('../../deploymentGroups/BulkActions')) const BulkEdit = lazy(() => import('../../bulkEdits/BulkEdits')) +const OnboardingGuide = lazy(() => import('../../onboardingGuide/OnboardingGuide')) + export const mainContext = createContext(null) export default function NavigationRoutes() { @@ -38,10 +51,71 @@ export default function NavigationRoutes() { fetchingServerInfo: false, }, ) + const [isHelpGettingStartedClicked, setHelpGettingStartedClicked] = useState(false) + const [loginCount, setLoginCount] = useState(0) + const [expiryDate, setExpiryDate] = useState(0) + const [isSuperAdmin, setSuperAdmin] = useState(false) + const [appListCount, setAppListCount] = useState(0) + const [loginLoader, setLoginLoader] = useState(true) + const [isDeployManageCardClicked, setDeployManageCardClicked] = useState(false) + const [showGettingStartedCard, setShowGettingStartedCard] = useState(true) + const [isGettingStartedClicked, setGettingStartedClicked] = useState(false) + + const showCloseButtonAfterGettingStartedClicked = () => { + setHelpGettingStartedClicked(true) + } + + useEffect(() => { + getInit() + setExpiryDate(+localStorage.getItem('clickedOkay')) + }, []) + + const getInit = () => { + setLoginLoader(true) + Promise.all([getUserRole(), serverMode === SERVER_MODE.FULL && getAppListMin(), getLoginData()]).then( + (response) => { + const superAdmin = response[0]?.result?.roles.includes('role:super-admin___') + setSuperAdmin(superAdmin) + setAppListCount(response[1]?.result?.length) + processLoginData(response[2], superAdmin) + setLoginLoader(false) + }, + (err) => { + setLoginLoader(false) + showError(err) + }, + ) + } + + const processLoginData = (response, superAdmin) => { + const count = response.result?.value ? parseInt(response.result.value) : 0 + setLoginCount(count) + if ( + typeof Storage !== 'undefined' && + (localStorage.getItem('isSSOLogin') || localStorage.getItem('isAdminLogin')) + ) { + localStorage.removeItem('isSSOLogin') + localStorage.removeItem('isAdminLogin') + if (count < MAX_LOGIN_COUNT) { + const updatedPayload = { + key: LOGIN_COUNT, + value: `${count + 1}`, + } + updateLoginCount(updatedPayload) + } + } + if (!count && superAdmin) { + history.push(`/${URLS.GETTING_STARTED}`) + } else if (!count) { + history.push(URLS.APP) + } + } useEffect(() => { const loginInfo = getLoginInfo() + if (!loginInfo) return + if (process.env.NODE_ENV === 'production' && window._env_) { if (window._env_.SENTRY_ERROR_ENABLED) { Sentry.configureScope(function (scope) { @@ -73,6 +147,7 @@ export default function NavigationRoutes() { }) } } + if (typeof Storage !== 'undefined') { if (localStorage.isDashboardLoggedIn) return dashboardLoggedIn() @@ -130,29 +205,59 @@ export default function NavigationRoutes() { } } - if (pageState === ViewType.LOADING) { + const onClickedDeployManageCardClicked = () =>{ + setDeployManageCardClicked(true) + } + + if (pageState === ViewType.LOADING || loginLoader) { return } else if (pageState === ViewType.ERROR) { return } else { return ( - -
- + +
+ {!window.location.href.includes(URLS.GETTING_STARTED) && ( + + )} + {serverMode && (
}> - } /> + ( + + )} + /> } /> + + + + + + + - + @@ -193,14 +311,23 @@ export default function NavigationRoutes() { } } -export function AppRouter() { +export function AppRouter({ isSuperAdmin, appListCount, loginCount }: AppRouterType) { const { path } = useRouteMatch() const [environmentId, setEnvironmentId] = useState(null) return ( - } /> + ( + + )} + /> } /> } /> } /> + - + @@ -220,19 +350,19 @@ export function AppRouter() { ) } -export function AppListRouter() { +export function AppListRouter({ isSuperAdmin, appListCount, loginCount }: AppRouterType) { const { path } = useRouteMatch() const [environmentId, setEnvironmentId] = useState(null) return ( - } /> + } /> - + @@ -240,19 +370,22 @@ export function AppListRouter() { ) } -export function RedirectWithSentry() { +export function RedirectUserWithSentry({ isFirstLoginUser }) { const { push } = useHistory() const { pathname } = useLocation() useEffect(() => { - if (pathname && pathname !== '/') Sentry.captureMessage(`redirecting to app-list from ${pathname}`, 'warning') - push(`${URLS.APP}/${URLS.APP_LIST}`) + if (pathname && pathname !== '/') Sentry.captureMessage(`redirecting to app-list from ${pathname}`, 'warning') + if (isFirstLoginUser) { + push(`${URLS.GETTING_STARTED}`) + } else { + push(`${URLS.APP}/${URLS.APP_LIST}`) + } }, []) return null } export function RedirectToAppList() { const { push } = useHistory() - const { pathname } = useLocation() const { serverMode } = useContext(mainContext) useEffect(() => { let baseUrl = `${URLS.APP}/${URLS.APP_LIST}` diff --git a/src/components/common/navigation/navigation.scss b/src/components/common/navigation/navigation.scss index 6eac626d95..75ca17ae90 100644 --- a/src/components/common/navigation/navigation.scss +++ b/src/components/common/navigation/navigation.scss @@ -4,6 +4,9 @@ display: grid; grid-template-columns: 64px 1fr; overflow-y: hidden; + &.no-nav{ + grid-template-columns: 1fr; + } } .main { @@ -255,9 +258,9 @@ .help-card { display: grid; - grid-template-rows: 36px 1px 36px 36px 1px 36px auto; + grid-template-rows: 36px 36px 1px 36px 1px 36px 1px 36px auto; align-items: center; - + z-index: var(--modal-index); .help-card__option { padding: 6px 12px; height: 36px; @@ -322,4 +325,4 @@ .expandable-active-nav { padding: 0px 0 0 12px; -} +} \ No newline at end of file diff --git a/src/components/login/Login.tsx b/src/components/login/Login.tsx index bb16159537..86e760d5a3 100644 --- a/src/components/login/Login.tsx +++ b/src/components/login/Login.tsx @@ -10,7 +10,7 @@ import { Progressing, showError } from '../common' import { LoginProps, LoginFormState } from './login.types' import { getSSOConfigList, loginAsAdmin } from './login.service' import './login.css' -import { dashboardAccessed } from '../../services/service' +import { dashboardAccessed, getLoginData, updateLoginCount } from '../../services/service' export default class Login extends Component { constructor(props) { @@ -23,6 +23,7 @@ export default class Login extends Component { username: 'admin', password: '', }, + loginCount: 0, } this.handleChange = this.handleChange.bind(this) this.autoFillLogin = this.autoFillLogin.bind(this) @@ -62,6 +63,7 @@ export default class Login extends Component { }) .catch((errors) => {}) } + } handleChange(e: React.ChangeEvent): void { @@ -91,6 +93,12 @@ export default class Login extends Component { return !isValid } + onClickSSO = (): void => { + if (typeof Storage !== 'undefined') { + localStorage.setItem('isSSOLogin', 'true') + } + } + login(e): void { e.preventDefault() let data = this.state.form @@ -101,7 +109,14 @@ export default class Login extends Component { this.setState({ loading: false }) let queryString = this.props.location.search.split('continue=')[1] let url = queryString ? `${queryString}` : URLS.APP - this.props.history.push(`${url}`) + + getLoginData().then((response) => { + const count = response.result?.value ? parseInt(response.result.value) : 0 + this.setState({ loginCount: count ?? 1 }) + localStorage.setItem('isAdminLogin', 'true') + + this.props.history.push(url) + }) } }) .catch((errors: ServerErrors) => { @@ -124,6 +139,7 @@ export default class Login extends Component { @@ -147,7 +163,7 @@ export default class Login extends Component { login

Your tool for Rapid, Reliable & Repeatable deployments

{/* @ts-ignore */} -
+ { } diff --git a/src/components/onboardingGuide/DeployManageGuide.tsx b/src/components/onboardingGuide/DeployManageGuide.tsx new file mode 100644 index 0000000000..c9ba0bd1db --- /dev/null +++ b/src/components/onboardingGuide/DeployManageGuide.tsx @@ -0,0 +1,181 @@ +import React, { useContext, useEffect, useState } from 'react' +import HelmSearch from '../../assets/img/guided-helm-search.png' +import HelmInCluster from '../../assets/img/guided-helm-cluster.png' +import ChartRepository from '../../assets/img/guided-chart-repository.png' +import HelmCollage from '../../assets/img/guided-helm-collage.png' +import { NavLink, useHistory } from 'react-router-dom' +import { SERVER_MODE, URLS } from '../../config' +import './onboardingGuide.scss' +import { Progressing, showError } from '../common' +import { getDevtronInstalledHelmApps } from '../app/list-new/AppListService' +import { mainContext } from '../common/navigation/NavigationRoutes' +import { NAVIGATION, handlePostHogEventUpdate, POSTHOG_EVENT_ONBOARDING } from './onboarding.utils' +import GuideCommonHeader from './GuideCommonHeader' +import { getClusterListMinWithoutAuth } from '../../services/service' +import { DeployManageGuideType } from './OnboardingGuide.type' + +function DeployManageGuide({ isGettingStartedClicked, loginCount }: DeployManageGuideType) { + const history = useHistory() + const [devtronHelmCount, setDevtronHelmCount] = useState(0) + const [loader, setLoader] = useState(false) + const { serverMode } = useContext(mainContext) + const [isDefaultCluster, setDefaultCluster] = useState(false) + + const _getInit = async () => { + if (serverMode === SERVER_MODE.FULL) { + setLoader(true) + + try { + const [installedHelmAppsRes, clusterListMinRes] = await Promise.all([ + getDevtronInstalledHelmApps(''), + getClusterListMinWithoutAuth(), + ]) + setLoader(false) + setDevtronHelmCount(installedHelmAppsRes?.result?.helmApps.length) + setDefaultCluster(clusterListMinRes?.result?.some((data) => data.cluster_name === 'default_cluster')) + } catch (err) { + showError(err) + } + } + } + + useEffect(() => { + _getInit() + }, []) + + const redirectToAppList = () => { + history.push(`${URLS.APP}/${URLS.APP_LIST}`) + } + + const onClickChartCard = (e) => { + handlePostHogEventUpdate(e, POSTHOG_EVENT_ONBOARDING.CONNECT_CHART_REPOSITORY) + } + + const onClickViewApplication = (e) => { + handlePostHogEventUpdate(e, POSTHOG_EVENT_ONBOARDING.VIEW_APPLICATION) + } + + const onClickHelmChart = (e) => { + handlePostHogEventUpdate(e, POSTHOG_EVENT_ONBOARDING.BROWSE_HELM_CHART) + } + + const onClickCluster = (e) => { + handlePostHogEventUpdate(e, POSTHOG_EVENT_ONBOARDING.CONNECT_CLUSTER) + } + + return loader ? ( +
+ +
+ ) : ( +
+ +
+
+
+ {devtronHelmCount > 0 && ( +
+ + Please connect cluster +
+ Check deployed helm apps
+
View applications
+
+
+
+ )} + +
+ + Please connect cluster +
+ I want to deploy popular helm charts
+
Browse helm charts
+
+
+
+
+ + Please connect cluster +
+ I have helm applications in other clusters
+
Connect a cluster
+
+
+
+
+ + Please connect cluster +
+ I want to connect my own chart repository
+
Connect chart repository
+
+
+
+
+
+ + Skip and explore Devtron on your own + +
Tip: You can return here anytime from the Help menu
+
+
+
+
+ ) +} + +export default DeployManageGuide diff --git a/src/components/onboardingGuide/DevtronAppGuidePage.tsx b/src/components/onboardingGuide/DevtronAppGuidePage.tsx new file mode 100644 index 0000000000..d287d2e36b --- /dev/null +++ b/src/components/onboardingGuide/DevtronAppGuidePage.tsx @@ -0,0 +1,63 @@ +import React from 'react' +import CreateApp from '../../assets/img/guide-create-app.png' +import PreviewImage from '../../assets/img/ic-preview.png' +import { DEVTRON_NODE_DEPLOY_VIDEO, PREVIEW_DEVTRON } from '../../config' +import { GuidedPageType } from './OnboardingGuide.type' +import './onboardingGuide.scss' + +function DevtronAppGuidePage({ openDevtronAppCreateModel }: GuidedPageType) { + const renderSubTitle = () => { + return ( + <> + Tip: +
+   Watch how to create a sample nodejs application + + + ) + } + + return ( +
+
+

Create your first application

+

{renderSubTitle()}

+
+ +
+
+
+
+ Please connect cluster +
+ Create an application from scratch +
+
+
+ + +
+
+
+ ) +} + +export default DevtronAppGuidePage diff --git a/src/components/onboardingGuide/GuideCommonHeader.tsx b/src/components/onboardingGuide/GuideCommonHeader.tsx new file mode 100644 index 0000000000..234ce7e6ee --- /dev/null +++ b/src/components/onboardingGuide/GuideCommonHeader.tsx @@ -0,0 +1,43 @@ +import React from 'react' +import { useHistory, useRouteMatch } from 'react-router-dom' +import { ReactComponent as Close } from '../../assets/icons/ic-close.svg' +import { ReactComponent as GoBack } from '../../assets/icons/ic-arrow-backward.svg' +import { GuideCommonHeaderType } from './OnboardingGuide.type' +import { URLS } from '../../config' + +function GuideCommonHeader({ loginCount, title, subtitle, onClickCloseButton, isGettingStartedClicked }: GuideCommonHeaderType) { + const history = useHistory() + const match = useRouteMatch() + const isGuidePage = match.url.includes(`${URLS.GETTING_STARTED}/${URLS.GUIDE}`) + const showCloseIcon = loginCount > 0 || isGettingStartedClicked + const onClickedGoBack = () => { + history.goBack() + } + + return ( +
+
+ {showCloseIcon && ( + + )} +
+
+ {isGuidePage && ( +
+ +
+ )} +
+

{title}

+

{subtitle}

+
+
+
+
+
+ ) +} + +export default GuideCommonHeader diff --git a/src/components/onboardingGuide/OnboardingGuide.tsx b/src/components/onboardingGuide/OnboardingGuide.tsx new file mode 100644 index 0000000000..93c85763b4 --- /dev/null +++ b/src/components/onboardingGuide/OnboardingGuide.tsx @@ -0,0 +1,123 @@ +import React from 'react' +import HelmCollage from '../../assets/img/helm-collage.png' +import DeployCICD from '../../assets/img/guide-onboard.png' +import { NavLink, useRouteMatch, useHistory } from 'react-router-dom' +import { PREVIEW_DEVTRON, SERVER_MODE, URLS } from '../../config' +import './onboardingGuide.scss' +import PreviewImage from '../../assets/img/ic-preview.png' +import { handlePostHogEventUpdate, POSTHOG_EVENT_ONBOARDING } from './onboarding.utils' +import GuideCommonHeader from './GuideCommonHeader' +import { OnboardingGuideProps } from './OnboardingGuide.type' + +function OnboardingGuide({ + loginCount, + serverMode, + onClickedDeployManageCardClicked, + isGettingStartedClicked, +}: OnboardingGuideProps) { + const match = useRouteMatch() + const history = useHistory() + + const onClickCloseButton = () => { + history.goBack() + } + + const redirectDeployCardToCICD = (): string => { + return serverMode === SERVER_MODE.FULL + ?`${URLS.APP}/${URLS.APP_LIST}` + : `${URLS.STACK_MANAGER_DISCOVER_MODULES_DETAILS}?id=cicd` + } + + const onClickedCICD = (e) => { + if (serverMode === SERVER_MODE.FULL) { + handlePostHogEventUpdate(e, POSTHOG_EVENT_ONBOARDING.DEPLOY_CUSTOM_APP_CI_CD) + } else { + handlePostHogEventUpdate(e, POSTHOG_EVENT_ONBOARDING.INSTALL_CUSTOM_CI_CD) + } + } + + const onClickPreviewCard = (e) => { + handlePostHogEventUpdate(e, POSTHOG_EVENT_ONBOARDING.PREVIEW) + } + + return ( +
+ +
+
+
+ +
+
+ + Deploy and manage helm +
+ Deploy and manage helm applications +
+
+
+ +
+ + Please connect cluster +
+ Deploy custom applications using CI/CD pipelines +
+
+
+
+
+ + Skip and explore Devtron on your own + +
Tip: You can return here anytime from the Help menu
+
+
+
+
+ ) +} + +export default OnboardingGuide diff --git a/src/components/onboardingGuide/OnboardingGuide.type.ts b/src/components/onboardingGuide/OnboardingGuide.type.ts new file mode 100644 index 0000000000..5a6227144b --- /dev/null +++ b/src/components/onboardingGuide/OnboardingGuide.type.ts @@ -0,0 +1,35 @@ +export interface GuideCommonHeaderType{ + loginCount: number + title: string + subtitle: string + onClickCloseButton: () => void + isGettingStartedClicked: boolean +} + +export interface DeployManageGuide{ + isGettingStartedClicked: boolean + loginCount: number +} + +export interface OnboardingGuideProps { + loginCount: number + isSuperAdmin: boolean + serverMode: string + onClickedDeployManageCardClicked: () => void + isGettingStartedClicked: boolean +} + +export interface DeployManageGuideType { + isGettingStartedClicked: boolean + loginCount: number +} +export interface GettingStartedType { + className: string + showHelpCard: boolean + hideGettingStartedCard: (count?: string) => void + loginCount: number +} + +export interface GuidedPageType { + openDevtronAppCreateModel: (event) => void +} diff --git a/src/components/onboardingGuide/onboarding.utils.ts b/src/components/onboardingGuide/onboarding.utils.ts new file mode 100644 index 0000000000..acbbea2d10 --- /dev/null +++ b/src/components/onboardingGuide/onboarding.utils.ts @@ -0,0 +1,34 @@ +import { URLS } from '../../config' +import { updatePostHogEvent } from '../../services/service' + +export const POSTHOG_EVENT_ONBOARDING = { + PREVIEW: 'Preview', + DEPLOY_CUSTOM_APP_CI_CD: 'Deploy custom app using CI/CD pipelines', + INSTALL_CUSTOM_CI_CD: 'Install CI/CD', + VIEW_APPLICATION: 'View helm application', + BROWSE_HELM_CHART: 'Browse helm chart', + CONNECT_CLUSTER: 'Connect cluster', + CONNECT_CHART_REPOSITORY: 'Connect chart repository', + TOOLTIP_OKAY: 'Tooltip okay', + TOOLTIP_DONT_SHOW_AGAIN: 'Tooltip Dont show again', + HELP: 'Clicked Help' +} + +export const LOGIN_COUNT = 'login-count' + +export const MAX_LOGIN_COUNT = 5 + +export const handlePostHogEventUpdate = (e, eventName?: string): void => { + const payload = { + eventType: eventName || e.target?.dataset.posthog, + key: LOGIN_COUNT, + value: '', + active: true, + } + updatePostHogEvent(payload) +} + +export const NAVIGATION = { + AUTOCOMPLETE: `${URLS.APP}/${URLS.APP_LIST}/${URLS.APP_LIST_HELM}?hOffset=0&namespace=1&offset=0`, + HELM_APPS: `${URLS.APP}/${URLS.APP_LIST}/${URLS.APP_LIST_HELM}` +} \ No newline at end of file diff --git a/src/components/onboardingGuide/onboardingGuide.scss b/src/components/onboardingGuide/onboardingGuide.scss new file mode 100644 index 0000000000..1fd2d37438 --- /dev/null +++ b/src/components/onboardingGuide/onboardingGuide.scss @@ -0,0 +1,111 @@ +.onboarding-container { + .onboarding-cards__wrap { + display: grid; + grid-template-columns: 300px 1px 300px 300px; + grid-gap: 20px; + } + + .onboarding-header { + background: var(--window-bg); + } + + .onboarding-card__img { + width: 298px; + height: 210px; + } + + .onboarding__bottom { + height: calc(100vh - 300px); + .onboarding-card a:hover { + color: var(--N900); + text-decoration: none; + } + .onboarding-card:hover { + box-shadow: 0 8px 12px 0 rgba(30, 35, 96, 0.1), 0 1px 4px 0 rgba(0, 0, 0, 0.1); + border-radius: 400; + } + } + + .onboarding__line { + width: 1px; + height: 150px; + background-color: var(--N200); + align-self: center; + } + + .onboarding__abs { + position: absolute; + top: -80px; + } +} + +.deploy-manage-container { + .deploy_arrow { + width: 56px; + height: 56px; + border: 1px solid var(--N200); + border-radius: 50%; + position: absolute; + left: -40px; + } + + .deploy-manage-cards__wrap { + display: flex; + grid-gap: 20px; + flex-wrap: wrap; + width: 820px; + justify-content: center; + a:hover { + color: var(--N900); + } + } + + .deploy-manage__body { + height: calc(100vh - 300px); + .deploy-card:hover { + box-shadow: 0 8px 12px 0 var(--gray); + border-radius: 400; + } + } + + .deploy-manage__abs { + position: absolute; + top: -80px; + } + + .deploy-manage__upper { + height: 236px; + } + + .deploy__title { + position: absolute; + width: 820px; + } +} + +.guide_skip a:hover { + color: var(--B500); +} + +.guide-body { + height: calc(100vh - 454px); +} + +.guide-container { + .guide-cards__wrap { + display: grid; + grid-template-columns: 300px 300px; + grid-gap: 20px; + position: absolute; + top: -80px; + } + + .guide-card__img { + width: 298px; + height: 210px; + } + + .guide-card:hover{ + box-shadow: 0 8px 12px 0 var(--gray); + } +} \ No newline at end of file diff --git a/src/components/userGroups/UserGroup.tsx b/src/components/userGroups/UserGroup.tsx index 02b024820c..9f37beec46 100644 --- a/src/components/userGroups/UserGroup.tsx +++ b/src/components/userGroups/UserGroup.tsx @@ -27,10 +27,10 @@ import { getGroupList, getUserId, getGroupId, - getUserRole, getEnvironmentListHelmApps, getUsersDataToExport, - getGroupsDataToExport + getGroupsDataToExport, + getUserRole } from './userGroup.service' import { get } from '../../services/api' import { getEnvironmentListMin, getProjectFilteredApps } from '../../services/service' diff --git a/src/components/userGroups/userGroup.service.ts b/src/components/userGroups/userGroup.service.ts index c6717c5f16..4729264000 100644 --- a/src/components/userGroups/userGroup.service.ts +++ b/src/components/userGroups/userGroup.service.ts @@ -144,13 +144,11 @@ interface UserRole extends ResponseType { } export function getUserRole(): Promise { - return get(`user/check/roles`); + return get(Routes.USER_CHECK_ROLE); } - export interface UsersDataToExportResponse extends ResponseType { result?: CreateUser[] } - export interface GroupsDataToExportResponse extends ResponseType { result?: CreateGroup[] } diff --git a/src/config/constants.ts b/src/config/constants.ts index f1d0b570f1..dd3326b5f0 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -4,9 +4,12 @@ export const Host = process.env.REACT_APP_ORCHESTRATOR_ROOT export const DEFAULTK8SVERSION = 'v1.16.0' export const Routes = { + GET: 'get', + UPDATE: 'update', API_VERSION_V2: 'v2', LOGIN: 'api/v1/session', SOURCE_CONFIG_GET: 'app/get', + USER_CHECK_ROLE:'user/check/roles', CHART_REFERENCES_MIN: 'chartref/autocomplete', CI_CONFIG_GET: 'app/ci-pipeline', @@ -113,6 +116,7 @@ export const Routes = { APP_CREATE_SECRET: 'config/global/cs', WORKFLOW: 'app/app-wf', + ATTRIBUTES_USER: 'attributes/user', APP_WORKFLOW_STATUS: 'app/workflow/status', APP_CREATE_ENV_SECRET: 'config/environment/cs', APP_CREATE_ENV_CONFIG_MAP: 'config/environment/cm', @@ -140,8 +144,8 @@ export const Routes = { PLUGIN_LIST: 'plugin/global/list', PLUGIN_DETAIL: 'plugin/global', GLOBAL_VARIABLES: 'plugin/global/list/global-variable', - DASHBOARD_ACCESSED: '/dashboard-event/dashboardAccessed', - DASHBOARD_LOGGEDIN: '/dashboard-event/dashboardLoggedIn', + DASHBOARD_ACCESSED: 'dashboard-event/dashboardAccessed', + DASHBOARD_LOGGEDIN: 'dashboard-event/dashboardLoggedIn', HELM_APP_HIBERNATE_API: 'application/hibernate', HELM_APP_UNHIBERNATE_API: 'application/unhibernate', EXTERNAL_LINKS_API: 'external-links', @@ -158,6 +162,7 @@ export const Routes = { NODE_LIST: 'k8s/capacity/node/list', NODE_CAPACITY: 'k8s/capacity/node', HELM_APP_TEMPLATE_CHART: 'application/template-chart', + TELEMETRY_EVENT: 'telemetry/event' } export const ViewType = { @@ -249,14 +254,17 @@ export const DOCUMENTATION = { PRE_POST_BUILD_STAGE: `${DOCUMENTATION_HOME_PAGE}/v/v0.5/usage/applications/creating-application/ci-pipeline/ci-build-pre-post-plugins`, CUSTOM_CHART: `${DOCUMENTATION_HOME_PAGE}/v/v0.5/getting-started/global-configurations/custom-charts`, - CUSTOM_CHART_PRE_REQUISITES: - `${DOCUMENTATION_HOME_PAGE}/v/v0.5/getting-started/global-configurations/custom-charts#prerequisites`, + CUSTOM_CHART_PRE_REQUISITES: `${DOCUMENTATION_HOME_PAGE}/v/v0.5/getting-started/global-configurations/custom-charts#prerequisites`, ADMIN_PASSWORD: `${DOCUMENTATION_HOME_PAGE}/v/v0.5/getting-started/install/install-devtron#devtron-admin-credentials`, EXTERNAL_LINKS: `${DOCUMENTATION_HOME_PAGE}/v/v0.5/getting-started/global-configurations/external-links`, GLOBAL_CONFIG_GIT_ACCESS_LINK: `${DOCUMENTATION_HOME_PAGE}/v/v0.5/getting-started/global-configurations/gitops#4.-git-access-credential`, - DEVTRON_UPGRADE: `${DOCUMENTATION_HOME_PAGE}/v/v0.5/getting-started/upgrade` + DEVTRON_UPGRADE: `${DOCUMENTATION_HOME_PAGE}/v/v0.5/getting-started/upgrade`, } +export const DEVTRON_NODE_DEPLOY_VIDEO = 'https://www.youtube.com/watch?v=9u-pKiWV-tM&t=1s' + +export const PREVIEW_DEVTRON = 'https://preview.devtron.ai/dashboard' + // APP LIST STARTS export const AppListConstants = { SAMPLE_NODE_REPO_URL: 'https://github.com/devtron-labs/getting-started-nodejs', @@ -533,4 +541,4 @@ export const EXTERNAL_TYPES = { HashiCorpVault: 'Hashi Corp Vault', } -export const ROLLOUT_DEPLOYMENT = 'Rollout Deployment' +export const ROLLOUT_DEPLOYMENT = 'Rollout Deployment' \ No newline at end of file diff --git a/src/config/routes.ts b/src/config/routes.ts index 307715f02e..9364219abb 100644 --- a/src/config/routes.ts +++ b/src/config/routes.ts @@ -58,6 +58,8 @@ export const URLS = { GLOBAL_CONFIG_PROJECT: '/global-config/projects', GLOBAL_CONFIG_LOGIN: '/global-config/login-service', GLOBAL_CONFIG_EXTERNAL_LINKS: '/global-config/external-links', + GUIDE: 'guide', + GETTING_STARTED: 'getting-started', SECURITY: '/security', STACK_MANAGER: '/stack-manager', STACK_MANAGER_DISCOVER_MODULES: '/stack-manager/discover', diff --git a/src/css/base.scss b/src/css/base.scss index c4b70e5338..9cf30039f8 100644 --- a/src/css/base.scss +++ b/src/css/base.scss @@ -1778,6 +1778,10 @@ button.anchor { line-height: 20px; } +.dc__lh-22{ + line-height: 22px; +} + .lh-32 { line-height: 32px; } @@ -2064,3 +2068,23 @@ textarea { fill: var(--N900); } } + +.href__link{ + color: var(--N900); +} + +.href__link:hover{ + color: var(--N900); +} + +.mt-120 { + margin-top: 120px; +} + +.h-300 { + min-height: 300px; +} + +.dc__window-bg{ + background: var(--window-bg); +} \ No newline at end of file diff --git a/src/css/colorPalette.css b/src/css/colorPalette.css index 8ed22c81a6..6dc2871cfb 100644 --- a/src/css/colorPalette.css +++ b/src/css/colorPalette.css @@ -48,4 +48,5 @@ --orange: rgba(252, 173, 38, 1); --window-bg: #f2f4f7; --white: #fff; + --gray: #1e23601a } diff --git a/src/services/service.ts b/src/services/service.ts index d2884ee6a7..0b13218b66 100644 --- a/src/services/service.ts +++ b/src/services/service.ts @@ -2,12 +2,13 @@ import { get, post } from './api'; import { ACCESS_TYPE_MAP, Routes } from '../config'; import { sortCallback } from '../components/common/helpers/util'; import moment from 'moment'; -import { ResponseType, CDPipelines, TeamList, AppListMin, ProjectFilteredApps, AppOtherEnvironment, LastExecutionResponseType, LastExecutionMinResponseType, APIOptions, ClusterEnvironmentDetailList, EnvironmentListHelmResponse, ClusterListResponse } from './service.types'; +import { ResponseType, CDPipelines, TeamList, AppListMin, ProjectFilteredApps, AppOtherEnvironment, LastExecutionResponseType, LastExecutionMinResponseType, APIOptions, ClusterEnvironmentDetailList, EnvironmentListHelmResponse, ClusterListResponse, LoginCountType } from './service.types'; import { Chart } from '../components/charts/charts.types'; import { fetchWithFullRoute } from './fetchWithFullRoute'; import { ModuleNameMap } from '../components/v2/devtronStackManager/DevtronStackManager.utils'; import { getModuleInfo } from '../components/v2/devtronStackManager/DevtronStackManager.service'; import { ModuleStatus } from '../components/v2/devtronStackManager/DevtronStackManager.type'; +import { LOGIN_COUNT } from '../components/onboardingGuide/onboarding.utils'; export function getAppConfigStatus(appId: number): Promise { @@ -415,3 +416,16 @@ export function dashboardAccessed() { export function dashboardLoggedIn() { return get(Routes.DASHBOARD_LOGGEDIN); } + + +export function getLoginData() : Promise { + return get(`${Routes.ATTRIBUTES_USER}/${Routes.GET}?key=${LOGIN_COUNT}`) +} + +export function updateLoginCount(payload): Promise { + return post(`${Routes.ATTRIBUTES_USER}/${Routes.UPDATE}`, payload) +} + +export function updatePostHogEvent(payload): Promise { + return post(Routes.TELEMETRY_EVENT, payload) +} diff --git a/src/services/service.types.ts b/src/services/service.types.ts index 90bfaa1667..dbb15a034e 100644 --- a/src/services/service.types.ts +++ b/src/services/service.types.ts @@ -182,4 +182,19 @@ export interface Cluster { id: number, cluster_name : string, active: boolean +} +export interface LoginCountType extends ResponseType { + result?: LoginCount +} + +export interface LoginCount { + emailId: string + key: string + value: string +} + +export interface AppRouterType { + isSuperAdmin?: boolean + appListCount: number + loginCount: number } \ No newline at end of file