From 4e1dccaabda4ca3a34ca14b1a26f4dcaad71d0f2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Kayla=20=E3=81=AF=E3=81=AA?=
Date: Wed, 6 May 2026 12:08:51 -0600
Subject: [PATCH] refactor: remove `ChooseOne` component (#24983)
---
.../Conditionals/ChooseOne.stories.tsx | 71 -----
.../src/components/Conditionals/ChooseOne.tsx | 53 ----
site/src/modules/resources/AgentStatus.tsx | 103 +++---
site/src/pages/AuditPage/AuditPageView.tsx | 141 +++++----
.../ConnectionLogPageView.tsx | 128 ++++----
.../IdpOrgSyncPage/IdpOrgSyncPage.tsx | 66 ++--
.../IdpOrgSyncPage/IdpOrgSyncPageView.tsx | 39 +--
site/src/pages/GroupsPage/GroupsPageView.tsx | 125 ++++----
.../CreateOrganizationPageView.tsx | 116 ++++---
.../CustomRolesPage/CustomRolesPageView.tsx | 115 ++++---
.../IdpSyncPage/IdpMappingTable.tsx | 44 ++-
.../IdpSyncPage/IdpSyncPage.tsx | 105 +++----
.../TemplatePermissionsPageView.tsx | 296 ++++++++++--------
.../TokensPage/TokensPageView.tsx | 146 +++++----
.../WorkspaceProxyPage/WorkspaceProxyView.tsx | 56 ++--
15 files changed, 774 insertions(+), 830 deletions(-)
delete mode 100644 site/src/components/Conditionals/ChooseOne.stories.tsx
delete mode 100644 site/src/components/Conditionals/ChooseOne.tsx
diff --git a/site/src/components/Conditionals/ChooseOne.stories.tsx b/site/src/components/Conditionals/ChooseOne.stories.tsx
deleted file mode 100644
index 8d228a3178..0000000000
--- a/site/src/components/Conditionals/ChooseOne.stories.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import type { Meta, StoryObj } from "@storybook/react-vite";
-import { ChooseOne, Cond } from "./ChooseOne";
-
-const meta: Meta = {
- title: "components/Conditionals/ChooseOne",
- component: ChooseOne,
-};
-
-export default meta;
-type Story = StoryObj;
-
-export const FirstIsTrue: Story = {
- args: {
- children: [
-
- The first one shows.
- ,
-
- The second one does not show.
- ,
- The default does not show.,
- ],
- },
-};
-
-export const SecondIsTrue: Story = {
- args: {
- children: [
-
- The first one does not show.
- ,
-
- The second one shows.
- ,
- The default does not show.,
- ],
- },
-};
-export const AllAreTrue: Story = {
- args: {
- children: [
-
- Only the first one shows.
- ,
-
- The second one does not show.
- ,
- The default does not show.,
- ],
- },
-};
-
-export const NoneAreTrue: Story = {
- args: {
- children: [
-
- The first one does not show.
- ,
-
- The second one does not show.
- ,
- The default shows.,
- ],
- },
-};
-
-export const OneCond: Story = {
- args: {
- children: An only child renders.,
- },
-};
diff --git a/site/src/components/Conditionals/ChooseOne.tsx b/site/src/components/Conditionals/ChooseOne.tsx
deleted file mode 100644
index 8897fd4bc4..0000000000
--- a/site/src/components/Conditionals/ChooseOne.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import {
- Children,
- type FC,
- type JSX,
- type PropsWithChildren,
- type ReactNode,
-} from "react";
-
-interface CondProps {
- condition?: boolean;
- children?: ReactNode;
-}
-
-/**
- * Wrapper component that attaches a condition to a child component so that ChooseOne can
- * determine which child to render. The last Cond in a ChooseOne is the fallback case and
- * should not have a condition.
- * @param condition boolean expression indicating whether the child should be rendered, or undefined
- * @returns child. Note that Cond alone does not enforce the condition; it should be used inside ChooseOne.
- * @deprecated Use standard conditional rendering (ternary operators or && expressions) instead.
- */
-export const Cond: FC = ({ children }) => {
- return <>{children}>;
-};
-
-/**
- * Wrapper component for rendering exactly one of its children. Wrap each child in Cond to associate it
- * with a condition under which it should be rendered. If no conditions are met, the final child
- * will be rendered.
- * @returns one of its children, or null if there are no children
- * @throws an error if its last child has a condition prop, or any non-final children do not have a condition prop
- * @deprecated Use standard conditional rendering (ternary operators or && expressions) instead.
- */
-export const ChooseOne: FC = ({ children }) => {
- const childArray = Children.toArray(children) as JSX.Element[];
- if (childArray.length === 0) {
- return null;
- }
- const conditionedOptions = childArray.slice(0, childArray.length - 1);
- const defaultCase = childArray[childArray.length - 1];
- if (defaultCase.props.condition !== undefined) {
- throw new Error(
- "The last Cond in a ChooseOne was given a condition prop, but it is the default case.",
- );
- }
- if (conditionedOptions.some((cond) => cond.props.condition === undefined)) {
- throw new Error(
- "A non-final Cond in a ChooseOne does not have a condition prop or the prop is undefined.",
- );
- }
- const chosen = conditionedOptions.find((child) => child.props.condition);
- return chosen ?? defaultCase;
-};
diff --git a/site/src/modules/resources/AgentStatus.tsx b/site/src/modules/resources/AgentStatus.tsx
index a3a812f412..65c9eb007d 100644
--- a/site/src/modules/resources/AgentStatus.tsx
+++ b/site/src/modules/resources/AgentStatus.tsx
@@ -6,7 +6,6 @@ import type {
WorkspaceAgent,
WorkspaceAgentDevcontainer,
} from "#/api/typesGenerated";
-import { ChooseOne, Cond } from "#/components/Conditionals/ChooseOne";
import {
HelpPopover,
HelpPopoverContent,
@@ -195,34 +194,28 @@ const ConnectedStatus: FC = ({ agent }) => {
if (agent.scripts.length === 0) {
return ;
}
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
+ if (agent.lifecycle_state === "ready") {
+ return ;
+ }
+ if (agent.lifecycle_state === "start_timeout") {
+ return ;
+ }
+ if (agent.lifecycle_state === "start_error") {
+ return ;
+ }
+ if (agent.lifecycle_state === "shutting_down") {
+ return ;
+ }
+ if (agent.lifecycle_state === "shutdown_timeout") {
+ return ;
+ }
+ if (agent.lifecycle_state === "shutdown_error") {
+ return ;
+ }
+ if (agent.lifecycle_state === "off") {
+ return ;
+ }
+ return ;
};
const DisconnectedStatus: FC = () => {
@@ -265,44 +258,32 @@ const TimeoutStatus: FC = ({ agent }) => (
);
export const AgentStatus: FC = ({ agent }) => {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
+ if (agent.status === "connected") {
+ return ;
+ }
+ if (agent.status === "disconnected") {
+ return ;
+ }
+ if (agent.status === "timeout") {
+ return ;
+ }
+ return ;
};
const SubAgentStatus: FC = ({ agent }) => {
if (!agent) {
return ;
}
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
+ if (agent.status === "connected") {
+ return ;
+ }
+ if (agent.status === "disconnected") {
+ return ;
+ }
+ if (agent.status === "timeout") {
+ return ;
+ }
+ return ;
};
const DevcontainerStartError: FC = ({ agent }) => (
diff --git a/site/src/pages/AuditPage/AuditPageView.tsx b/site/src/pages/AuditPage/AuditPageView.tsx
index c9d5a8730c..f6753b9aa8 100644
--- a/site/src/pages/AuditPage/AuditPageView.tsx
+++ b/site/src/pages/AuditPage/AuditPageView.tsx
@@ -1,6 +1,5 @@
import type { ComponentProps, FC } from "react";
import type { AuditLog } from "#/api/typesGenerated";
-import { ChooseOne, Cond } from "#/components/Conditionals/ChooseOne";
import { EmptyState } from "#/components/EmptyState/EmptyState";
import { Margins } from "#/components/Margins/Margins";
import {
@@ -63,8 +62,8 @@ export const AuditPageView: FC = ({
View events in your audit log.
-
-
+ {isAuditLogVisible ? (
+ <>
= ({
>
-
- {/* Error condition should just show an empty table. */}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {auditLogs && (
- new Date(log.time)}
- row={(log) => (
-
- )}
- />
- )}
-
-
+
-
-
-
-
-
-
+ >
+ ) : (
+
+ )}
);
};
+
+interface AuditTableBodyProps {
+ auditLogs: readonly AuditLog[] | undefined;
+ error: unknown;
+ isLoading: boolean;
+ isEmpty: boolean;
+ isNonInitialPage: boolean;
+ showOrgDetails: boolean;
+}
+
+const AuditTableBody: FC = ({
+ auditLogs,
+ error,
+ isLoading,
+ isEmpty,
+ isNonInitialPage,
+ showOrgDetails,
+}) => {
+ // An error renders as an empty table.
+ if (error) {
+ return (
+
+
+
+
+
+ );
+ }
+ if (isLoading) {
+ return ;
+ }
+ if (isEmpty) {
+ const emptyMessage = isNonInitialPage
+ ? "No audit logs available on this page"
+ : "No audit logs available";
+ return (
+
+
+
+
+
+ );
+ }
+ if (!auditLogs) {
+ return null;
+ }
+ return (
+ new Date(log.time)}
+ row={(log) => (
+
+ )}
+ />
+ );
+};
diff --git a/site/src/pages/ConnectionLogPage/ConnectionLogPageView.tsx b/site/src/pages/ConnectionLogPage/ConnectionLogPageView.tsx
index 2d5fd4ab5c..c5d2a920d4 100644
--- a/site/src/pages/ConnectionLogPage/ConnectionLogPageView.tsx
+++ b/site/src/pages/ConnectionLogPage/ConnectionLogPageView.tsx
@@ -1,6 +1,5 @@
import type { ComponentProps, FC } from "react";
import type { ConnectionLog } from "#/api/typesGenerated";
-import { ChooseOne, Cond } from "#/components/Conditionals/ChooseOne";
import { EmptyState } from "#/components/EmptyState/EmptyState";
import { Margins } from "#/components/Margins/Margins";
import {
@@ -64,8 +63,8 @@ export const ConnectionLogPageView: FC = ({
-
-
+ {isConnectionLogVisible ? (
+ <>
= ({
>
-
- {/* Error condition should just show an empty table. */}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {connectionLogs && (
- new Date(log.connect_time)}
- row={(log) => (
-
- )}
- />
- )}
-
-
+
-
-
-
-
-
-
+ >
+ ) : (
+
+ )}
);
};
+
+interface ConnectionLogTableBodyProps {
+ connectionLogs: readonly ConnectionLog[] | undefined;
+ error: unknown;
+ isLoading: boolean;
+ isEmpty: boolean;
+ isNonInitialPage: boolean;
+}
+
+const ConnectionLogTableBody: FC = ({
+ connectionLogs,
+ error,
+ isLoading,
+ isEmpty,
+ isNonInitialPage,
+}) => {
+ // An error renders as an empty table.
+ if (error) {
+ return (
+
+
+
+
+
+ );
+ }
+ if (isLoading) {
+ return ;
+ }
+ if (isEmpty) {
+ const emptyMessage = isNonInitialPage
+ ? "No connection logs available on this page"
+ : "No connection logs available";
+ return (
+
+
+
+
+
+ );
+ }
+ if (!connectionLogs) {
+ return null;
+ }
+ return (
+ new Date(log.connect_time)}
+ row={(log) => }
+ />
+ );
+};
diff --git a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPage.tsx b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPage.tsx
index fbb492f2b5..3b0fa5f2e7 100644
--- a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPage.tsx
+++ b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPage.tsx
@@ -7,7 +7,6 @@ import {
organizationIdpSyncSettings,
patchOrganizationSyncSettings,
} from "#/api/queries/idpsync";
-import { ChooseOne, Cond } from "#/components/Conditionals/ChooseOne";
import { Link } from "#/components/Link/Link";
import { Loader } from "#/components/Loader/Loader";
import { PaywallPremium } from "#/components/Paywall/PaywallPremium";
@@ -76,40 +75,37 @@ const IdpOrgSyncPage: FC = () => {
-
-
-
-
-
- {
- try {
- await patchOrganizationSyncSettingsMutation.mutateAsync(data);
- toast.success("Organization sync settings updated.");
- } catch (error) {
- toast.error(
- getErrorMessage(
- error,
- "Failed to update organization IdP sync settings.",
- ),
- {
- description: getErrorDetail(error),
- },
- );
- }
- }}
- error={settingsQuery.error || fieldValuesQuery.error}
- />
-
-
+ {!isIdpSyncEnabled ? (
+
+ ) : (
+ {
+ try {
+ await patchOrganizationSyncSettingsMutation.mutateAsync(data);
+ toast.success("Organization sync settings updated.");
+ } catch (error) {
+ toast.error(
+ getErrorMessage(
+ error,
+ "Failed to update organization IdP sync settings.",
+ ),
+ {
+ description: getErrorDetail(error),
+ },
+ );
+ }
+ }}
+ error={settingsQuery.error || fieldValuesQuery.error}
+ />
+ )}
>
);
diff --git a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx
index d136642dc1..74fce05ebb 100644
--- a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx
+++ b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx
@@ -17,7 +17,6 @@ import {
ComboboxList,
ComboboxTrigger,
} from "#/components/Combobox/Combobox";
-import { ChooseOne, Cond } from "#/components/Conditionals/ChooseOne";
import {
Dialog,
DialogContent,
@@ -408,27 +407,23 @@ const IdpMappingTable: FC = ({ isEmpty, children }) => {
-
-
-
-
-
- How to set up IdP organization sync
-
- }
- />
-
-
-
-
- {children}
-
+ {isEmpty ? (
+
+
+
+ How to set up IdP organization sync
+
+ }
+ />
+
+
+ ) : (
+ children
+ )}
);
diff --git a/site/src/pages/GroupsPage/GroupsPageView.tsx b/site/src/pages/GroupsPage/GroupsPageView.tsx
index f2ce1b63dd..f855fd19a3 100644
--- a/site/src/pages/GroupsPage/GroupsPageView.tsx
+++ b/site/src/pages/GroupsPage/GroupsPageView.tsx
@@ -7,7 +7,6 @@ import { AvatarData } from "#/components/Avatar/AvatarData";
import { AvatarDataSkeleton } from "#/components/Avatar/AvatarDataSkeleton";
import { Badge } from "#/components/Badge/Badge";
import { Button } from "#/components/Button/Button";
-import { ChooseOne, Cond } from "#/components/Conditionals/ChooseOne";
import { EmptyState } from "#/components/EmptyState/EmptyState";
import { PaywallPremium } from "#/components/Paywall/PaywallPremium";
import { Skeleton } from "#/components/Skeleton/Skeleton";
@@ -37,68 +36,76 @@ export const GroupsPageView: FC = ({
canCreateGroup,
groupsEnabled,
}) => {
- const isLoading = Boolean(groups === undefined);
- const isEmpty = Boolean(groups && groups.length === 0);
+ if (!groupsEnabled) {
+ return (
+
+ );
+ }
return (
-
-
-
-
-
-
-
-
- Name
- Users
-
-
-
-
-
-
-
-
+
+
+
+ Name
+ Users
+
+
+
+
+
+
+
+ );
+};
-
-
-
-
-
-
- Create group
-
-
- )
- }
- />
-
-
-
+interface GroupsTableBodyProps {
+ groups: Group[] | undefined;
+ canCreateGroup: boolean;
+}
-
- {groups?.map((group) => (
-
- ))}
-
-
-
-
-
-
+const GroupsTableBody: FC = ({
+ groups,
+ canCreateGroup,
+}) => {
+ if (groups === undefined) {
+ return ;
+ }
+ if (groups.length === 0) {
+ return (
+
+
+
+
+
+ Create group
+
+
+ )
+ }
+ />
+
+
+ );
+ }
+ return (
+ <>
+ {groups.map((group) => (
+
+ ))}
+ >
);
};
diff --git a/site/src/pages/OrganizationSettingsPage/CreateOrganizationPageView.tsx b/site/src/pages/OrganizationSettingsPage/CreateOrganizationPageView.tsx
index eaaed69fe5..4b1bfedf21 100644
--- a/site/src/pages/OrganizationSettingsPage/CreateOrganizationPageView.tsx
+++ b/site/src/pages/OrganizationSettingsPage/CreateOrganizationPageView.tsx
@@ -9,7 +9,6 @@ import type { CreateOrganizationRequest } from "#/api/typesGenerated";
import { ErrorAlert } from "#/components/Alert/ErrorAlert";
import { Badges, PremiumBadge } from "#/components/Badges/Badges";
import { Button } from "#/components/Button/Button";
-import { ChooseOne, Cond } from "#/components/Conditionals/ChooseOne";
import { IconField } from "#/components/IconField/IconField";
import { PaywallPremium } from "#/components/Paywall/PaywallPremium";
import { PopoverPaywall } from "#/components/Paywall/PopoverPaywall";
@@ -112,67 +111,64 @@ export const CreateOrganizationPageView: FC<
-
-
-
-
-
-
+ ) : (
+
+
+
+ )}
);
diff --git a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.tsx b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.tsx
index 787cfdea9d..db6556027e 100644
--- a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.tsx
+++ b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.tsx
@@ -3,7 +3,6 @@ import type { FC } from "react";
import { Link as RouterLink, useNavigate } from "react-router";
import type { AssignableRoles, Role } from "#/api/typesGenerated";
import { Button, Button as ShadcnButton } from "#/components/Button/Button";
-import { ChooseOne, Cond } from "#/components/Conditionals/ChooseOne";
import {
DropdownMenu,
DropdownMenuContent,
@@ -117,8 +116,6 @@ const RoleTable: FC = ({
canDeleteOrgRole,
onDeleteRole,
}) => {
- const isLoading = roles === undefined;
- const isEmpty = Boolean(roles && roles.length === 0);
return (
@@ -129,58 +126,76 @@ const RoleTable: FC = ({
-
-
-
-
-
-
-
-
-
-
-
- Create custom role
-
-
- )
- }
- />
-
-
-
-
-
- {[...(roles ?? [])]
- .sort((a, b) => a.name.localeCompare(b.name))
- .map((role) => (
- onDeleteRole(role)}
- />
- ))}
-
-
+
);
};
+const RoleTableBody: FC = ({
+ roles,
+ isCustomRolesEnabled,
+ canCreateOrgRole,
+ canUpdateOrgRole,
+ canDeleteOrgRole,
+ onDeleteRole,
+}) => {
+ if (roles === undefined) {
+ return ;
+ }
+ if (roles.length === 0) {
+ return (
+
+
+
+
+
+ Create custom role
+
+
+ )
+ }
+ />
+
+
+ );
+ }
+ return (
+ <>
+ {[...roles]
+ .sort((a, b) => a.name.localeCompare(b.name))
+ .map((role) => (
+ onDeleteRole(role)}
+ />
+ ))}
+ >
+ );
+};
+
interface RoleRowProps {
role: AssignableRoles;
canUpdateOrgRole: boolean;
diff --git a/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpMappingTable.tsx b/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpMappingTable.tsx
index 4b63c07073..3d92b6e133 100644
--- a/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpMappingTable.tsx
+++ b/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpMappingTable.tsx
@@ -1,5 +1,4 @@
import type { FC } from "react";
-import { ChooseOne, Cond } from "#/components/Conditionals/ChooseOne";
import { EmptyState } from "#/components/EmptyState/EmptyState";
import { Link } from "#/components/Link/Link";
import {
@@ -37,28 +36,27 @@ export const IdpMappingTable: FC = ({
-
-
-
-
-
- How to setup IdP {type.toLocaleLowerCase()} sync
-
- }
- />
-
-
-
- {children}
-
+ {rowCount === 0 ? (
+
+
+
+ How to setup IdP {type.toLocaleLowerCase()} sync
+
+ }
+ />
+
+
+ ) : (
+ children
+ )}
diff --git a/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpSyncPage.tsx b/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpSyncPage.tsx
index 91d1a066c3..37ab7b1f3a 100644
--- a/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpSyncPage.tsx
+++ b/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpSyncPage.tsx
@@ -12,7 +12,6 @@ import {
roleIdpSyncSettings,
} from "#/api/queries/organizations";
import { organizationRoles } from "#/api/queries/roles";
-import { ChooseOne, Cond } from "#/components/Conditionals/ChooseOne";
import { EmptyState } from "#/components/EmptyState/EmptyState";
import { Link } from "#/components/Link/Link";
import { PaywallPremium } from "#/components/Paywall/PaywallPremium";
@@ -128,61 +127,57 @@ const IdpSyncPage: FC = () => {
-
-
-
-
-
- {
- const mutation =
- patchGroupSyncSettingsMutation.mutateAsync(data);
- toast.promise(mutation, {
- loading: "Updating IdP group sync settings...",
- success: "IdP group sync settings updated.",
- error: (error) => ({
- message: getErrorMessage(
- error,
- "Failed to update IdP group sync settings.",
- ),
+ {!isIdpSyncEnabled ? (
+
+ ) : (
+ {
+ const mutation = patchGroupSyncSettingsMutation.mutateAsync(data);
+ toast.promise(mutation, {
+ loading: "Updating IdP group sync settings...",
+ success: "IdP group sync settings updated.",
+ error: (error) => ({
+ message: getErrorMessage(
+ error,
+ "Failed to update IdP group sync settings.",
+ ),
+ description: getErrorDetail(error),
+ }),
+ });
+ }}
+ onSubmitRoleSyncSettings={async (data) => {
+ try {
+ await patchRoleSyncSettingsMutation.mutateAsync(data);
+ toast.success("IdP Role sync settings updated.");
+ } catch (error) {
+ toast.error(
+ getErrorMessage(
+ error,
+ "Failed to update IdP role sync settings.",
+ ),
+ {
description: getErrorDetail(error),
- }),
- });
- }}
- onSubmitRoleSyncSettings={async (data) => {
- try {
- await patchRoleSyncSettingsMutation.mutateAsync(data);
- toast.success("IdP Role sync settings updated.");
- } catch (error) {
- toast.error(
- getErrorMessage(
- error,
- "Failed to update IdP role sync settings.",
- ),
- {
- description: getErrorDetail(error),
- },
- );
- }
- }}
- />
-
-
+ },
+ );
+ }
+ }}
+ />
+ )}
);
diff --git a/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPageView.tsx b/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPageView.tsx
index bf5def8e5d..44ef8328ec 100644
--- a/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPageView.tsx
+++ b/site/src/pages/TemplateSettingsPage/TemplatePermissionsPage/TemplatePermissionsPageView.tsx
@@ -11,7 +11,6 @@ import type {
import { Avatar } from "#/components/Avatar/Avatar";
import { AvatarData } from "#/components/Avatar/AvatarData";
import { Button } from "#/components/Button/Button";
-import { ChooseOne, Cond } from "#/components/Conditionals/ChooseOne";
import {
DropdownMenu,
DropdownMenuContent,
@@ -225,12 +224,6 @@ export const TemplatePermissionsPageView: FC<
onUpdateGroup,
onRemoveGroup,
}) => {
- const isEmpty = Boolean(
- templateACL &&
- templateACL.users.length === 0 &&
- templateACL.group.length === 0,
- );
-
return (
<>
@@ -259,137 +252,170 @@ export const TemplatePermissionsPageView: FC<
-
-
-
-
-
-
-
-
-
-
-
-
- {templateACL?.group.map((group) => (
-
-
-
- }
- title={group.display_name || group.name}
- subtitle={getGroupSubtitle(group)}
- />
-
-
-
-
- {
- onUpdateGroup(group, role);
- }}
- />
-
-
- {group.role}
-
-
-
-
-
- {canUpdatePermissions && (
-
-
-
-
-
- onRemoveGroup(group)}
- >
- Remove
-
-
-
- )}
-
-
- ))}
-
- {templateACL?.users.map((user) => (
-
-
-
-
-
-
-
- {
- onUpdateUser(user, role);
- }}
- />
-
-
- {user.role}
-
-
-
-
-
- {canUpdatePermissions && (
-
-
-
-
-
- onRemoveUser(user)}
- >
- Remove
-
-
-
- )}
-
-
- ))}
-
-
+
>
);
};
+
+interface MembersTableBodyProps {
+ templateACL: TemplateACL | undefined;
+ canUpdatePermissions: boolean;
+ updatingUserId: TemplateUser["id"] | undefined;
+ updatingGroupId: TemplateGroup["id"] | undefined;
+ onUpdateUser: (user: TemplateUser, role: TemplateRole) => void;
+ onRemoveUser: (user: TemplateUser) => void;
+ onUpdateGroup: (group: TemplateGroup, role: TemplateRole) => void;
+ onRemoveGroup: (group: Group) => void;
+}
+
+const MembersTableBody: FC = ({
+ templateACL,
+ canUpdatePermissions,
+ updatingUserId,
+ updatingGroupId,
+ onUpdateUser,
+ onRemoveUser,
+ onUpdateGroup,
+ onRemoveGroup,
+}) => {
+ if (!templateACL) {
+ return ;
+ }
+
+ const isEmpty =
+ templateACL.users.length === 0 && templateACL.group.length === 0;
+ if (isEmpty) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ return (
+ <>
+ {templateACL.group.map((group) => (
+
+
+
+ }
+ title={group.display_name || group.name}
+ subtitle={getGroupSubtitle(group)}
+ />
+
+
+ {canUpdatePermissions ? (
+ {
+ onUpdateGroup(group, role);
+ }}
+ />
+ ) : (
+ {group.role}
+ )}
+
+
+
+ {canUpdatePermissions && (
+
+
+
+
+
+ onRemoveGroup(group)}
+ >
+ Remove
+
+
+
+ )}
+
+
+ ))}
+
+ {templateACL.users.map((user) => (
+
+
+
+
+
+ {canUpdatePermissions ? (
+ {
+ onUpdateUser(user, role);
+ }}
+ />
+ ) : (
+ {user.role}
+ )}
+
+
+
+ {canUpdatePermissions && (
+
+
+
+
+
+ onRemoveUser(user)}
+ >
+ Remove
+
+
+
+ )}
+
+
+ ))}
+ >
+ );
+};
diff --git a/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx b/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx
index 8a0311c2fe..e9bfd92dc7 100644
--- a/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx
+++ b/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx
@@ -6,7 +6,6 @@ import type { FC, ReactNode } from "react";
import type { APIKeyWithOwner } from "#/api/typesGenerated";
import { ErrorAlert } from "#/components/Alert/ErrorAlert";
import { Button } from "#/components/Button/Button";
-import { ChooseOne, Cond } from "#/components/Conditionals/ChooseOne";
import {
Table,
TableBody,
@@ -44,8 +43,6 @@ export const TokensPageView: FC = ({
onDelete,
deleteTokenError,
}) => {
- const theme = useTheme();
-
return (
{Boolean(getTokensError) &&
}
@@ -63,71 +60,88 @@ export const TokensPageView: FC
= ({
-
-
-
-
-
-
-
-
- {tokens?.map((token) => {
- return (
-
-
-
- {token.id}
-
-
-
-
-
- {token.token_name}
-
-
-
- {lastUsedOrNever(token.last_used)}
-
-
-
- {dayjs(token.expires_at).fromNow()}
-
-
-
-
-
- {dayjs(token.created_at).fromNow()}
-
-
-
-
-
-
-
-
-
- );
- })}
-
-
+
);
};
+
+interface TokensTableBodyProps {
+ tokens?: APIKeyWithOwner[];
+ isLoading: boolean;
+ hasLoaded: boolean;
+ onDelete: (token: APIKeyWithOwner) => void;
+}
+
+const TokensTableBody: FC = ({
+ tokens,
+ isLoading,
+ hasLoaded,
+ onDelete,
+}) => {
+ const theme = useTheme();
+
+ if (isLoading) {
+ return ;
+ }
+ if (hasLoaded && (!tokens || tokens.length === 0)) {
+ return ;
+ }
+ return (
+ <>
+ {tokens?.map((token) => (
+
+
+
+ {token.id}
+
+
+
+
+
+ {token.token_name}
+
+
+
+ {lastUsedOrNever(token.last_used)}
+
+
+
+ {dayjs(token.expires_at).fromNow()}
+
+
+
+
+
+ {dayjs(token.created_at).fromNow()}
+
+
+
+
+
+
+
+
+
+ ))}
+ >
+ );
+};
diff --git a/site/src/pages/UserSettingsPage/WorkspaceProxyPage/WorkspaceProxyView.tsx b/site/src/pages/UserSettingsPage/WorkspaceProxyPage/WorkspaceProxyView.tsx
index 09418bae98..4719bf0250 100644
--- a/site/src/pages/UserSettingsPage/WorkspaceProxyPage/WorkspaceProxyView.tsx
+++ b/site/src/pages/UserSettingsPage/WorkspaceProxyPage/WorkspaceProxyView.tsx
@@ -1,7 +1,6 @@
import type { FC } from "react";
import type { Region } from "#/api/typesGenerated";
import { ErrorAlert } from "#/components/Alert/ErrorAlert";
-import { ChooseOne, Cond } from "#/components/Conditionals/ChooseOne";
import {
SettingsHeader,
SettingsHeaderDescription,
@@ -61,25 +60,46 @@ export const WorkspaceProxyView: FC = ({
-
-
-
-
-
-
-
-
- {proxies?.map((proxy) => (
-
- ))}
-
-
+
);
};
+
+interface ProxiesTableBodyProps {
+ proxies?: readonly Region[];
+ proxyLatencies?: Record;
+ isLoading: boolean;
+ hasLoaded: boolean;
+}
+
+const ProxiesTableBody: FC = ({
+ proxies,
+ proxyLatencies,
+ isLoading,
+ hasLoaded,
+}) => {
+ if (isLoading) {
+ return ;
+ }
+ if (hasLoaded && proxies?.length === 0) {
+ return ;
+ }
+ return (
+ <>
+ {proxies?.map((proxy) => (
+
+ ))}
+ >
+ );
+};