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<

- - -
- -
-
- -
-
+ +
+ ) : ( +
+ +
-
+ + + form.setFieldValue("icon", value)} + /> +
+
+ +
-
- - -
- -
-
-
+ Cancel + + + + + )} ); 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) => ( + + ))} + + ); +};