Skip to content

Commit

Permalink
feat(web): Local studio being local (novuhq#5812)
Browse files Browse the repository at this point in the history
* feat(web): Load local studio via npm run dev

* feat: Add back button via header

---------

Co-authored-by: Denis Kralj <[email protected]>
Co-authored-by: Joel Anton <[email protected]>
Co-authored-by: Dima Grossman <[email protected]>
  • Loading branch information
4 people authored Jun 26, 2024
1 parent 452c5b8 commit 0e4c0e3
Show file tree
Hide file tree
Showing 49 changed files with 1,071 additions and 344 deletions.
1 change: 1 addition & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,7 @@
"npmjs",
"npmrc",
"nrwl",
"ntfr",
"ntoa",
"ntvs",
"nunito",
Expand Down
29 changes: 16 additions & 13 deletions apps/web/src/AppRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ import { AccessSecurityPage, BillingPage, TeamPage, UserProfilePage } from './pa
import { SettingsPageNew as SettingsPage } from './pages/settings/SettingsPageNew';
import { OrganizationPage } from './pages/settings/organization';
import { LayoutsPage } from './pages/layouts/LayoutsPage';
import { StudioPageLayout } from './studio/StudioPageLayout';
import { LocalStudioAuthenticator } from './studio/LocalStudioAuthenticator';
import {
WorkflowsListPage,
WorkflowsDetailPage,
Expand Down Expand Up @@ -101,7 +103,7 @@ export const AppRoutes = () => {
<Route path={ROUTES.WORKFLOWS_DIGEST_PLAYGROUND} element={<TemplatesDigestPlaygroundPage />} />
<Route path={ROUTES.WORKFLOWS_CREATE} element={<TemplateEditorPage />} />
<Route path={ROUTES.WORKFLOWS_V2_STEP_EDIT} element={<WorkflowsStepEditorPageV2 />} />

<Route path={ROUTES.WORKFLOWS_V2_TEST} element={<WorkflowsTestPage />} />
<Route path={ROUTES.WORKFLOWS_EDIT_TEMPLATEID} element={<TemplateEditorPage />}>
<Route path="" element={<Sidebar />} />
<Route path="settings" element={<TemplateSettings />} />
Expand Down Expand Up @@ -155,24 +157,25 @@ export const AppRoutes = () => {
<Route path={ROUTES.TEAM} element={<MembersInvitePage />} />
<Route path={ROUTES.CHANGES} element={<PromoteChangesPage />} />
<Route path={ROUTES.SUBSCRIBERS} element={<SubscribersList />} />
<Route path={ROUTES.WORKFLOWS_V2_TEST} element={<WorkflowsTestPage />} />
<Route path={ROUTES.STUDIO}>
<Route path="" element={<Navigate to={ROUTES.STUDIO_FLOWS} replace />} />
<Route path={ROUTES.STUDIO_FLOWS} element={<WorkflowsListPage />} />
<Route path={ROUTES.STUDIO_FLOWS_VIEW} element={<WorkflowsDetailPage />} />
<Route path={ROUTES.STUDIO_FLOWS_STEP_EDITOR} element={<WorkflowsStepEditorPage />} />
<Route path={ROUTES.STUDIO_FLOWS_TEST} element={<WorkflowsTestPage />} />
</Route>

<Route path="/translations/*" element={<TranslationRoutes />} />
<Route path={ROUTES.LAYOUT} element={<LayoutsPage />} />
<Route path={ROUTES.API_KEYS} element={<ApiKeysPage />} />
<Route path={ROUTES.WEBHOOK} element={<WebhookPage />} />
<Route path={ROUTES.ANY} element={<HomePage />} />
</Route>
<Route path={ROUTES.STUDIO_ONBOARDING} element={<StudioOnboarding />} />
<Route path={ROUTES.STUDIO_ONBOARDING_PREVIEW} element={<StudioOnboardingPreview />} />
<Route path={ROUTES.STUDIO_ONBOARDING_SUCCESS} element={<StudioOnboardingSuccess />} />

<Route path={ROUTES.LOCAL_STUDIO_AUTH} element={<LocalStudioAuthenticator />} />

<Route path={ROUTES.STUDIO} element={<StudioPageLayout />}>
<Route path="" element={<Navigate to={ROUTES.STUDIO_FLOWS} replace />} />
<Route path={ROUTES.STUDIO_FLOWS} element={<WorkflowsListPage />} />
<Route path={ROUTES.STUDIO_FLOWS_VIEW} element={<WorkflowsDetailPage />} />
<Route path={ROUTES.STUDIO_FLOWS_STEP_EDITOR} element={<WorkflowsStepEditorPage />} />
<Route path={ROUTES.STUDIO_FLOWS_TEST} element={<WorkflowsTestPage />} />
<Route path={ROUTES.STUDIO_ONBOARDING} element={<StudioOnboarding />} />
<Route path={ROUTES.STUDIO_ONBOARDING_PREVIEW} element={<StudioOnboardingPreview />} />
<Route path={ROUTES.STUDIO_ONBOARDING_SUCCESS} element={<StudioOnboardingSuccess />} />
</Route>
</Routes>
);
};
5 changes: 4 additions & 1 deletion apps/web/src/Providers.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ThemeProvider } from '@novu/design-system';
import { SegmentProvider } from './components/providers/SegmentProvider';
import { StudioStateProvider } from './studio/StudioStateProvider';
import { CONTEXT_PATH } from './config';
import * as Sentry from '@sentry/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
Expand Down Expand Up @@ -32,7 +33,9 @@ const Providers: React.FC<PropsWithChildren<{}>> = ({ children }) => {
<SegmentProvider>
<QueryClientProvider client={queryClient}>
<BrowserRouter basename={CONTEXT_PATH}>
<HelmetProvider>{children}</HelmetProvider>
<HelmetProvider>
<StudioStateProvider>{children}</StudioStateProvider>
</HelmetProvider>
</BrowserRouter>
</QueryClientProvider>
</SegmentProvider>
Expand Down
37 changes: 0 additions & 37 deletions apps/web/src/api/bridge/bridge.api.ts

This file was deleted.

55 changes: 0 additions & 55 deletions apps/web/src/api/bridge/bridge.http.ts

This file was deleted.

5 changes: 0 additions & 5 deletions apps/web/src/api/bridge/utils.ts

This file was deleted.

95 changes: 95 additions & 0 deletions apps/web/src/bridgeApi/bridgeApi.client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import axios from 'axios';

export type StepPreviewParams = {
workflowId: string;
stepId: string;
payload: Record<string, unknown>;
controls: Record<string, unknown>;
};

export type TriggerParams = {
workflowId: string;
bridgeUrl?: string;
to: { subscriberId: string; email: string };
payload: Record<string, unknown>;
};

export function buildBridgeHTTPClient(baseURL: string) {
const httpClient = axios.create({
baseURL,
headers: {
'Content-Type': 'application/json',
// Required for localtunnel.it
'Bypass-Tunnel-Reminder': true,
},
});

const get = async (url, params = {}) => {
try {
const response = await httpClient.get(url, { params });

return response.data;
} catch (error) {
// TODO: Handle error?.response?.data || error?.response || error;
throw error;
}
};

// POST method
const post = async (url, data = {}) => {
try {
const response = await httpClient.post(url, data);

return response.data;
} catch (error) {
// TODO: Handle error?.response?.data || error?.response || error;
throw error;
}
};

return {
/**
* TODO: Use framework shared types
*/
async discover(): Promise<{ workflows: any[] }> {
return get('', {
action: 'discover',
});
},

/**
* TODO: Use framework shared types
*/
async getWorkflow(workflowId: string): Promise<any> {
const { workflows } = await this.discover();

return workflows.find((workflow) => workflow.workflowId === workflowId);
},

/**
* TODO: Use framework shared types
*/
async getStepPreview({ workflowId, stepId, controls, payload }: StepPreviewParams): Promise<any> {
return post(`${baseURL}?action=preview&workflowId=${workflowId}&stepId=${stepId}`, {
// TODO: Rename to controls
inputs: controls || {},
// TODO: Rename to payload
data: payload || {},
});
},

/**
* TODO: Use framework shared types
*/
async trigger({ workflowId, bridgeUrl, to, payload }: TriggerParams): Promise<any> {
payload = payload || {};
payload.__source = 'studio-test-workflow';

return post(`${baseURL}?action=trigger&workflowId=${workflowId}`, {
bridgeUrl,
to,
payload,
});
},
};
}
43 changes: 43 additions & 0 deletions apps/web/src/components/layout/components/LocalStudioHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Header } from '@mantine/core';
import { Text } from '@novu/novui';
import { IconOutlineArrowBack } from '@novu/novui/icons';
import { hstack } from '@novu/novui/patterns';
import { FC } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { HEADER_NAV_HEIGHT } from '../constants';

export const LocalStudioHeader: FC = () => {
const navigate = useNavigate();

return (
<Header
height={`${HEADER_NAV_HEIGHT}px`}
className={hstack({
position: 'sticky',
top: 0,
borderBottom: 'none !important',
// TODO: fix when we re-do z-index across the app
zIndex: 199,
padding: '50',
justifyContent: 'flex-start',
})}
>
{/** TODO temporary back-button. To be refined later */}
<button
className={hstack({
cursor: 'pointer',
gap: 'margins.icons.Icon20-txt',
px: '75',
py: '25',
_hover: { opacity: 'hover' },
})}
onClick={() => navigate(-1)}
>
<IconOutlineArrowBack />
<Text fontWeight="strong" color="typography.text.secondary">
Back
</Text>
</button>
</Header>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import * as Sentry from '@sentry/react';
import { Outlet } from 'react-router-dom';
import styled from '@emotion/styled';
import { css } from '@novu/novui/css';
import { LocalStudioHeader } from './LocalStudioHeader';

const AppShell = styled.div`
display: flex;
width: 100vw;
height: 100vh;
min-width: 1024px;
`;

const ContentShell = styled.div`
display: flex;
flex-direction: column;
flex: 1 1 0%;
overflow: hidden; // for appropriate scroll
`;

export function LocalStudioPageLayout() {
return (
<Sentry.ErrorBoundary
fallback={({ error, eventId }) => (
<>
Sorry, but something went wrong. <br />
Our team has been notified and we are investigating.
<br />
<code>
<small style={{ color: 'lightGrey' }}>
Event Id: {eventId}.
<br />
{error.toString()}
</small>
</code>
</>
)}
>
<AppShell className={css({ '& *': { colorPalette: 'mode.local' } })}>
<ContentShell>
<LocalStudioHeader />
<Outlet />
</ContentShell>
</AppShell>
</Sentry.ErrorBoundary>
);
}
Loading

0 comments on commit 0e4c0e3

Please sign in to comment.