mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
refactor(site): clean up workspace and template settings layouts (#25209)
This commit is contained in:
@@ -32,7 +32,7 @@ export const Default: Story = {
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="/organizations">Organizations</BreadcrumbLink>
|
||||
<BreadcrumbLink to="/organizations">Organizations</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @see {@link https://ui.shadcn.com/docs/components/breadcrumb}
|
||||
*/
|
||||
import { MoreHorizontalIcon } from "lucide-react";
|
||||
import { Slot } from "radix-ui";
|
||||
import { NavLink } from "react-router";
|
||||
import { cn } from "#/utils/cn";
|
||||
|
||||
type BreadcrumbProps = React.ComponentPropsWithRef<"nav"> & {
|
||||
@@ -44,19 +44,14 @@ export const BreadcrumbItem: React.FC<React.ComponentPropsWithRef<"li">> = ({
|
||||
);
|
||||
};
|
||||
|
||||
type BreadcrumbLinkProps = React.ComponentPropsWithRef<"a"> & {
|
||||
asChild?: boolean;
|
||||
};
|
||||
type BreadcrumbLinkProps = React.ComponentProps<typeof NavLink>;
|
||||
|
||||
export const BreadcrumbLink: React.FC<BreadcrumbLinkProps> = ({
|
||||
asChild,
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
const Comp = asChild ? Slot.Root : "a";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
<NavLink
|
||||
className={cn(
|
||||
"text-content-secondary transition-colors hover:text-content-primary no-underline hover:underline",
|
||||
className,
|
||||
|
||||
@@ -122,6 +122,7 @@ const OrganizationSettingsLayout: FC = () => {
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
<div className="h-px border-none bg-border" />
|
||||
|
||||
<Suspense fallback={<Loader />}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
|
||||
@@ -1,51 +1,25 @@
|
||||
import {
|
||||
LockIcon,
|
||||
TimerIcon as ScheduleIcon,
|
||||
SettingsIcon,
|
||||
CodeIcon as VariablesIcon,
|
||||
} from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import type { Template } from "#/api/typesGenerated";
|
||||
import { Avatar } from "#/components/Avatar/Avatar";
|
||||
import {
|
||||
Sidebar as BaseSidebar,
|
||||
SidebarHeader,
|
||||
SidebarNavItem,
|
||||
SettingsSidebarNavItem,
|
||||
} from "#/components/Sidebar/Sidebar";
|
||||
import { linkToTemplate, useLinks } from "#/modules/navigation";
|
||||
|
||||
interface SidebarProps {
|
||||
template: Template;
|
||||
}
|
||||
|
||||
export const Sidebar: FC<SidebarProps> = ({ template }) => {
|
||||
const getLink = useLinks();
|
||||
|
||||
export const Sidebar: React.FC = () => {
|
||||
return (
|
||||
<BaseSidebar>
|
||||
<SidebarHeader
|
||||
avatar={
|
||||
<Avatar variant="icon" src={template.icon} fallback={template.name} />
|
||||
}
|
||||
title={template.display_name || template.name}
|
||||
linkTo={getLink(
|
||||
linkToTemplate(template.organization_name, template.name),
|
||||
)}
|
||||
subtitle={template.name}
|
||||
/>
|
||||
|
||||
<SidebarNavItem href="" icon={SettingsIcon}>
|
||||
General
|
||||
</SidebarNavItem>
|
||||
<SidebarNavItem href="permissions" icon={LockIcon}>
|
||||
Permissions
|
||||
</SidebarNavItem>
|
||||
<SidebarNavItem href="variables" icon={VariablesIcon}>
|
||||
Variables
|
||||
</SidebarNavItem>
|
||||
<SidebarNavItem href="schedule" icon={ScheduleIcon}>
|
||||
Schedule
|
||||
</SidebarNavItem>
|
||||
<div className="flex flex-col gap-1">
|
||||
<SettingsSidebarNavItem end href="">
|
||||
General
|
||||
</SettingsSidebarNavItem>
|
||||
<SettingsSidebarNavItem href="permissions">
|
||||
Permissions
|
||||
</SettingsSidebarNavItem>
|
||||
<SettingsSidebarNavItem href="variables">
|
||||
Variables
|
||||
</SettingsSidebarNavItem>
|
||||
<SettingsSidebarNavItem href="schedule">
|
||||
Schedule
|
||||
</SettingsSidebarNavItem>
|
||||
</div>
|
||||
</BaseSidebar>
|
||||
);
|
||||
};
|
||||
|
||||
+12
-8
@@ -1,9 +1,10 @@
|
||||
import type { ComponentProps, FC } from "react";
|
||||
import type { Template, UpdateTemplateMeta } from "#/api/typesGenerated";
|
||||
import {
|
||||
PageHeader,
|
||||
PageHeaderTitle,
|
||||
} from "#/components/PageHeader/PageHeader";
|
||||
SettingsHeader,
|
||||
SettingsHeaderDescription,
|
||||
SettingsHeaderTitle,
|
||||
} from "#/components/SettingsHeader/SettingsHeader";
|
||||
import { TemplateSettingsForm } from "./TemplateSettingsForm";
|
||||
|
||||
interface TemplateSettingsPageViewProps {
|
||||
@@ -32,10 +33,13 @@ export const TemplateSettingsPageView: FC<TemplateSettingsPageViewProps> = ({
|
||||
sharedPortControlsEnabled,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<PageHeader className="pt-0">
|
||||
<PageHeaderTitle>General Settings</PageHeaderTitle>
|
||||
</PageHeader>
|
||||
<div className="flex flex-col gap-12">
|
||||
<SettingsHeader>
|
||||
<SettingsHeaderTitle>General</SettingsHeaderTitle>
|
||||
<SettingsHeaderDescription>
|
||||
Update template metadata and workspace policies.
|
||||
</SettingsHeaderDescription>
|
||||
</SettingsHeader>
|
||||
|
||||
<TemplateSettingsForm
|
||||
initialTouched={initialTouched}
|
||||
@@ -48,6 +52,6 @@ export const TemplateSettingsPageView: FC<TemplateSettingsPageViewProps> = ({
|
||||
advancedSchedulingEnabled={advancedSchedulingEnabled}
|
||||
portSharingControlsEnabled={sharedPortControlsEnabled}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+13
-9
@@ -18,10 +18,6 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "#/components/DropdownMenu/DropdownMenu";
|
||||
import { EmptyState } from "#/components/EmptyState/EmptyState";
|
||||
import {
|
||||
PageHeader,
|
||||
PageHeaderTitle,
|
||||
} from "#/components/PageHeader/PageHeader";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -29,6 +25,11 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "#/components/Select/Select";
|
||||
import {
|
||||
SettingsHeader,
|
||||
SettingsHeaderDescription,
|
||||
SettingsHeaderTitle,
|
||||
} from "#/components/SettingsHeader/SettingsHeader";
|
||||
import { Spinner } from "#/components/Spinner/Spinner";
|
||||
import {
|
||||
Table,
|
||||
@@ -225,10 +226,13 @@ export const TemplatePermissionsPageView: FC<
|
||||
onRemoveGroup,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<PageHeader className="pt-0">
|
||||
<PageHeaderTitle>Permissions</PageHeaderTitle>
|
||||
</PageHeader>
|
||||
<div className="flex flex-col gap-12">
|
||||
<SettingsHeader>
|
||||
<SettingsHeaderTitle>Permissions</SettingsHeaderTitle>
|
||||
<SettingsHeaderDescription>
|
||||
Manage which members and groups can use this template.
|
||||
</SettingsHeaderDescription>
|
||||
</SettingsHeader>
|
||||
|
||||
<div className="flex flex-col gap-2.5">
|
||||
{canUpdatePermissions && (
|
||||
@@ -265,7 +269,7 @@ export const TemplatePermissionsPageView: FC<
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+12
-8
@@ -1,9 +1,10 @@
|
||||
import type { ComponentProps, FC } from "react";
|
||||
import type { Template, UpdateTemplateMeta } from "#/api/typesGenerated";
|
||||
import {
|
||||
PageHeader,
|
||||
PageHeaderTitle,
|
||||
} from "#/components/PageHeader/PageHeader";
|
||||
SettingsHeader,
|
||||
SettingsHeaderDescription,
|
||||
SettingsHeaderTitle,
|
||||
} from "#/components/SettingsHeader/SettingsHeader";
|
||||
import { TemplateScheduleForm } from "./TemplateScheduleForm";
|
||||
|
||||
interface TemplateSchedulePageViewProps {
|
||||
@@ -28,10 +29,13 @@ export const TemplateSchedulePageView: FC<TemplateSchedulePageViewProps> = ({
|
||||
initialTouched,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<PageHeader className="pt-0">
|
||||
<PageHeaderTitle>Template schedule</PageHeaderTitle>
|
||||
</PageHeader>
|
||||
<div className="flex flex-col gap-12">
|
||||
<SettingsHeader>
|
||||
<SettingsHeaderTitle>Schedule</SettingsHeaderTitle>
|
||||
<SettingsHeaderDescription>
|
||||
Configure workspace schedule defaults for this template.
|
||||
</SettingsHeaderDescription>
|
||||
</SettingsHeader>
|
||||
|
||||
<TemplateScheduleForm
|
||||
allowAdvancedScheduling={allowAdvancedScheduling}
|
||||
@@ -42,6 +46,6 @@ export const TemplateSchedulePageView: FC<TemplateSchedulePageViewProps> = ({
|
||||
onCancel={onCancel}
|
||||
error={submitError}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,8 +5,17 @@ import { checkAuthorization } from "#/api/queries/authCheck";
|
||||
import { templateByName } from "#/api/queries/templates";
|
||||
import type { AuthorizationResponse, Template } from "#/api/typesGenerated";
|
||||
import { ErrorAlert } from "#/components/Alert/ErrorAlert";
|
||||
import { Avatar } from "#/components/Avatar/Avatar";
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "#/components/Breadcrumb/Breadcrumb";
|
||||
import { Loader } from "#/components/Loader/Loader";
|
||||
import { Margins } from "#/components/Margins/Margins";
|
||||
import { useDashboard } from "#/modules/dashboard/useDashboard";
|
||||
import { pageTitle } from "#/utils/page";
|
||||
import { Sidebar } from "./Sidebar";
|
||||
|
||||
@@ -24,11 +33,13 @@ export function useTemplateSettings() {
|
||||
}
|
||||
|
||||
export const TemplateSettingsLayout: FC = () => {
|
||||
const { showOrganizations } = useDashboard();
|
||||
const { organization: organizationName = "default", template: templateName } =
|
||||
useParams() as { organization?: string; template: string };
|
||||
const templateQuery = useQuery(
|
||||
templateByName(organizationName, templateName),
|
||||
);
|
||||
|
||||
const permissionsQuery = useQuery({
|
||||
...checkAuthorization({
|
||||
checks: {
|
||||
@@ -44,35 +55,87 @@ export const TemplateSettingsLayout: FC = () => {
|
||||
enabled: templateQuery.isSuccess,
|
||||
});
|
||||
|
||||
if (!(templateQuery.data && permissionsQuery.data)) {
|
||||
if (!templateQuery.data || !permissionsQuery.data) {
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
const error = templateQuery.isError || permissionsQuery.isError;
|
||||
const template = templateQuery.data;
|
||||
|
||||
return (
|
||||
<>
|
||||
<title>{pageTitle(templateName, "Settings")}</title>
|
||||
<title>
|
||||
{pageTitle(template?.display_name ?? templateName, "Template Settings")}
|
||||
</title>
|
||||
|
||||
<Margins className="max-sm:!px-4">
|
||||
<div className="flex flex-col gap-8 py-6 lg:flex-row lg:gap-20 lg:py-12">
|
||||
{templateQuery.isError || permissionsQuery.isError ? (
|
||||
<ErrorAlert error={templateQuery.error} />
|
||||
) : (
|
||||
<TemplateSettings.Provider
|
||||
value={{
|
||||
template: templateQuery.data,
|
||||
permissions: permissionsQuery.data,
|
||||
}}
|
||||
>
|
||||
<Sidebar template={templateQuery.data} />
|
||||
<Suspense fallback={<Loader />}>
|
||||
<div className="w-full min-w-0">
|
||||
<Outlet />
|
||||
<div>
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>Template Settings</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
{template && (
|
||||
<>
|
||||
{showOrganizations && (
|
||||
<>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage className="flex items-center gap-2">
|
||||
<Avatar
|
||||
size="sm"
|
||||
fallback={
|
||||
template.organization_display_name ||
|
||||
template.organization_name
|
||||
}
|
||||
src={template.organization_icon}
|
||||
/>
|
||||
{template.organization_display_name}
|
||||
</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</>
|
||||
)}
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink to="..">
|
||||
<BreadcrumbPage className="flex items-center gap-2">
|
||||
<Avatar
|
||||
variant="icon"
|
||||
size="sm"
|
||||
fallback={template.display_name || template.name}
|
||||
src={template.icon}
|
||||
/>
|
||||
{template.display_name || template.name}
|
||||
</BreadcrumbPage>
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
</>
|
||||
)}
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
<div className="h-px border-none bg-border" />
|
||||
|
||||
<section className="px-4 sm:px-6 lg:px-10 max-w-screen-2xl mx-auto">
|
||||
<div className="flex flex-col gap-8 py-6 lg:flex-row lg:gap-28 lg:py-10">
|
||||
{error ? (
|
||||
<ErrorAlert error={error} />
|
||||
) : (
|
||||
<TemplateSettings.Provider
|
||||
value={{
|
||||
template: templateQuery.data,
|
||||
permissions: permissionsQuery.data,
|
||||
}}
|
||||
>
|
||||
<Sidebar />
|
||||
<div className="grow min-w-0">
|
||||
<Suspense fallback={<Loader />}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</div>
|
||||
</Suspense>
|
||||
</TemplateSettings.Provider>
|
||||
)}
|
||||
</div>
|
||||
</Margins>
|
||||
</TemplateSettings.Provider>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+14
-9
@@ -7,9 +7,10 @@ import type {
|
||||
import { Alert } from "#/components/Alert/Alert";
|
||||
import { ErrorAlert } from "#/components/Alert/ErrorAlert";
|
||||
import {
|
||||
PageHeader,
|
||||
PageHeaderTitle,
|
||||
} from "#/components/PageHeader/PageHeader";
|
||||
SettingsHeader,
|
||||
SettingsHeaderDescription,
|
||||
SettingsHeaderTitle,
|
||||
} from "#/components/SettingsHeader/SettingsHeader";
|
||||
import { TemplateVariablesForm } from "./TemplateVariablesForm";
|
||||
|
||||
interface TemplateVariablesPageViewProps {
|
||||
@@ -45,12 +46,16 @@ export const TemplateVariablesPageView: FC<TemplateVariablesPageViewProps> = ({
|
||||
const hasError = Object.values(errors).some((error) => Boolean(error));
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader className="pt-0">
|
||||
<PageHeaderTitle>Template variables</PageHeaderTitle>
|
||||
</PageHeader>
|
||||
<div className="flex flex-col gap-12">
|
||||
<SettingsHeader>
|
||||
<SettingsHeaderTitle>Variables</SettingsHeaderTitle>
|
||||
<SettingsHeaderDescription>
|
||||
Update the variables used by this template.
|
||||
</SettingsHeaderDescription>
|
||||
</SettingsHeader>
|
||||
|
||||
{hasError && (
|
||||
<div className="flex flex-col gap-4 mb-16">
|
||||
<div className="flex flex-col gap-4">
|
||||
{Boolean(errors.buildError) && (
|
||||
<ErrorAlert error={errors.buildError} />
|
||||
)}
|
||||
@@ -75,6 +80,6 @@ export const TemplateVariablesPageView: FC<TemplateVariablesPageViewProps> = ({
|
||||
This template does not use managed variables.
|
||||
</Alert>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { type FC, Suspense } from "react";
|
||||
import { Outlet } from "react-router";
|
||||
import { Avatar } from "#/components/Avatar/Avatar";
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "#/components/Breadcrumb/Breadcrumb";
|
||||
import { Loader } from "#/components/Loader/Loader";
|
||||
import { useAuthenticated } from "#/hooks/useAuthenticated";
|
||||
@@ -26,12 +28,20 @@ const Layout: FC = () => {
|
||||
User Settings
|
||||
</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage className="flex items-center gap-2">
|
||||
<Avatar size="sm" fallback={me.username} src={me.avatar_url} />
|
||||
{me.username}
|
||||
</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
<div className="h-px border-none bg-border" />
|
||||
|
||||
<section className="px-4 sm:px-6 lg:px-10 max-w-screen-2xl mx-auto">
|
||||
<div className="flex flex-col gap-8 py-6 lg:flex-row lg:gap-28 lg:py-10">
|
||||
<Sidebar user={me} />
|
||||
<Sidebar />
|
||||
<div className="grow min-w-0">
|
||||
<Suspense fallback={<Loader />}>
|
||||
<Outlet />
|
||||
|
||||
@@ -1,20 +1,12 @@
|
||||
import type { FC } from "react";
|
||||
import type { User } from "#/api/typesGenerated";
|
||||
import { Avatar } from "#/components/Avatar/Avatar";
|
||||
import { FeatureStageBadge } from "#/components/FeatureStageBadge/FeatureStageBadge";
|
||||
import {
|
||||
Sidebar as BaseSidebar,
|
||||
SettingsSidebarNavItem,
|
||||
SidebarHeader,
|
||||
} from "#/components/Sidebar/Sidebar";
|
||||
import { useDashboard } from "#/modules/dashboard/useDashboard";
|
||||
import { getPrereleaseFlag } from "#/utils/buildInfo";
|
||||
|
||||
interface SidebarProps {
|
||||
user: User;
|
||||
}
|
||||
|
||||
export const Sidebar: FC<SidebarProps> = ({ user }) => {
|
||||
export const Sidebar: React.FC = () => {
|
||||
const { entitlements, experiments, buildInfo } = useDashboard();
|
||||
const showSchedulePage =
|
||||
entitlements.features.advanced_template_scheduling.enabled;
|
||||
@@ -23,11 +15,6 @@ export const Sidebar: FC<SidebarProps> = ({ user }) => {
|
||||
|
||||
return (
|
||||
<BaseSidebar>
|
||||
<SidebarHeader
|
||||
avatar={<Avatar fallback={user.username} src={user.avatar_url} />}
|
||||
title={user.username}
|
||||
subtitle={user.email}
|
||||
/>
|
||||
<div className="flex flex-col gap-1">
|
||||
<SettingsSidebarNavItem href="account">Account</SettingsSidebarNavItem>
|
||||
<SettingsSidebarNavItem href="appearance">
|
||||
|
||||
@@ -1,49 +1,30 @@
|
||||
import {
|
||||
SettingsIcon as GeneralIcon,
|
||||
CodeIcon as ParameterIcon,
|
||||
TimerIcon as ScheduleIcon,
|
||||
UsersIcon as SharingIcon,
|
||||
} from "lucide-react";
|
||||
import { Avatar } from "#/components/Avatar/Avatar";
|
||||
import {
|
||||
Sidebar as BaseSidebar,
|
||||
SidebarHeader,
|
||||
SidebarNavItem,
|
||||
SettingsSidebarNavItem,
|
||||
} from "#/components/Sidebar/Sidebar";
|
||||
import { useWorkspaceSettings } from "./useWorkspaceSettings";
|
||||
|
||||
export const Sidebar: React.FC = () => {
|
||||
const { owner, workspace, permissions } = useWorkspaceSettings();
|
||||
const { permissions } = useWorkspaceSettings();
|
||||
|
||||
return (
|
||||
<BaseSidebar>
|
||||
<SidebarHeader
|
||||
avatar={
|
||||
<Avatar
|
||||
variant="icon"
|
||||
src={workspace.template_icon}
|
||||
fallback={workspace.name}
|
||||
/>
|
||||
}
|
||||
title={workspace.name}
|
||||
linkTo={`/@${owner}/${workspace.name}`}
|
||||
subtitle={workspace.template_display_name ?? workspace.template_name}
|
||||
/>
|
||||
|
||||
<SidebarNavItem href="" icon={GeneralIcon}>
|
||||
General
|
||||
</SidebarNavItem>
|
||||
<SidebarNavItem href="parameters" icon={ParameterIcon}>
|
||||
Parameters
|
||||
</SidebarNavItem>
|
||||
<SidebarNavItem href="schedule" icon={ScheduleIcon}>
|
||||
Schedule
|
||||
</SidebarNavItem>
|
||||
{permissions?.shareWorkspace && (
|
||||
<SidebarNavItem href="sharing" icon={SharingIcon}>
|
||||
Sharing
|
||||
</SidebarNavItem>
|
||||
)}
|
||||
<div className="flex flex-col gap-1">
|
||||
<SettingsSidebarNavItem end href="">
|
||||
General
|
||||
</SettingsSidebarNavItem>
|
||||
<SettingsSidebarNavItem href="parameters">
|
||||
Parameters
|
||||
</SettingsSidebarNavItem>
|
||||
<SettingsSidebarNavItem href="schedule">
|
||||
Schedule
|
||||
</SettingsSidebarNavItem>
|
||||
{permissions?.shareWorkspace && (
|
||||
<SettingsSidebarNavItem href="sharing">
|
||||
Sharing
|
||||
</SettingsSidebarNavItem>
|
||||
)}
|
||||
</div>
|
||||
</BaseSidebar>
|
||||
);
|
||||
};
|
||||
|
||||
+17
-8
@@ -15,6 +15,11 @@ import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog"
|
||||
import { EmptyState } from "#/components/EmptyState/EmptyState";
|
||||
import { Link } from "#/components/Link/Link";
|
||||
import { Loader } from "#/components/Loader/Loader";
|
||||
import {
|
||||
SettingsHeader,
|
||||
SettingsHeaderDescription,
|
||||
SettingsHeaderTitle,
|
||||
} from "#/components/SettingsHeader/SettingsHeader";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -263,13 +268,12 @@ const WorkspaceParametersPage: FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 max-w-screen-md">
|
||||
<div className="flex flex-col gap-12">
|
||||
<title>{pageTitle(workspace.name, "Parameters")}</title>
|
||||
|
||||
<header className="flex flex-col items-start gap-2">
|
||||
<span className="flex flex-row items-center gap-2 justify-between w-full">
|
||||
<span className="flex flex-row items-center gap-2">
|
||||
<h1 className="text-3xl m-0">Workspace parameters</h1>
|
||||
<SettingsHeader>
|
||||
<SettingsHeaderTitle
|
||||
tooltip={
|
||||
<TooltipProvider delayDuration={100}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
@@ -290,9 +294,14 @@ const WorkspaceParametersPage: FC = () => {
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</span>
|
||||
</span>
|
||||
</header>
|
||||
}
|
||||
>
|
||||
Parameters
|
||||
</SettingsHeaderTitle>
|
||||
<SettingsHeaderDescription>
|
||||
Update the build parameters for this workspace.
|
||||
</SettingsHeaderDescription>
|
||||
</SettingsHeader>
|
||||
|
||||
{Boolean(error) && <ErrorAlert error={error} />}
|
||||
|
||||
|
||||
+12
-8
@@ -14,9 +14,10 @@ import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog"
|
||||
import { Link } from "#/components/Link/Link";
|
||||
import { Loader } from "#/components/Loader/Loader";
|
||||
import {
|
||||
PageHeader,
|
||||
PageHeaderTitle,
|
||||
} from "#/components/PageHeader/PageHeader";
|
||||
SettingsHeader,
|
||||
SettingsHeaderDescription,
|
||||
SettingsHeaderTitle,
|
||||
} from "#/components/SettingsHeader/SettingsHeader";
|
||||
import {
|
||||
scheduleChanged,
|
||||
scheduleToAutostart,
|
||||
@@ -71,12 +72,15 @@ const WorkspaceSchedulePage: FC = () => {
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-12">
|
||||
<title>{pageTitle(workspaceName, "Schedule")}</title>
|
||||
|
||||
<PageHeader className="pt-0">
|
||||
<PageHeaderTitle>Workspace Schedule</PageHeaderTitle>
|
||||
</PageHeader>
|
||||
<SettingsHeader>
|
||||
<SettingsHeaderTitle>Schedule</SettingsHeaderTitle>
|
||||
<SettingsHeaderDescription>
|
||||
Configure when this workspace starts and stops automatically.
|
||||
</SettingsHeaderDescription>
|
||||
</SettingsHeader>
|
||||
|
||||
{error && <ErrorAlert error={error} />}
|
||||
|
||||
@@ -162,7 +166,7 @@ const WorkspaceSchedulePage: FC = () => {
|
||||
navigate(`/@${username}/${workspaceName}`);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -6,8 +6,16 @@ import {
|
||||
workspacePermissions,
|
||||
} from "#/api/queries/workspaces";
|
||||
import { ErrorAlert } from "#/components/Alert/ErrorAlert";
|
||||
import { Avatar } from "#/components/Avatar/Avatar";
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "#/components/Breadcrumb/Breadcrumb";
|
||||
import { Loader } from "#/components/Loader/Loader";
|
||||
import { Margins } from "#/components/Margins/Margins";
|
||||
import { pageTitle } from "#/utils/page";
|
||||
import { Sidebar } from "./Sidebar";
|
||||
import { WorkspaceSettings } from "./useWorkspaceSettings";
|
||||
@@ -30,35 +38,79 @@ export const WorkspaceSettingsLayout: FC = () => {
|
||||
}
|
||||
|
||||
const error = workspaceQuery.error || permissionsQuery.error;
|
||||
const workspace = workspaceQuery.data;
|
||||
|
||||
return (
|
||||
<>
|
||||
<title>{pageTitle(workspaceName, "Settings")}</title>
|
||||
<title>{pageTitle(workspaceName, "Workspace Settings")}</title>
|
||||
|
||||
<Margins className="max-sm:!px-4">
|
||||
<div className="flex flex-col gap-8 py-6 lg:flex-row lg:gap-20 lg:py-12">
|
||||
{error ? (
|
||||
<ErrorAlert error={error} />
|
||||
) : (
|
||||
workspaceQuery.data && (
|
||||
<WorkspaceSettings.Provider
|
||||
value={{
|
||||
owner: username,
|
||||
workspace: workspaceQuery.data,
|
||||
permissions: permissionsQuery.data,
|
||||
}}
|
||||
>
|
||||
<Sidebar />
|
||||
<Suspense fallback={<Loader />}>
|
||||
<div className="w-full min-w-0">
|
||||
<Outlet />
|
||||
<div>
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>Workspace Settings</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
{workspace && (
|
||||
<>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage className="flex items-center gap-2">
|
||||
<Avatar
|
||||
size="sm"
|
||||
fallback={workspace.owner_name}
|
||||
src={workspace.owner_avatar_url}
|
||||
/>
|
||||
{workspace.owner_name}
|
||||
</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink to="..">
|
||||
<BreadcrumbPage className="flex items-center gap-2">
|
||||
<Avatar
|
||||
variant="icon"
|
||||
size="sm"
|
||||
fallback={
|
||||
workspace.template_display_name ||
|
||||
workspace.template_name
|
||||
}
|
||||
src={workspace.template_icon}
|
||||
/>
|
||||
{workspace.name}
|
||||
</BreadcrumbPage>
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
</>
|
||||
)}
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
<div className="h-px border-none bg-border" />
|
||||
|
||||
<section className="px-4 sm:px-6 lg:px-10 max-w-screen-2xl mx-auto">
|
||||
<div className="flex flex-col gap-8 py-6 lg:flex-row lg:gap-28 lg:py-10">
|
||||
{error ? (
|
||||
<ErrorAlert error={error} />
|
||||
) : (
|
||||
workspaceQuery.data && (
|
||||
<WorkspaceSettings.Provider
|
||||
value={{
|
||||
owner: username,
|
||||
workspace: workspaceQuery.data,
|
||||
permissions: permissionsQuery.data,
|
||||
}}
|
||||
>
|
||||
<Sidebar />
|
||||
<div className="grow min-w-0">
|
||||
<Suspense fallback={<Loader />}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</div>
|
||||
</Suspense>
|
||||
</WorkspaceSettings.Provider>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</Margins>
|
||||
</WorkspaceSettings.Provider>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { ComponentProps, FC } from "react";
|
||||
import type { Workspace } from "#/api/typesGenerated";
|
||||
import {
|
||||
PageHeader,
|
||||
PageHeaderTitle,
|
||||
} from "#/components/PageHeader/PageHeader";
|
||||
SettingsHeader,
|
||||
SettingsHeaderDescription,
|
||||
SettingsHeaderTitle,
|
||||
} from "#/components/SettingsHeader/SettingsHeader";
|
||||
import { WorkspaceSettingsForm } from "./WorkspaceSettingsForm";
|
||||
|
||||
type WorkspaceSettingsPageViewProps = {
|
||||
@@ -20,10 +21,13 @@ export const WorkspaceSettingsPageView: FC<WorkspaceSettingsPageViewProps> = ({
|
||||
workspace,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<PageHeader className="pt-0">
|
||||
<PageHeaderTitle>Workspace Settings</PageHeaderTitle>
|
||||
</PageHeader>
|
||||
<div className="flex flex-col gap-12">
|
||||
<SettingsHeader>
|
||||
<SettingsHeaderTitle>General</SettingsHeaderTitle>
|
||||
<SettingsHeaderDescription>
|
||||
Update the name and automatic update behavior for this workspace.
|
||||
</SettingsHeaderDescription>
|
||||
</SettingsHeader>
|
||||
|
||||
<WorkspaceSettingsForm
|
||||
error={error}
|
||||
@@ -31,6 +35,6 @@ export const WorkspaceSettingsPageView: FC<WorkspaceSettingsPageViewProps> = ({
|
||||
onCancel={onCancel}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import type { FC } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import { checkAuthorization } from "#/api/queries/authCheck";
|
||||
import { Link } from "#/components/Link/Link";
|
||||
import type { WorkspacePermissions } from "#/modules/workspaces/permissions";
|
||||
import { workspaceChecks } from "#/modules/workspaces/permissions";
|
||||
import { useWorkspaceSharing } from "#/modules/workspaces/WorkspaceSharingForm/useWorkspaceSharing";
|
||||
import { docs } from "#/utils/docs";
|
||||
import { pageTitle } from "#/utils/page";
|
||||
import { useWorkspaceSettings } from "../useWorkspaceSettings";
|
||||
import { WorkspaceSharingPageView } from "./WorkspaceSharingPageView";
|
||||
@@ -25,20 +23,9 @@ const WorkspaceSharingPage: FC = () => {
|
||||
sharing.error ?? permissionsQuery.error ?? sharing.mutationError;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-12">
|
||||
<>
|
||||
<title>{pageTitle(workspace.name, "Sharing")}</title>
|
||||
|
||||
<header className="flex flex-col">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="text-3xl m-0">Workspace sharing</h1>
|
||||
<p className="flex flex-row gap-1 text-sm text-content-secondary font-medium m-0">
|
||||
Workspace sharing allows you to share workspaces with other users
|
||||
and groups.{" "}
|
||||
<Link href={docs("/user-guides/shared-workspaces")}>View docs</Link>
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<WorkspaceSharingPageView
|
||||
workspace={workspace}
|
||||
workspaceACL={sharing.workspaceACL}
|
||||
@@ -56,7 +43,7 @@ const WorkspaceSharingPage: FC = () => {
|
||||
onRemoveGroup={sharing.removeGroup}
|
||||
hasRemovedMember={sharing.hasRemovedMember}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+47
-25
@@ -7,9 +7,16 @@ import type {
|
||||
WorkspaceRole,
|
||||
WorkspaceUser,
|
||||
} from "#/api/typesGenerated";
|
||||
import {
|
||||
SettingsHeader,
|
||||
SettingsHeaderDescription,
|
||||
SettingsHeaderDocsLink,
|
||||
SettingsHeaderTitle,
|
||||
} from "#/components/SettingsHeader/SettingsHeader";
|
||||
import { isGroup } from "#/modules/groups";
|
||||
import { AddWorkspaceUserOrGroup } from "#/modules/workspaces/WorkspaceSharingForm/AddWorkspaceUserOrGroup";
|
||||
import { WorkspaceSharingForm } from "#/modules/workspaces/WorkspaceSharingForm/WorkspaceSharingForm";
|
||||
import { docs } from "#/utils/docs";
|
||||
|
||||
interface WorkspaceSharingPageViewProps {
|
||||
workspace: Workspace;
|
||||
@@ -51,30 +58,45 @@ export const WorkspaceSharingPageView: FC<WorkspaceSharingPageViewProps> = ({
|
||||
hasRemovedMember,
|
||||
}) => {
|
||||
return (
|
||||
<WorkspaceSharingForm
|
||||
organizationId={workspace.organization_id}
|
||||
workspaceACL={workspaceACL}
|
||||
canUpdatePermissions={canUpdatePermissions}
|
||||
error={error}
|
||||
updatingUserId={updatingUserId}
|
||||
onUpdateUser={onUpdateUser}
|
||||
onRemoveUser={onRemoveUser}
|
||||
updatingGroupId={updatingGroupId}
|
||||
onUpdateGroup={onUpdateGroup}
|
||||
onRemoveGroup={onRemoveGroup}
|
||||
showRestartWarning={hasRemovedMember}
|
||||
addMemberForm={
|
||||
<AddWorkspaceUserOrGroup
|
||||
organizationID={workspace.organization_id}
|
||||
workspaceACL={workspaceACL}
|
||||
isLoading={isAddingUser || isAddingGroup}
|
||||
onSubmit={(value, role, resetAutocomplete) =>
|
||||
isGroup(value)
|
||||
? onAddGroup(value, role, resetAutocomplete)
|
||||
: onAddUser(value, role, resetAutocomplete)
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<div className="flex flex-col gap-12">
|
||||
<SettingsHeader
|
||||
actions={
|
||||
<SettingsHeaderDocsLink
|
||||
href={docs("/user-guides/shared-workspaces")}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<SettingsHeaderTitle>Sharing</SettingsHeaderTitle>
|
||||
<SettingsHeaderDescription>
|
||||
Share this workspace with other users and groups.
|
||||
</SettingsHeaderDescription>
|
||||
</SettingsHeader>
|
||||
|
||||
<WorkspaceSharingForm
|
||||
organizationId={workspace.organization_id}
|
||||
workspaceACL={workspaceACL}
|
||||
canUpdatePermissions={canUpdatePermissions}
|
||||
error={error}
|
||||
updatingUserId={updatingUserId}
|
||||
onUpdateUser={onUpdateUser}
|
||||
onRemoveUser={onRemoveUser}
|
||||
updatingGroupId={updatingGroupId}
|
||||
onUpdateGroup={onUpdateGroup}
|
||||
onRemoveGroup={onRemoveGroup}
|
||||
showRestartWarning={hasRemovedMember}
|
||||
addMemberForm={
|
||||
<AddWorkspaceUserOrGroup
|
||||
organizationID={workspace.organization_id}
|
||||
workspaceACL={workspaceACL}
|
||||
isLoading={isAddingUser || isAddingGroup}
|
||||
onSubmit={(value, role, resetAutocomplete) =>
|
||||
isGroup(value)
|
||||
? onAddGroup(value, role, resetAutocomplete)
|
||||
: onAddUser(value, role, resetAutocomplete)
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user