diff --git a/packages/app/src/app/graphql/types.ts b/packages/app/src/app/graphql/types.ts index 1c203490373..02617a1591f 100644 --- a/packages/app/src/app/graphql/types.ts +++ b/packages/app/src/app/graphql/types.ts @@ -427,6 +427,7 @@ export type Team = { */ projects: Array; sandboxes: Array; + sdkWorkspace: Scalars['Boolean']; settings: Maybe; shortid: Scalars['String']; subscription: Maybe; @@ -3578,6 +3579,7 @@ export type CurrentTeamInfoFragmentFragment = { legacy: boolean; frozen: boolean; insertedAt: string; + sdkWorkspace: boolean; users: Array<{ __typename?: 'User'; id: any; @@ -5279,6 +5281,7 @@ export type GetTeamQuery = { legacy: boolean; frozen: boolean; insertedAt: string; + sdkWorkspace: boolean; users: Array<{ __typename?: 'User'; id: any; diff --git a/packages/app/src/app/hooks/useActiveTeamInfo.ts b/packages/app/src/app/hooks/useActiveTeamInfo.ts new file mode 100644 index 00000000000..a16ea5b0daa --- /dev/null +++ b/packages/app/src/app/hooks/useActiveTeamInfo.ts @@ -0,0 +1,37 @@ +import { useAppState } from 'app/overmind'; + +export type ActiveTeamInfo = { + id: string | null; + name: string | null; + sdkWorkspace: boolean; + frozen: boolean; +}; + +/** + * Hook to access active team information. + * + * This hook provides an abstraction layer over the Overmind state, + * making it easier to migrate away from Overmind in the future. + * + * @returns Active team information or null values if no team is active + */ +export const useActiveTeamInfo = (): ActiveTeamInfo => { + const { activeTeamInfo } = useAppState(); + + if (!activeTeamInfo) { + return { + id: null, + name: null, + sdkWorkspace: false, + frozen: false, + }; + } + + return { + id: activeTeamInfo.id, + name: activeTeamInfo.name, + sdkWorkspace: activeTeamInfo.sdkWorkspace ?? false, + frozen: activeTeamInfo.frozen, + }; +}; + diff --git a/packages/app/src/app/overmind/effects/gql/dashboard/fragments.ts b/packages/app/src/app/overmind/effects/gql/dashboard/fragments.ts index 3acf3adbd49..aea6bd95676 100644 --- a/packages/app/src/app/overmind/effects/gql/dashboard/fragments.ts +++ b/packages/app/src/app/overmind/effects/gql/dashboard/fragments.ts @@ -187,6 +187,7 @@ export const currentTeamInfoFragment = gql` legacy frozen insertedAt + sdkWorkspace users { id avatarUrl diff --git a/packages/app/src/app/pages/Dashboard/Content/routes/GetStarted/SDKRow.tsx b/packages/app/src/app/pages/Dashboard/Content/routes/GetStarted/SDKRow.tsx new file mode 100644 index 00000000000..047c8ef6d4f --- /dev/null +++ b/packages/app/src/app/pages/Dashboard/Content/routes/GetStarted/SDKRow.tsx @@ -0,0 +1,76 @@ +import track from '@codesandbox/common/lib/utils/analytics'; +import { ArticleCard, Text, Stack } from '@codesandbox/components'; +import { Carousel } from 'app/pages/Dashboard/Components/Carousel/Carousel'; +import React from 'react'; + +type ArticleProps = React.ComponentProps; + +type DocsItem = { label: string } & ArticleProps; + +export const appendOnboardingTracking = (url: string): string => { + const baseUrl = new URL(url); + baseUrl.searchParams.append('utm_source', 'dashboard_onboarding'); + + return baseUrl.toString(); +}; + +const DOCS: DocsItem[] = [ + { + label: 'docs_sdk_core-concepts', + title: 'Core concepts', + url: 'https://codesandbox.io/docs/sdk/core-concepts', + thumbnail: '/static/img/thumbnails/docs_getting-started.png', + }, + { + label: 'docs_sdk_manage-sandboxes', + title: 'Lifecycle management', + url: 'https://codesandbox.io/docs/sdk/manage-sandboxes', + thumbnail: '/static/img/thumbnails/youtube.png', + }, + { + label: 'docs_sdk_templates', + title: 'Templates', + url: 'https://codesandbox.io/docs/sdk/templates', + thumbnail: '/static/img/thumbnails/blog_design-system.png', + }, + { + label: 'docs_sdk_cli', + title: 'CLI Dashboard', + url: 'https://codesandbox.io/docs/sdk/cli', + thumbnail: '/static/img/thumbnails/video_command-palette.png', + }, +]; + +export const SDKRow = () => { + const handleTrack = (label: string) => { + track('Empty State Card - Content card', { + codesandbox: 'V1', + event_source: 'UI', + card_type: label, + }); + }; + + const items = DOCS.map(({ label, url, ...item }) => { + const urlWithTracking = appendOnboardingTracking(url); + + return { + id: label, + Component: ArticleCard, + props: { + onClick: () => handleTrack(label), + url: urlWithTracking, + ...item, + }, + }; + }); + + return ( + + + Get started with the SDK + + + + ); +}; + diff --git a/packages/app/src/app/pages/Dashboard/Content/routes/GetStarted/index.tsx b/packages/app/src/app/pages/Dashboard/Content/routes/GetStarted/index.tsx index 8243a2a437b..cd81ff4185c 100644 --- a/packages/app/src/app/pages/Dashboard/Content/routes/GetStarted/index.tsx +++ b/packages/app/src/app/pages/Dashboard/Content/routes/GetStarted/index.tsx @@ -2,20 +2,32 @@ import React from 'react'; import { Helmet } from 'react-helmet'; import { StyledContentWrapper } from 'app/pages/Dashboard/Components/shared/elements'; import { Element } from '@codesandbox/components'; +import { useActiveTeamInfo } from 'app/hooks/useActiveTeamInfo'; import { InstructionsRow } from './InstructionsRow'; import { DocumentationRow } from './DocumentationRow'; +import { SDKRow } from './SDKRow'; export const GetStarted = () => { + const { sdkWorkspace } = useActiveTeamInfo(); + return ( Get started - CodeSandbox - - - - + {sdkWorkspace ? ( + + + + ) : ( + <> + + + + + + )} ); }; diff --git a/packages/app/src/app/pages/Dashboard/Header/index.tsx b/packages/app/src/app/pages/Dashboard/Header/index.tsx index 801a83d21b4..5c53dd75a0e 100644 --- a/packages/app/src/app/pages/Dashboard/Header/index.tsx +++ b/packages/app/src/app/pages/Dashboard/Header/index.tsx @@ -12,6 +12,7 @@ import { UserMenu } from 'app/pages/common/UserMenu'; import { Notifications } from 'app/components/Notifications'; import { dashboard as dashboardUrls } from '@codesandbox/common/lib/utils/url-generator'; import { useWorkspaceLimits } from 'app/hooks/useWorkspaceLimits'; +import { useActiveTeamInfo } from 'app/hooks/useActiveTeamInfo'; import { TeamAvatar } from 'app/components/TeamAvatar'; import { WorkspaceSelect } from 'app/components/WorkspaceSelect'; import { SkeletonTextBlock } from 'app/components/Skeleton/elements'; @@ -25,6 +26,7 @@ export const Header: React.FC = React.memo( const history = useHistory(); const actions = useActions(); const { isFrozen } = useWorkspaceLimits(); + const { sdkWorkspace } = useActiveTeamInfo(); const { activeWorkspaceAuthorization, hasLogIn, @@ -96,31 +98,35 @@ export const Header: React.FC = React.memo( - - - + {!sdkWorkspace && ( + <> + + + + + )} {hasLogIn && } diff --git a/packages/app/src/app/pages/Dashboard/Sidebar/index.tsx b/packages/app/src/app/pages/Dashboard/Sidebar/index.tsx index 9922dbf55ad..5fb5f207f57 100644 --- a/packages/app/src/app/pages/Dashboard/Sidebar/index.tsx +++ b/packages/app/src/app/pages/Dashboard/Sidebar/index.tsx @@ -7,6 +7,7 @@ import css from '@styled-system/css'; import { useWorkspaceAuthorization } from 'app/hooks/useWorkspaceAuthorization'; import { useWorkspaceSubscription } from 'app/hooks/useWorkspaceSubscription'; import { useWorkspaceFeatureFlags } from 'app/hooks/useWorkspaceFeatureFlags'; +import { useActiveTeamInfo } from 'app/hooks/useActiveTeamInfo'; import { ContextMenu } from './ContextMenu'; import { DashboardBaseFolder } from '../types'; import { Position } from '../Components/Selection'; @@ -76,7 +77,13 @@ export const Sidebar: React.FC = ({ setNewFolderPath, }; - const showRespositories = !state.environment.isOnPrem; + const { sdkWorkspace } = useActiveTeamInfo(); + + const hasRepositories = state.activeTeam && state.sidebar[state.activeTeam] + ? state.sidebar[state.activeTeam].repositories.length > 0 + : false; + + const showRespositories = !state.environment.isOnPrem && (!sdkWorkspace || hasRepositories); const { ubbBeta } = useWorkspaceFeatureFlags(); const { isPrimarySpace, isTeamAdmin, isTeamEditor } = useWorkspaceAuthorization(); @@ -204,15 +211,17 @@ export const Sidebar: React.FC = ({ size={2} css={css({ color: 'sideBarSectionHeader.foreground' })} > - Devboxes and Sandboxes + {sdkWorkspace ? 'Sandboxes' : 'Devboxes and Sandboxes'} - + {!sdkWorkspace && ( + + )} = ({ path={dashboardUrls.deleted(activeTeam)} icon="trash" /> - - + {!sdkWorkspace && ( + <> + + + + )} {ubbBeta && state.activeTeamInfo && (