Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions studio/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,22 @@ This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-opti

We use [Connect](https://connect.build/) to unify the communication between all components of the cosmo platform. Connect is a framework build on top of [gRPC](https://grpc.io/) and simplify code-generation and reuse between `Studio` -> `Controlplane`.

## Source Maps (Firefox)

Firefox does not handle webpack's default `eval-source-map` devtool correctly ([webpack#9267](https://github.com/webpack/webpack/issues/9267)). To get proper source maps in Firefox DevTools, set:

```bash
NEXT_DEVTOOL=source-map pnpm dev
```

or with make:

```bash
NEXT_DEVTOOL=source-map make start-studio
```

This generates separate `.map` files instead of eval-based inline maps. Note: incremental rebuilds will be slower.

## Docker Info

We want runtime envs for docker for each on prem customer. Therefore we have two files to achieve this. One is .env.docker that uses a placeholder env name and an entrypoint.sh script that replaces all placeholder env name with the correct one at runtime in the .next folder. This also requires us to SSR the studio.
17 changes: 16 additions & 1 deletion studio/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,22 @@ const config = {
},
// This is done to reduce the production build size
// see: https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/tree-shaking/
webpack: (config, { webpack }) => {
webpack: (config, { webpack, dev, isServer }) => {
// Firefox doesn't handle eval-based source maps well (webpack/webpack#9267).
// Replace Next.js's default EvalSourceMapDevToolPlugin with SourceMapDevToolPlugin
// to generate proper source maps. Opt-in via NEXT_DEVTOOL=source-map.
if (dev && !isServer && process.env.NEXT_DEVTOOL === 'source-map') {
config.plugins = config.plugins.filter((plugin) => plugin.constructor.name !== 'EvalSourceMapDevToolPlugin');
config.devtool = false;
config.plugins.push(
new webpack.SourceMapDevToolPlugin({
filename: '[file].map',
module: true,
columns: true,
}),
);
}

config.plugins.push(
new webpack.DefinePlugin({
__SENTRY_TRACING__: !isSentryTracesEnabled,
Expand Down
3 changes: 2 additions & 1 deletion studio/src/components/app-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,8 @@ export const AppProvider = ({ children }: { children: ReactNode }) => {

if (
(router.pathname === '/' || router.pathname === '/login' || !currentOrg) &&
router.pathname !== '/account/invitations'
router.pathname !== '/account/invitations' &&
!router.pathname.startsWith('/onboarding')
) {
const url = new URL(window.location.origin + router.basePath + router.asPath);
const params = new URLSearchParams(url.search);
Expand Down
3 changes: 3 additions & 0 deletions studio/src/components/dashboard/workspace-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { WorkspaceNamespace } from '@wundergraph/cosmo-connect/dist/platform/v1/
import { useRouter } from 'next/router';
import { useApplyParams } from '@/components/analytics/use-apply-params';
import { useLocalStorage } from '@/hooks/use-local-storage';
import { useOnboardingNavigation } from '@/hooks/use-onboarding-navigation';
import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common/common_pb';

const DEFAULT_NAMESPACE_NAME = 'default';
Expand Down Expand Up @@ -103,6 +104,8 @@ export function WorkspaceProvider({ children }: React.PropsWithChildren) {
[namespace, namespaces, setStoredNamespace, applyParams],
);

useOnboardingNavigation();

// Finally, render :)
return (
<WorkspaceContext.Provider
Expand Down
28 changes: 27 additions & 1 deletion studio/src/components/federatedgraphs-cards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@ import { docsBaseURL } from '@/lib/constants';
import { formatMetric } from '@/lib/format-metric';
import { useChartData } from '@/lib/insights-helpers';
import { cn } from '@/lib/utils';
import { ChevronDoubleRightIcon, CommandLineIcon, DocumentArrowDownIcon } from '@heroicons/react/24/outline';
import {
BoltIcon,
BookmarkIcon,
ChevronDoubleRightIcon,
CommandLineIcon,
DocumentArrowDownIcon,
} from '@heroicons/react/24/outline';
import { Component2Icon } from '@radix-ui/react-icons';
import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common/common_pb';
import { migrateFromApollo } from '@wundergraph/cosmo-connect/dist/platform/v1/platform-PlatformService_connectquery';
Expand Down Expand Up @@ -38,6 +44,7 @@ import { useMutation } from '@connectrpc/connect-query';
import { useCheckUserAccess } from '@/hooks/use-check-user-access';
import { useWorkspace } from '@/hooks/use-workspace';
import { useCurrentOrganization } from '@/hooks/use-current-organization';
import { useOnboarding } from '@/hooks/use-onboarding';

// this is required to render a blank line with LineChart
const fallbackData = [
Expand Down Expand Up @@ -616,6 +623,7 @@ export const FederatedGraphsCards = ({ graphs, refetch }: { graphs?: FederatedGr
const [token, setToken] = useState<string | undefined>();
const [isMigrating, setIsMigrating] = useState(false);
const checkUserAccess = useCheckUserAccess();
const { onboarding, enabled, currentStep } = useOnboarding();

useEffect(() => {
if (isMigrationSuccess) {
Expand All @@ -624,6 +632,24 @@ export const FederatedGraphsCards = ({ graphs, refetch }: { graphs?: FederatedGr
}
}, [isMigrationSuccess]);

if (enabled && onboarding && onboarding.federatedGraphsCount === 0) {
return currentStep !== undefined && !onboarding.finishedAt ? (
<EmptyState
icon={<BookmarkIcon />}
title="Dive right back in"
description="Want to finish the onboarding and create your first federated graph?"
actions={<Link href={`/onboarding/${currentStep}`}>Continue</Link>}
/>
) : (
<EmptyState
icon={<BoltIcon />}
title="Need help?"
description="Take a quick 5-minute tour to help you set up your first federated graph"
actions={<Link href={`/onboarding/1`}>Start here</Link>}
/>
);
}

if (!graphs || graphs.length === 0)
return (
<Empty
Expand Down
11 changes: 11 additions & 0 deletions studio/src/components/layout/onboarding-layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface OnboardingLayoutProps {
children?: React.ReactNode;
}

export const OnboardingLayout = ({ children }: OnboardingLayoutProps) => {
return (
<div className="flex min-h-screen w-full flex-col items-center justify-center bg-background font-sans antialiased">
<main className="w-full max-w-lg px-4">{children}</main>
</div>
);
};
86 changes: 86 additions & 0 deletions studio/src/components/onboarding/onboarding-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import {
createContext,
type Dispatch,
useCallback,
useContext,
useMemo,
useState,
type SetStateAction,
type ReactNode,
} from 'react';
import { PostHogFeatureFlagContext } from '../posthog-feature-flag-provider';
import { useSessionStorage } from '@/hooks/use-session-storage';

type Onboarding = {
finishedAt?: Date;
federatedGraphsCount: number;
};

export interface OnboardingState {
enabled: boolean;
onboarding?: Onboarding;
setOnboarding: Dispatch<SetStateAction<Onboarding | undefined>>;
currentStep: number | undefined;
setStep: (step: number | undefined) => void;
skipped: boolean;
setSkipped: () => void;
resetSkipped: () => void;
}

export const OnboardingContext = createContext<OnboardingState>({
onboarding: undefined,
enabled: false,
setOnboarding: () => undefined,
currentStep: undefined,
setStep: () => undefined,
skipped: false,
setSkipped: () => undefined,
resetSkipped: () => undefined,
});

const ONBOARDING_V1_LAST_STEP = 4;

export const OnboardingProvider = ({ children }: { children: ReactNode }) => {
const { onboarding: onboardingFlag, status: featureFlagStatus } = useContext(PostHogFeatureFlagContext);
const [onboarding, setOnboarding] = useState<Onboarding | undefined>(undefined);
const [currentStep, setCurrentStep] = useSessionStorage<undefined | number>('cosmo-onboarding-v1-step', undefined);
const [skipped, setSkippedValue] = useSessionStorage('cosmo-onboarding-v1-skipped', false);

const setSkipped = useCallback(() => {
setSkippedValue(true);
}, [setSkippedValue]);

const resetSkipped = useCallback(() => {
setSkippedValue(false);
}, [setSkippedValue]);

const setStep = useCallback(
(step: number | undefined) => {
if (step === undefined) {
setCurrentStep(1);
resetSkipped();
return;
}

resetSkipped();
setCurrentStep(Math.max(Math.min(step, ONBOARDING_V1_LAST_STEP), 0));
},
[setCurrentStep, resetSkipped],
);

const value = useMemo(
() => ({
onboarding,
enabled: Boolean(onboardingFlag.enabled && featureFlagStatus === 'success' && onboardingFlag),
setOnboarding,
currentStep,
setStep,
setSkipped,
resetSkipped,
skipped,
}),
[onboarding, onboardingFlag, featureFlagStatus, currentStep, setStep, setSkipped, resetSkipped, skipped],
);

return <OnboardingContext.Provider value={value}>{children}</OnboardingContext.Provider>;
};
76 changes: 76 additions & 0 deletions studio/src/components/onboarding/step-1.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { useEffect } from 'react';
import { Link } from '../ui/link';
import { Button } from '../ui/button';
import { useOnboarding } from '@/hooks/use-onboarding';
import { useMutation } from '@connectrpc/connect-query';
import { createOnboarding } from '@wundergraph/cosmo-connect/dist/platform/v1/platform-PlatformService_connectquery';
import { useRouter } from 'next/router';
import { useCurrentOrganization } from '@/hooks/use-current-organization';
import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common/common_pb';
import { useToast } from '../ui/use-toast';

export const Step1 = () => {
const router = useRouter();
const { toast } = useToast();
const organization = useCurrentOrganization();
const { setStep, setSkipped, setOnboarding } = useOnboarding();

const { mutate, isPending } = useMutation(createOnboarding, {
onSuccess: (d) => {
if (d.response?.code !== EnumStatusCode.OK) {
toast({
description: d.response?.details ?? 'We had issues with storing your data. Please try again.',
duration: 3000,
});
return;
}

setOnboarding({
federatedGraphsCount: d.federatedGraphsCount,
finishedAt: d.finishedAt ? new Date(d.finishedAt) : undefined,
});
router.push('/onboarding/2');
},
onError: (error) => {
toast({
description: error.details.toString() ?? 'We had issues with storing your data. Please try again.',
duration: 3000,
});
},
});

useEffect(() => {
setStep(1);
}, [setStep]);

return (
<div className="flex flex-col items-center gap-4 text-center">
<h2 className="text-2xl font-semibold tracking-tight">Step 1</h2>
<div className="flex w-full justify-between">
<Button asChild variant="secondary" onClick={setSkipped}>
<Link href="/">Skip</Link>
</Button>
<div className="flex">
<Button className="mr-2" asChild disabled>
<Link href="#">Back</Link>
</Button>
<Button
onClick={() => {
// TODO: replace with real values in form
mutate({
organizationName: organization?.name ?? '',
slack: true,
email: false,
invititationEmails: [],
});
}}
isLoading={isPending}
disabled={isPending}
>
Next
</Button>
</div>
</div>
</div>
);
};
31 changes: 31 additions & 0 deletions studio/src/components/onboarding/step-2.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useEffect } from 'react';
import { Link } from '../ui/link';
import { Button } from '../ui/button';
import { useOnboarding } from '@/hooks/use-onboarding';

export const Step2 = () => {
const { setStep, setSkipped } = useOnboarding();

useEffect(() => {
setStep(2);
}, [setStep]);

return (
<div className="flex flex-col items-center gap-4 text-center">
<h2 className="text-2xl font-semibold tracking-tight">Step 2</h2>
<div className="flex w-full justify-between">
<Button asChild variant="secondary" onClick={setSkipped}>
<Link href="/">Skip</Link>
</Button>
<div className="flex">
<Button className="mr-2" asChild>
<Link href="/onboarding/1">Back</Link>
</Button>
<Button asChild>
<Link href="/onboarding/3">Next</Link>
</Button>
</div>
</div>
</div>
);
};
31 changes: 31 additions & 0 deletions studio/src/components/onboarding/step-3.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useEffect } from 'react';
import { Link } from '../ui/link';
import { Button } from '../ui/button';
import { useOnboarding } from '@/hooks/use-onboarding';

export const Step3 = () => {
const { setStep, setSkipped } = useOnboarding();

useEffect(() => {
setStep(3);
}, [setStep]);

return (
<div className="flex flex-col items-center gap-4 text-center">
<h2 className="text-2xl font-semibold tracking-tight">Step 3</h2>
<div className="flex w-full justify-between">
<Button asChild variant="secondary" onClick={setSkipped}>
<Link href="/">Skip</Link>
</Button>
<div className="flex">
<Button className="mr-2" asChild>
<Link href="/onboarding/2">Back</Link>
</Button>
<Button asChild>
<Link href="/onboarding/4">Next</Link>
</Button>
</div>
</div>
</div>
);
};
Loading
Loading