mirror of https://github.com/coder/coder.git
feat(site): warn user if they leave the editor without publishing (#12406)
This commit is contained in:
parent
61bd341a36
commit
bc30c9c013
|
@ -1,13 +1,14 @@
|
|||
import { QueryClient, QueryClientProvider } from "react-query";
|
||||
import { type FC, type ReactNode, useEffect, useState } from "react";
|
||||
import { HelmetProvider } from "react-helmet-async";
|
||||
import { AppRouter } from "./AppRouter";
|
||||
import { router } from "./router";
|
||||
import { ThemeProvider } from "./contexts/ThemeProvider";
|
||||
import { AuthProvider } from "./contexts/auth/AuthProvider";
|
||||
import { ErrorBoundary } from "./components/ErrorBoundary/ErrorBoundary";
|
||||
import { GlobalSnackbar } from "./components/GlobalSnackbar/GlobalSnackbar";
|
||||
import "./theme/globalFonts";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
import { RouterProvider } from "react-router-dom";
|
||||
|
||||
const defaultQueryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
|
@ -61,7 +62,7 @@ export const App: FC = () => {
|
|||
return (
|
||||
<AppProviders>
|
||||
<ErrorBoundary>
|
||||
<AppRouter />
|
||||
<RouterProvider router={router} />
|
||||
</ErrorBoundary>
|
||||
</AppProviders>
|
||||
);
|
||||
|
|
|
@ -6,7 +6,7 @@ import { MONOSPACE_FONT_FAMILY } from "theme/constants";
|
|||
|
||||
loader.config({ monaco });
|
||||
|
||||
interface MonacoEditorProps {
|
||||
export interface MonacoEditorProps {
|
||||
value?: string;
|
||||
path?: string;
|
||||
onChange?: (value: string) => void;
|
||||
|
|
|
@ -2,7 +2,10 @@ import Button from "@mui/material/Button";
|
|||
import IconButton from "@mui/material/IconButton";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import CreateIcon from "@mui/icons-material/AddOutlined";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import {
|
||||
Link as RouterLink,
|
||||
unstable_usePrompt as usePrompt,
|
||||
} from "react-router-dom";
|
||||
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
|
||||
import { type FC, useCallback, useEffect, useRef, useState } from "react";
|
||||
import AlertTitle from "@mui/material/AlertTitle";
|
||||
|
@ -65,8 +68,8 @@ export interface TemplateVersionEditorProps {
|
|||
defaultFileTree: FileTree;
|
||||
buildLogs?: ProvisionerJobLog[];
|
||||
resources?: WorkspaceResource[];
|
||||
disablePreview?: boolean;
|
||||
disableUpdate?: boolean;
|
||||
isBuilding: boolean;
|
||||
canPublish: boolean;
|
||||
onPreview: (files: FileTree) => Promise<void>;
|
||||
onPublish: () => void;
|
||||
onConfirmPublish: (data: PublishVersionData) => void;
|
||||
|
@ -88,8 +91,8 @@ export interface TemplateVersionEditorProps {
|
|||
}
|
||||
|
||||
export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
|
||||
disablePreview,
|
||||
disableUpdate,
|
||||
isBuilding,
|
||||
canPublish,
|
||||
template,
|
||||
templateVersion,
|
||||
defaultFileTree,
|
||||
|
@ -179,6 +182,10 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
|
|||
}
|
||||
}, [buildLogs]);
|
||||
|
||||
useLeaveSiteWarning(canPublish);
|
||||
|
||||
const canBuild = !isBuilding && dirty;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div css={{ height: "100%", display: "flex", flexDirection: "column" }}>
|
||||
|
@ -242,7 +249,7 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
|
|||
borderLeft: "1px solid #FFF",
|
||||
},
|
||||
}}
|
||||
disabled={disablePreview}
|
||||
disabled={!canBuild}
|
||||
>
|
||||
<TopbarButton
|
||||
startIcon={
|
||||
|
@ -251,7 +258,7 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
|
|||
/>
|
||||
}
|
||||
title="Build template (Ctrl + Enter)"
|
||||
disabled={disablePreview}
|
||||
disabled={!canBuild}
|
||||
onClick={async () => {
|
||||
await triggerPreview();
|
||||
}}
|
||||
|
@ -276,7 +283,7 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
|
|||
|
||||
<TopbarButton
|
||||
variant="contained"
|
||||
disabled={dirty || disableUpdate}
|
||||
disabled={dirty || !canPublish}
|
||||
onClick={onPublish}
|
||||
>
|
||||
Publish
|
||||
|
@ -540,7 +547,7 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
|
|||
</button>
|
||||
|
||||
<button
|
||||
disabled={disableUpdate}
|
||||
disabled={!canPublish}
|
||||
css={styles.tab}
|
||||
className={selectedTab === "resources" ? "active" : ""}
|
||||
onClick={() => {
|
||||
|
@ -649,6 +656,38 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
const useLeaveSiteWarning = (enabled: boolean) => {
|
||||
const MESSAGE =
|
||||
"You have unpublished changes. Are you sure you want to leave?";
|
||||
|
||||
// This works for regular browser actions like close tab and back button
|
||||
useEffect(() => {
|
||||
const onBeforeUnload = (e: BeforeUnloadEvent) => {
|
||||
if (enabled) {
|
||||
e.preventDefault();
|
||||
return MESSAGE;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("beforeunload", onBeforeUnload);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("beforeunload", onBeforeUnload);
|
||||
};
|
||||
}, [enabled]);
|
||||
|
||||
// This is used for react router navigation that is not triggered by the
|
||||
// browser
|
||||
usePrompt({
|
||||
message: MESSAGE,
|
||||
when: ({ nextLocation }) => {
|
||||
// We need to check the path because we change the URL when new template
|
||||
// version is created during builds
|
||||
return enabled && !nextLocation.pathname.endsWith("/edit");
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const styles = {
|
||||
tab: (theme) => ({
|
||||
"&:not(:disabled)": {
|
||||
|
|
|
@ -22,6 +22,7 @@ import { server } from "testHelpers/server";
|
|||
import { rest } from "msw";
|
||||
import { AppProviders } from "App";
|
||||
import { TemplateVersion } from "api/typesGenerated";
|
||||
import { MonacoEditorProps } from "./MonacoEditor";
|
||||
|
||||
// For some reason this component in Jest is throwing a MUI style warning so,
|
||||
// since we don't need it for this test, we can mock it out
|
||||
|
@ -35,7 +36,15 @@ jest.mock(
|
|||
// Occasionally, Jest encounters HTML5 canvas errors. As the MonacoEditor is not
|
||||
// required for these tests, we can safely mock it.
|
||||
jest.mock("pages/TemplateVersionEditorPage/MonacoEditor", () => ({
|
||||
MonacoEditor: () => <div />,
|
||||
MonacoEditor: (props: MonacoEditorProps) => (
|
||||
<textarea
|
||||
data-testid="monaco-editor"
|
||||
value={props.value}
|
||||
onChange={(e) => {
|
||||
props.onChange?.(e.target.value);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
|
||||
const renderTemplateEditorPage = () => {
|
||||
|
@ -51,6 +60,11 @@ const renderTemplateEditorPage = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const typeOnEditor = async (value: string, user: UserEvent) => {
|
||||
const editor = await screen.findByTestId("monaco-editor");
|
||||
await user.type(editor, value);
|
||||
};
|
||||
|
||||
const buildTemplateVersion = async (
|
||||
templateVersion: TemplateVersion,
|
||||
user: UserEvent,
|
||||
|
@ -94,6 +108,8 @@ test("Use custom name, message and set it as active when publishing", async () =
|
|||
id: "new-version-id",
|
||||
name: "new-version",
|
||||
};
|
||||
|
||||
await typeOnEditor("new content", user);
|
||||
await buildTemplateVersion(newTemplateVersion, user, topbar);
|
||||
|
||||
// Publish
|
||||
|
@ -138,6 +154,8 @@ test("Do not mark as active if promote is not checked", async () => {
|
|||
id: "new-version-id",
|
||||
name: "new-version",
|
||||
};
|
||||
|
||||
await typeOnEditor("new content", user);
|
||||
await buildTemplateVersion(newTemplateVersion, user, topbar);
|
||||
|
||||
// Publish
|
||||
|
@ -181,6 +199,8 @@ test("Patch request is not send when there are no changes", async () => {
|
|||
name: "new-version",
|
||||
message: "",
|
||||
};
|
||||
|
||||
await typeOnEditor("new content", user);
|
||||
await buildTemplateVersion(newTemplateVersion, user, topbar);
|
||||
|
||||
// Publish
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
createTemplateVersion,
|
||||
resources,
|
||||
templateByName,
|
||||
templateByNameKey,
|
||||
templateVersionByName,
|
||||
templateVersionVariables,
|
||||
} from "api/queries/templates";
|
||||
|
@ -68,6 +69,11 @@ export const TemplateVersionEditorPage: FC = () => {
|
|||
const [isPublishingDialogOpen, setIsPublishingDialogOpen] = useState(false);
|
||||
const publishVersionMutation = useMutation({
|
||||
mutationFn: publishVersion,
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries(
|
||||
templateByNameKey(orgId, templateName),
|
||||
);
|
||||
},
|
||||
});
|
||||
const [lastSuccessfulPublishedVersion, setLastSuccessfulPublishedVersion] =
|
||||
useState<TemplateVersion>();
|
||||
|
@ -179,16 +185,16 @@ export const TemplateVersionEditorPage: FC = () => {
|
|||
`/templates/${templateName}/workspace?${params.toString()}`,
|
||||
);
|
||||
}}
|
||||
disablePreview={
|
||||
templateVersionQuery.data.job.status === "running" ||
|
||||
templateVersionQuery.data.job.status === "pending" ||
|
||||
isBuilding={
|
||||
createTemplateVersionMutation.isLoading ||
|
||||
uploadFileMutation.isLoading
|
||||
uploadFileMutation.isLoading ||
|
||||
templateVersionQuery.data.job.status === "running" ||
|
||||
templateVersionQuery.data.job.status === "pending"
|
||||
}
|
||||
disableUpdate={
|
||||
templateVersionQuery.data.job.status !== "succeeded" ||
|
||||
templateVersionQuery.data.name ===
|
||||
lastSuccessfulPublishedVersion?.name
|
||||
canPublish={
|
||||
templateVersionQuery.data.job.status === "succeeded" &&
|
||||
templateQuery.data.active_version_id !==
|
||||
templateVersionQuery.data.id
|
||||
}
|
||||
resources={resourcesQuery.data}
|
||||
buildLogs={logs}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { type FC, lazy, Suspense } from "react";
|
||||
import { Suspense, lazy } from "react";
|
||||
import {
|
||||
Route,
|
||||
Routes,
|
||||
BrowserRouter as Router,
|
||||
createBrowserRouter,
|
||||
Navigate,
|
||||
createRoutesFromChildren,
|
||||
Outlet,
|
||||
} from "react-router-dom";
|
||||
import { DashboardLayout } from "./modules/dashboard/DashboardLayout";
|
||||
import { RequireAuth } from "./contexts/auth/RequireAuth";
|
||||
import { FullScreenLoader } from "./components/Loader/FullScreenLoader";
|
||||
import AuditPage from "./pages/AuditPage/AuditPage";
|
||||
import { DeploySettingsLayout } from "./pages/DeploySettingsPage/DeploySettingsLayout";
|
||||
import LoginPage from "./pages/LoginPage/LoginPage";
|
||||
|
@ -21,6 +21,7 @@ import WorkspacesPage from "./pages/WorkspacesPage/WorkspacesPage";
|
|||
import UserSettingsLayout from "./pages/UserSettingsPage/Layout";
|
||||
import { TemplateSettingsLayout } from "./pages/TemplateSettingsPage/TemplateSettingsLayout";
|
||||
import { WorkspaceSettingsLayout } from "./pages/WorkspaceSettingsPage/WorkspaceSettingsLayout";
|
||||
import { FullScreenLoader } from "components/Loader/FullScreenLoader";
|
||||
|
||||
// Lazy load pages
|
||||
// - Pages that are secondary, not in the main navigation or not usually accessed
|
||||
|
@ -240,200 +241,189 @@ const ProvisionerDaemonsHealthPage = lazy(
|
|||
() => import("./pages/HealthPage/ProvisionerDaemonsPage"),
|
||||
);
|
||||
|
||||
export const AppRouter: FC = () => {
|
||||
const RoutesWithSuspense = () => {
|
||||
return (
|
||||
<Suspense fallback={<FullScreenLoader />}>
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="login" element={<LoginPage />} />
|
||||
<Route path="setup" element={<SetupPage />} />
|
||||
|
||||
{/* Dashboard routes */}
|
||||
<Route element={<RequireAuth />}>
|
||||
<Route element={<DashboardLayout />}>
|
||||
<Route index element={<Navigate to="/workspaces" replace />} />
|
||||
|
||||
<Route
|
||||
path="/external-auth/:provider"
|
||||
element={<ExternalAuthPage />}
|
||||
/>
|
||||
|
||||
<Route path="/workspaces" element={<WorkspacesPage />} />
|
||||
|
||||
<Route path="/starter-templates">
|
||||
<Route index element={<StarterTemplatesPage />} />
|
||||
<Route path=":exampleId" element={<StarterTemplatePage />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/templates">
|
||||
<Route index element={<TemplatesPage />} />
|
||||
<Route path="new" element={<CreateTemplatePage />} />
|
||||
<Route path=":template">
|
||||
<Route element={<TemplateLayout />}>
|
||||
<Route index element={<TemplateSummaryPage />} />
|
||||
<Route path="docs" element={<TemplateDocsPage />} />
|
||||
<Route path="files" element={<TemplateFilesPage />} />
|
||||
<Route path="versions" element={<TemplateVersionsPage />} />
|
||||
<Route path="embed" element={<TemplateEmbedPage />} />
|
||||
<Route path="insights" element={<TemplateInsightsPage />} />
|
||||
</Route>
|
||||
|
||||
<Route path="workspace" element={<CreateWorkspacePage />} />
|
||||
|
||||
<Route path="settings" element={<TemplateSettingsLayout />}>
|
||||
<Route index element={<TemplateSettingsPage />} />
|
||||
<Route
|
||||
path="permissions"
|
||||
element={<TemplatePermissionsPage />}
|
||||
/>
|
||||
<Route
|
||||
path="variables"
|
||||
element={<TemplateVariablesPage />}
|
||||
/>
|
||||
<Route path="schedule" element={<TemplateSchedulePage />} />
|
||||
</Route>
|
||||
|
||||
<Route path="versions">
|
||||
<Route path=":version">
|
||||
<Route index element={<TemplateVersionPage />} />
|
||||
</Route>
|
||||
</Route>
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
<Route path="/users">
|
||||
<Route element={<UsersLayout />}>
|
||||
<Route index element={<UsersPage />} />
|
||||
</Route>
|
||||
|
||||
<Route path="create" element={<CreateUserPage />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/groups">
|
||||
<Route element={<UsersLayout />}>
|
||||
<Route index element={<GroupsPage />} />
|
||||
</Route>
|
||||
|
||||
<Route path="create" element={<CreateGroupPage />} />
|
||||
<Route path=":groupId" element={<GroupPage />} />
|
||||
<Route
|
||||
path=":groupId/settings"
|
||||
element={<SettingsGroupPage />}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
<Route path="/audit" element={<AuditPage />} />
|
||||
|
||||
<Route path="/deployment" element={<DeploySettingsLayout />}>
|
||||
<Route path="general" element={<GeneralSettingsPage />} />
|
||||
<Route path="licenses" element={<LicensesSettingsPage />} />
|
||||
<Route path="licenses/add" element={<AddNewLicensePage />} />
|
||||
<Route path="security" element={<SecuritySettingsPage />} />
|
||||
<Route
|
||||
path="observability"
|
||||
element={<ObservabilitySettingsPage />}
|
||||
/>
|
||||
<Route path="appearance" element={<AppearanceSettingsPage />} />
|
||||
<Route path="network" element={<NetworkSettingsPage />} />
|
||||
<Route path="userauth" element={<UserAuthSettingsPage />} />
|
||||
<Route
|
||||
path="external-auth"
|
||||
element={<ExternalAuthSettingsPage />}
|
||||
/>
|
||||
|
||||
<Route path="oauth2-provider">
|
||||
<Route index element={<NotFoundPage />} />
|
||||
<Route path="apps">
|
||||
<Route index element={<OAuth2AppsSettingsPage />} />
|
||||
<Route path="add" element={<CreateOAuth2AppPage />} />
|
||||
<Route path=":appId" element={<EditOAuth2AppPage />} />
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
<Route
|
||||
path="workspace-proxies"
|
||||
element={<WorkspaceProxyPage />}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
<Route path="/settings" element={<UserSettingsLayout />}>
|
||||
<Route path="account" element={<AccountPage />} />
|
||||
<Route path="appearance" element={<AppearancePage />} />
|
||||
<Route path="schedule" element={<SchedulePage />} />
|
||||
<Route path="security" element={<SecurityPage />} />
|
||||
<Route path="ssh-keys" element={<SSHKeysPage />} />
|
||||
<Route
|
||||
path="external-auth"
|
||||
element={<UserExternalAuthSettingsPage />}
|
||||
/>
|
||||
<Route
|
||||
path="oauth2-provider"
|
||||
element={<UserOAuth2ProviderSettingsPage />}
|
||||
/>
|
||||
<Route path="tokens">
|
||||
<Route index element={<TokensPage />} />
|
||||
<Route path="new" element={<CreateTokenPage />} />
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
{/* In order for the 404 page to work properly the routes that start with
|
||||
top level parameter must be fully qualified. */}
|
||||
<Route
|
||||
path="/:username/:workspace/builds/:buildNumber"
|
||||
element={<WorkspaceBuildPage />}
|
||||
/>
|
||||
<Route
|
||||
path="/:username/:workspace/settings"
|
||||
element={<WorkspaceSettingsLayout />}
|
||||
>
|
||||
<Route index element={<WorkspaceSettingsPage />} />
|
||||
<Route
|
||||
path="parameters"
|
||||
element={<WorkspaceParametersPage />}
|
||||
/>
|
||||
<Route path="schedule" element={<WorkspaceSchedulePage />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/health" element={<HealthLayout />}>
|
||||
<Route index element={<Navigate to="access-url" replace />} />
|
||||
<Route path="access-url" element={<AccessURLPage />} />
|
||||
<Route path="database" element={<DatabasePage />} />
|
||||
<Route path="derp" element={<DERPPage />} />
|
||||
<Route
|
||||
path="derp/regions/:regionId"
|
||||
element={<DERPRegionPage />}
|
||||
/>
|
||||
<Route path="websocket" element={<WebsocketPage />} />
|
||||
<Route
|
||||
path="workspace-proxy"
|
||||
element={<WorkspaceProxyHealthPage />}
|
||||
/>
|
||||
<Route
|
||||
path="provisioner-daemons"
|
||||
element={<ProvisionerDaemonsHealthPage />}
|
||||
/>
|
||||
</Route>
|
||||
{/* Using path="*"" means "match anything", so this route
|
||||
acts like a catch-all for URLs that we don't have explicit
|
||||
routes for. */}
|
||||
<Route path="*" element={<NotFoundPage />} />
|
||||
</Route>
|
||||
|
||||
{/* Pages that don't have the dashboard layout */}
|
||||
<Route path="/:username/:workspace" element={<WorkspacePage />} />
|
||||
<Route
|
||||
path="/templates/:template/versions/:version/edit"
|
||||
element={<TemplateVersionEditorPage />}
|
||||
/>
|
||||
<Route
|
||||
path="/:username/:workspace/terminal"
|
||||
element={<TerminalPage />}
|
||||
/>
|
||||
<Route path="/cli-auth" element={<CliAuthenticationPage />} />
|
||||
<Route path="/icons" element={<IconsPage />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</Router>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
export const router = createBrowserRouter(
|
||||
createRoutesFromChildren(
|
||||
<Route element={<RoutesWithSuspense />}>
|
||||
<Route path="login" element={<LoginPage />} />
|
||||
<Route path="setup" element={<SetupPage />} />
|
||||
|
||||
{/* Dashboard routes */}
|
||||
<Route element={<RequireAuth />}>
|
||||
<Route element={<DashboardLayout />}>
|
||||
<Route index element={<Navigate to="/workspaces" replace />} />
|
||||
|
||||
<Route
|
||||
path="/external-auth/:provider"
|
||||
element={<ExternalAuthPage />}
|
||||
/>
|
||||
|
||||
<Route path="/workspaces" element={<WorkspacesPage />} />
|
||||
|
||||
<Route path="/starter-templates">
|
||||
<Route index element={<StarterTemplatesPage />} />
|
||||
<Route path=":exampleId" element={<StarterTemplatePage />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/templates">
|
||||
<Route index element={<TemplatesPage />} />
|
||||
<Route path="new" element={<CreateTemplatePage />} />
|
||||
<Route path=":template">
|
||||
<Route element={<TemplateLayout />}>
|
||||
<Route index element={<TemplateSummaryPage />} />
|
||||
<Route path="docs" element={<TemplateDocsPage />} />
|
||||
<Route path="files" element={<TemplateFilesPage />} />
|
||||
<Route path="versions" element={<TemplateVersionsPage />} />
|
||||
<Route path="embed" element={<TemplateEmbedPage />} />
|
||||
<Route path="insights" element={<TemplateInsightsPage />} />
|
||||
</Route>
|
||||
|
||||
<Route path="workspace" element={<CreateWorkspacePage />} />
|
||||
|
||||
<Route path="settings" element={<TemplateSettingsLayout />}>
|
||||
<Route index element={<TemplateSettingsPage />} />
|
||||
<Route
|
||||
path="permissions"
|
||||
element={<TemplatePermissionsPage />}
|
||||
/>
|
||||
<Route path="variables" element={<TemplateVariablesPage />} />
|
||||
<Route path="schedule" element={<TemplateSchedulePage />} />
|
||||
</Route>
|
||||
|
||||
<Route path="versions">
|
||||
<Route path=":version">
|
||||
<Route index element={<TemplateVersionPage />} />
|
||||
</Route>
|
||||
</Route>
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
<Route path="/users">
|
||||
<Route element={<UsersLayout />}>
|
||||
<Route index element={<UsersPage />} />
|
||||
</Route>
|
||||
|
||||
<Route path="create" element={<CreateUserPage />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/groups">
|
||||
<Route element={<UsersLayout />}>
|
||||
<Route index element={<GroupsPage />} />
|
||||
</Route>
|
||||
|
||||
<Route path="create" element={<CreateGroupPage />} />
|
||||
<Route path=":groupId" element={<GroupPage />} />
|
||||
<Route path=":groupId/settings" element={<SettingsGroupPage />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/audit" element={<AuditPage />} />
|
||||
|
||||
<Route path="/deployment" element={<DeploySettingsLayout />}>
|
||||
<Route path="general" element={<GeneralSettingsPage />} />
|
||||
<Route path="licenses" element={<LicensesSettingsPage />} />
|
||||
<Route path="licenses/add" element={<AddNewLicensePage />} />
|
||||
<Route path="security" element={<SecuritySettingsPage />} />
|
||||
<Route
|
||||
path="observability"
|
||||
element={<ObservabilitySettingsPage />}
|
||||
/>
|
||||
<Route path="appearance" element={<AppearanceSettingsPage />} />
|
||||
<Route path="network" element={<NetworkSettingsPage />} />
|
||||
<Route path="userauth" element={<UserAuthSettingsPage />} />
|
||||
<Route
|
||||
path="external-auth"
|
||||
element={<ExternalAuthSettingsPage />}
|
||||
/>
|
||||
|
||||
<Route path="oauth2-provider">
|
||||
<Route index element={<NotFoundPage />} />
|
||||
<Route path="apps">
|
||||
<Route index element={<OAuth2AppsSettingsPage />} />
|
||||
<Route path="add" element={<CreateOAuth2AppPage />} />
|
||||
<Route path=":appId" element={<EditOAuth2AppPage />} />
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
<Route path="workspace-proxies" element={<WorkspaceProxyPage />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/settings" element={<UserSettingsLayout />}>
|
||||
<Route path="account" element={<AccountPage />} />
|
||||
<Route path="appearance" element={<AppearancePage />} />
|
||||
<Route path="schedule" element={<SchedulePage />} />
|
||||
<Route path="security" element={<SecurityPage />} />
|
||||
<Route path="ssh-keys" element={<SSHKeysPage />} />
|
||||
<Route
|
||||
path="external-auth"
|
||||
element={<UserExternalAuthSettingsPage />}
|
||||
/>
|
||||
<Route
|
||||
path="oauth2-provider"
|
||||
element={<UserOAuth2ProviderSettingsPage />}
|
||||
/>
|
||||
<Route path="tokens">
|
||||
<Route index element={<TokensPage />} />
|
||||
<Route path="new" element={<CreateTokenPage />} />
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
{/* In order for the 404 page to work properly the routes that start with
|
||||
top level parameter must be fully qualified. */}
|
||||
<Route
|
||||
path="/:username/:workspace/builds/:buildNumber"
|
||||
element={<WorkspaceBuildPage />}
|
||||
/>
|
||||
<Route
|
||||
path="/:username/:workspace/settings"
|
||||
element={<WorkspaceSettingsLayout />}
|
||||
>
|
||||
<Route index element={<WorkspaceSettingsPage />} />
|
||||
<Route path="parameters" element={<WorkspaceParametersPage />} />
|
||||
<Route path="schedule" element={<WorkspaceSchedulePage />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/health" element={<HealthLayout />}>
|
||||
<Route index element={<Navigate to="access-url" replace />} />
|
||||
<Route path="access-url" element={<AccessURLPage />} />
|
||||
<Route path="database" element={<DatabasePage />} />
|
||||
<Route path="derp" element={<DERPPage />} />
|
||||
<Route path="derp/regions/:regionId" element={<DERPRegionPage />} />
|
||||
<Route path="websocket" element={<WebsocketPage />} />
|
||||
<Route
|
||||
path="workspace-proxy"
|
||||
element={<WorkspaceProxyHealthPage />}
|
||||
/>
|
||||
<Route
|
||||
path="provisioner-daemons"
|
||||
element={<ProvisionerDaemonsHealthPage />}
|
||||
/>
|
||||
</Route>
|
||||
{/* Using path="*"" means "match anything", so this route
|
||||
acts like a catch-all for URLs that we don't have explicit
|
||||
routes for. */}
|
||||
<Route path="*" element={<NotFoundPage />} />
|
||||
</Route>
|
||||
|
||||
{/* Pages that don't have the dashboard layout */}
|
||||
<Route path="/:username/:workspace" element={<WorkspacePage />} />
|
||||
<Route
|
||||
path="/templates/:template/versions/:version/edit"
|
||||
element={<TemplateVersionEditorPage />}
|
||||
/>
|
||||
<Route
|
||||
path="/:username/:workspace/terminal"
|
||||
element={<TerminalPage />}
|
||||
/>
|
||||
<Route path="/cli-auth" element={<CliAuthenticationPage />} />
|
||||
<Route path="/icons" element={<IconsPage />} />
|
||||
</Route>
|
||||
</Route>,
|
||||
),
|
||||
);
|
Loading…
Reference in New Issue