From f90f08854b8eab00d8db28a876d602a0ec330659 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Wed, 4 Sep 2024 15:58:45 +0000 Subject: [PATCH 01/15] chore: move schedule controls to the right side of the screen --- .../WorkspaceNotifications.tsx | 6 + .../WorkspaceScheduleControls.tsx | 11 +- .../pages/WorkspacePage/WorkspaceTopbar.tsx | 113 +++++++++--------- 3 files changed, 71 insertions(+), 59 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspaceNotifications/WorkspaceNotifications.tsx b/site/src/pages/WorkspacePage/WorkspaceNotifications/WorkspaceNotifications.tsx index ab6bbce85f368..bcab68a9a592f 100644 --- a/site/src/pages/WorkspacePage/WorkspaceNotifications/WorkspaceNotifications.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceNotifications/WorkspaceNotifications.tsx @@ -220,6 +220,12 @@ export const WorkspaceNotifications: FC = ({ (n) => n.severity === "warning", ); + // We have to avoid rendering out a div at all if there is no content so + // that we don't introduce additional gaps via the parent flex container + if (infoNotifications.length === 0 && warningNotifications.length === 0) { + return null; + } + return (
{infoNotifications.length > 0 && ( diff --git a/site/src/pages/WorkspacePage/WorkspaceScheduleControls.tsx b/site/src/pages/WorkspacePage/WorkspaceScheduleControls.tsx index 196f742104d21..607ab4d86e4d1 100644 --- a/site/src/pages/WorkspacePage/WorkspaceScheduleControls.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceScheduleControls.tsx @@ -30,14 +30,15 @@ import { } from "utils/schedule"; import { isWorkspaceOn } from "utils/workspace"; -export interface WorkspaceScheduleContainerProps { +interface WorkspaceScheduleContainerProps { children?: ReactNode; onClickIcon?: () => void; } -export const WorkspaceScheduleContainer: FC< - WorkspaceScheduleContainerProps -> = ({ children, onClickIcon }) => { +const WorkspaceScheduleContainer: FC = ({ + children, + onClickIcon, +}) => { const icon = ( @@ -49,6 +50,7 @@ export const WorkspaceScheduleContainer: FC< {onClickIcon ? (
-
- {!isImmutable && ( - <> - - - - - )} -
+ {!isImmutable && ( +
+ + + + +
+ )} ); }; From 4956ebb2700c6c1ad6b8a0bb5eab6a4ba9208bb0 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Wed, 4 Sep 2024 16:41:05 +0000 Subject: [PATCH 02/15] chore: add org display to workspace topbar --- .../pages/WorkspacePage/WorkspaceTopbar.tsx | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx index db8cb855b4d40..1031ca888ea7e 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx @@ -116,6 +116,12 @@ export const WorkspaceTopbar: FC = ({ linkToTemplate(workspace.organization_name, workspace.template_name), ); + // Organization logic + const { organizations, showOrganizations } = useDashboard(); + const matchedOrganization = organizations.find( + (o) => o.id === workspace.organization_id, + ); + return ( @@ -146,6 +152,24 @@ export const WorkspaceTopbar: FC = ({ {workspace.owner_name} + {showOrganizations && ( + <> + + + {matchedOrganization && ( + + )} + + + {workspace.organization_name} + + + )} + From 43ce78641f30ffae190d3f6d62a144e509164e52 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Wed, 4 Sep 2024 16:43:49 +0000 Subject: [PATCH 03/15] fix: force organizations to be readonly array --- site/src/modules/dashboard/DashboardProvider.tsx | 2 +- .../ManagementSettingsPage/GroupsPage/GroupsPage.tsx | 7 +++++-- .../ManagementSettingsLayout.tsx | 6 +++--- .../OrganizationSettingsPage.tsx | 10 +++++++--- .../pages/TemplatePage/TemplateRedirectController.tsx | 7 +++++-- 5 files changed, 21 insertions(+), 11 deletions(-) diff --git a/site/src/modules/dashboard/DashboardProvider.tsx b/site/src/modules/dashboard/DashboardProvider.tsx index 958e0f199e55f..7744f16e5fdeb 100644 --- a/site/src/modules/dashboard/DashboardProvider.tsx +++ b/site/src/modules/dashboard/DashboardProvider.tsx @@ -19,7 +19,7 @@ export interface DashboardValue { entitlements: Entitlements; experiments: Experiments; appearance: AppearanceConfig; - organizations: Organization[]; + organizations: readonly Organization[]; showOrganizations: boolean; } diff --git a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPage.tsx b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPage.tsx index 4777f289e73eb..ce0e3fd0804d3 100644 --- a/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPage.tsx +++ b/site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPage.tsx @@ -99,5 +99,8 @@ export const GroupsPage: FC = () => { export default GroupsPage; -export const getOrganizationNameByDefault = (organizations: Organization[]) => - organizations.find((org) => org.is_default)?.name; +export const getOrganizationNameByDefault = ( + organizations: readonly Organization[], +) => { + return organizations.find((org) => org.is_default)?.name; +}; diff --git a/site/src/pages/ManagementSettingsPage/ManagementSettingsLayout.tsx b/site/src/pages/ManagementSettingsPage/ManagementSettingsLayout.tsx index 8692c5dcd0408..4a74417e86a65 100644 --- a/site/src/pages/ManagementSettingsPage/ManagementSettingsLayout.tsx +++ b/site/src/pages/ManagementSettingsPage/ManagementSettingsLayout.tsx @@ -12,9 +12,9 @@ import { Outlet } from "react-router-dom"; import { DeploySettingsContext } from "../DeploySettingsPage/DeploySettingsLayout"; import { Sidebar } from "./Sidebar"; -type OrganizationSettingsValue = { - organizations: Organization[]; -}; +type OrganizationSettingsValue = Readonly<{ + organizations: readonly Organization[]; +}>; export const useOrganizationSettings = (): OrganizationSettingsValue => { const { organizations } = useDashboard(); diff --git a/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.tsx b/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.tsx index bce41745a2d7d..d3eb2a488498e 100644 --- a/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.tsx +++ b/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.tsx @@ -50,7 +50,7 @@ const OrganizationSettingsPage: FC = () => { // Redirect /organizations => /organizations/default-org, or if they cannot edit // the default org, then the first org they can edit, if any. if (!organizationName) { - const editableOrg = organizations + const editableOrg = [...organizations] .sort((a, b) => { // Prefer default org (it may not be first). // JavaScript will happily subtract booleans, but use numbers to keep @@ -107,5 +107,9 @@ const OrganizationSettingsPage: FC = () => { export default OrganizationSettingsPage; -const getOrganizationByName = (organizations: Organization[], name: string) => - organizations.find((org) => org.name === name); +const getOrganizationByName = ( + organizations: readonly Organization[], + name: string, +) => { + return organizations.find((org) => org.name === name); +}; diff --git a/site/src/pages/TemplatePage/TemplateRedirectController.tsx b/site/src/pages/TemplatePage/TemplateRedirectController.tsx index d1052b7a9c2d3..c4164746d1a6a 100644 --- a/site/src/pages/TemplatePage/TemplateRedirectController.tsx +++ b/site/src/pages/TemplatePage/TemplateRedirectController.tsx @@ -45,8 +45,11 @@ export const TemplateRedirectController: FC = () => { return ; }; -const getOrganizationNameByDefault = (organizations: Organization[]) => - organizations.find((org) => org.is_default)?.name; +const getOrganizationNameByDefault = ( + organizations: readonly Organization[], +) => { + return organizations.find((org) => org.is_default)?.name; +}; // I really hate doing it this way, but React Router does not provide a better way. const removePrefix = (self: string, prefix: string) => From b2512a7655b0596cd40c600d569e00074764f6d0 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Thu, 5 Sep 2024 17:55:24 +0000 Subject: [PATCH 04/15] fix update type mismatch for organizations again --- .../OrganizationProvisionersPage.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/site/src/pages/ManagementSettingsPage/OrganizationProvisionersPage.tsx b/site/src/pages/ManagementSettingsPage/OrganizationProvisionersPage.tsx index c233826ef07fc..3a048db2cb059 100644 --- a/site/src/pages/ManagementSettingsPage/OrganizationProvisionersPage.tsx +++ b/site/src/pages/ManagementSettingsPage/OrganizationProvisionersPage.tsx @@ -59,5 +59,9 @@ const OrganizationProvisionersPage: FC = () => { export default OrganizationProvisionersPage; -const getOrganizationByName = (organizations: Organization[], name: string) => - organizations.find((org) => org.name === name); +const getOrganizationByName = ( + organizations: readonly Organization[], + name: string, +) => { + return organizations.find((org) => org.name === name); +}; From 68290172f04d1349d7b387c504deac0c8a8b3a52 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Fri, 6 Sep 2024 19:13:28 +0000 Subject: [PATCH 05/15] fix: update quota querying logic to use new endpoint --- site/src/api/api.ts | 4 +++- site/src/api/queries/workspaceQuota.ts | 16 +++++++++------- .../pages/WorkspacePage/WorkspaceTopbar.tsx | 18 ++++++++++-------- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 6a23045ff9401..3b84a34509309 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -1684,11 +1684,13 @@ class ApiMethods { }; getWorkspaceQuota = async ( + organizationName: string, username: string, ): Promise => { const response = await this.axios.get( - `/api/v2/workspace-quota/${encodeURIComponent(username)}`, + `/api/v2/organizations/${encodeURIComponent(organizationName)}/members/${encodeURIComponent(username)}/workspace-quota`, ); + return response.data; }; diff --git a/site/src/api/queries/workspaceQuota.ts b/site/src/api/queries/workspaceQuota.ts index 0c44a06375309..e7de47d6398bc 100644 --- a/site/src/api/queries/workspaceQuota.ts +++ b/site/src/api/queries/workspaceQuota.ts @@ -1,14 +1,16 @@ import { API } from "api/api"; -export const getWorkspaceQuotaQueryKey = (username: string) => [ - username, - "workspaceQuota", -]; +export const getWorkspaceQuotaQueryKey = ( + organizationName: string, + username: string, +) => { + return [organizationName, username, "workspaceQuota"]; +}; -export const workspaceQuota = (username: string) => { +export const workspaceQuota = (organizationName: string, username: string) => { return { - queryKey: getWorkspaceQuotaQueryKey(username), - queryFn: () => API.getWorkspaceQuota(username), + queryKey: getWorkspaceQuotaQueryKey(organizationName, username), + queryFn: () => API.getWorkspaceQuota(organizationName, username), }; }; diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx index 1031ca888ea7e..61ea072f84393 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx @@ -87,18 +87,22 @@ export const WorkspaceTopbar: FC = ({ latestVersion, permissions, }) => { + const { entitlements, organizations, showOrganizations } = useDashboard(); const getLink = useLinks(); const theme = useTheme(); // Quota const hasDailyCost = workspace.latest_build.daily_cost > 0; const { data: quota } = useQuery({ - ...workspaceQuota(workspace.owner_name), + ...workspaceQuota(workspace.organization_name, workspace.owner_name), + + // Don't need to tie the enabled condition to showOrganizations because + // even if the customer hasn't enabled the orgs enterprise feature, all + // workspaces have an associated organization under the hood enabled: hasDailyCost, }); // Dormant - const { entitlements } = useDashboard(); const allowAdvancedScheduling = entitlements.features.advanced_template_scheduling.enabled; // This check can be removed when https://github.com/coder/coder/milestone/19 @@ -108,6 +112,10 @@ export const WorkspaceTopbar: FC = ({ allowAdvancedScheduling, ); + const matchedOrganization = organizations.find( + (org) => org.id === workspace.organization_id, + ); + const isImmutable = workspace.latest_build.status === "deleted" || workspace.latest_build.status === "deleting"; @@ -116,12 +124,6 @@ export const WorkspaceTopbar: FC = ({ linkToTemplate(workspace.organization_name, workspace.template_name), ); - // Organization logic - const { organizations, showOrganizations } = useDashboard(); - const matchedOrganization = organizations.find( - (o) => o.id === workspace.organization_id, - ); - return ( From dcfb84f168ac36aa1f7de2f88c74ffd7f2ff8466 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Fri, 6 Sep 2024 19:29:03 +0000 Subject: [PATCH 06/15] fix: add logic for handling long workspace or org names --- .../WorkspacePage/WorkspaceTopbar.stories.tsx | 6 +- .../pages/WorkspacePage/WorkspaceTopbar.tsx | 59 ++++++++++++++----- 2 files changed, 48 insertions(+), 17 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar.stories.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar.stories.tsx index 67dbe85c2c95c..dc9e096f7cc3d 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar.stories.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar.stories.tsx @@ -3,6 +3,7 @@ import { expect, screen, userEvent, waitFor, within } from "@storybook/test"; import { getWorkspaceQuotaQueryKey } from "api/queries/workspaceQuota"; import { addHours, addMinutes } from "date-fns"; import { + MockOrganization, MockTemplate, MockTemplateVersion, MockUser, @@ -266,7 +267,10 @@ export const WithQuota: Story = { parameters: { queries: [ { - key: getWorkspaceQuotaQueryKey(MockUser.username), + key: getWorkspaceQuotaQueryKey( + MockOrganization.name, + MockUser.username, + ), data: { credits_consumed: 2, budget: 40, diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx index 61ea072f84393..e2aac59dd4fc6 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx @@ -145,29 +145,56 @@ export const WorkspaceTopbar: FC = ({ }} > - - - {workspace.owner_name} + + + + + {workspace.owner_name} + {showOrganizations && ( <> - {matchedOrganization && ( - - )} + + + {matchedOrganization && ( + + )} - - {workspace.organization_name} + {workspace.organization_name} + )} From 836a2d4dabd938579c3f1bce121d5edb7b60a9a1 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Fri, 6 Sep 2024 20:03:29 +0000 Subject: [PATCH 07/15] chore: add links for workspaces by org --- .../pages/WorkspacePage/WorkspaceTopbar.tsx | 60 ++++++++++++------- 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx index e2aac59dd4fc6..156d9212dcafe 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx @@ -1,7 +1,7 @@ import { useTheme } from "@emotion/react"; import ArrowBackOutlined from "@mui/icons-material/ArrowBackOutlined"; import DeleteOutline from "@mui/icons-material/DeleteOutline"; -import MonetizationOnOutlined from "@mui/icons-material/MonetizationOnOutlined"; +import QuotaIcon from "@mui/icons-material/MonetizationOnOutlined"; import Link from "@mui/material/Link"; import Tooltip from "@mui/material/Tooltip"; import { workspaceQuota } from "api/queries/workspaceQuota"; @@ -112,7 +112,7 @@ export const WorkspaceTopbar: FC = ({ allowAdvancedScheduling, ); - const matchedOrganization = organizations.find( + const activeOrg = organizations.find( (org) => org.id === workspace.organization_id, ); @@ -185,11 +185,11 @@ export const WorkspaceTopbar: FC = ({ cursor: "default", }} > - {matchedOrganization && ( + {activeOrg && ( )} @@ -256,6 +256,37 @@ export const WorkspaceTopbar: FC = ({ + {quota && quota.budget > 0 && ( + + + + + + + + {workspace.latest_build.daily_cost}{" "} + + credits of + {" "} + {quota.budget} + + + + )} + {shouldDisplayDormantData && ( @@ -272,28 +303,11 @@ export const WorkspaceTopbar: FC = ({ Deletion on {new Date(workspace.deleting_at).toLocaleString()} ) : ( - "Deleting soon" + "Deletion soon" )} )} - - {quota && quota.budget > 0 && ( - - - - - - - - {workspace.latest_build.daily_cost}{" "} - - credits of - {" "} - {quota.budget} - - - )} {!isImmutable && ( From 6696c0ab6f2d8f2c933dc50c715c1e363ef082ee Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Fri, 6 Sep 2024 20:13:56 +0000 Subject: [PATCH 08/15] chore: expand tooltip styling for org --- .../pages/WorkspacePage/WorkspaceTopbar.tsx | 79 +++++++++++++------ 1 file changed, 56 insertions(+), 23 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx index 156d9212dcafe..fd953a7a53143 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx @@ -172,30 +172,63 @@ export const WorkspaceTopbar: FC = ({ <> - - - {activeOrg && ( - - )} + + + + {activeOrg && ( + + )} - {workspace.organization_name} - - + {workspace.organization_name} + + + + + + {workspace.organization_name} + + ) : ( + workspace.organization_name + ) + } + subtitle="Organization" + avatar={ + activeOrg !== undefined && ( + + ) + } + /> + + )} From de554d6133c3453a17af421a0db2c49113313b75 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Fri, 6 Sep 2024 20:18:43 +0000 Subject: [PATCH 09/15] chore: expand tooltip styling for owner --- .../pages/WorkspacePage/WorkspaceTopbar.tsx | 55 ++++++++++++------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx index fd953a7a53143..bf3941ac212dc 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx @@ -145,28 +145,41 @@ export const WorkspaceTopbar: FC = ({ }} > - - + + + + + {workspace.owner_name} + + + + - - - {workspace.owner_name} - - + + {showOrganizations && ( <> @@ -272,7 +285,7 @@ export const WorkspaceTopbar: FC = ({ to={`${templateLink}/versions/${workspace.latest_build.template_version_name}`} css={{ color: "inherit" }} > - {workspace.latest_build.template_version_name} + Version: {workspace.latest_build.template_version_name} } avatar={ From 3b410f74ee075b66544f507952f1dd89a4e0c3ee Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Fri, 6 Sep 2024 21:46:55 +0000 Subject: [PATCH 10/15] refactor: split off breadcrumbs for readability --- .../pages/WorkspacePage/WorkspaceTopbar.tsx | 356 ++++++++++-------- 1 file changed, 197 insertions(+), 159 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx index bf3941ac212dc..8b97f0f4136e4 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx @@ -1,4 +1,4 @@ -import { useTheme } from "@emotion/react"; +import { type Interpolation, type Theme, useTheme } from "@emotion/react"; import ArrowBackOutlined from "@mui/icons-material/ArrowBackOutlined"; import DeleteOutline from "@mui/icons-material/DeleteOutline"; import QuotaIcon from "@mui/icons-material/MonetizationOnOutlined"; @@ -132,174 +132,37 @@ export const WorkspaceTopbar: FC = ({ -
+
- - - - - - {workspace.owner_name} - - - - - - - + {showOrganizations && ( <> - - - - - {activeOrg && ( - - )} - - {workspace.organization_name} - - - - - - {workspace.organization_name} - - ) : ( - workspace.organization_name - ) - } - subtitle="Organization" - avatar={ - activeOrg !== undefined && ( - - ) - } - /> - - + )} - - - - - {workspace.name} - - - - - - {workspace.template_display_name.length > 0 - ? workspace.template_display_name - : workspace.template_name} - - } - subtitle={ - - Version: {workspace.latest_build.template_version_name} - - } - avatar={ - workspace.template_icon !== "" && ( - - ) - } - /> - - + {quota && quota.budget > 0 && ( @@ -406,3 +269,178 @@ export const WorkspaceTopbar: FC = ({ ); }; + +type OwnerBreadcrumbProps = Readonly<{ + ownerName: string; + ownerAvatarUrl: string; +}>; + +const OwnerBreadcrumb: FC = ({ + ownerName, + ownerAvatarUrl, +}) => { + return ( + + + + + + {ownerName} + + + + + + + + ); +}; + +type OrganizationBreadcrumbProps = Readonly<{ + orgName: string; + orgPageUrl?: string; + orgIconUrl?: string; +}>; + +const OrganizationBreadcrumb: FC = ({ + orgName, + orgPageUrl, + orgIconUrl, +}) => { + return ( + + + + + {orgName} + + + + + + {orgName} + + ) : ( + orgName + ) + } + subtitle="Organization" + avatar={ + orgIconUrl && ( + + ) + } + /> + + + ); +}; + +type WorkspaceBreadcrumbProps = Readonly<{ + workspaceName: string; + templateIconUrl: string; + rootTemplateUrl: string; + templateVersionName: string; + templateVersionDisplayName?: string; +}>; + +const WorkspaceBreadcrumb: FC = ({ + workspaceName, + templateIconUrl, + rootTemplateUrl, + templateVersionName, + templateVersionDisplayName = templateVersionName, +}) => { + return ( + + + + + {workspaceName} + + + + + + {templateVersionDisplayName} + + } + subtitle={ + + Version: {templateVersionDisplayName} + + } + avatar={ + + } + /> + + + ); +}; + +const styles = { + topbarLeft: { + display: "flex", + alignItems: "center", + columnGap: 24, + rowGap: 8, + flexWrap: "wrap", + // 12px - It is needed to keep vertical spacing when the content is wrapped + padding: "12px", + marginRight: "auto", + }, + + breadcrumbSegment: { + display: "flex", + flexFlow: "row nowrap", + gap: "8px", + maxWidth: "160px", + textOverflow: "ellipsis", + overflowX: "hidden", + whiteSpace: "nowrap", + cursor: "default", + }, +} satisfies Record>; From c8f9226cbbac1bc178e01e3ad9c67589a41c9442 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Fri, 6 Sep 2024 22:47:51 +0000 Subject: [PATCH 11/15] fix: display correct template version name in dropdown --- site/src/pages/WorkspacePage/WorkspaceTopbar.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx index 8b97f0f4136e4..991b522a0b213 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx @@ -162,6 +162,9 @@ export const WorkspaceTopbar: FC = ({ rootTemplateUrl={templateLink} templateVersionName={workspace.template_name} templateVersionDisplayName={workspace.template_display_name} + latestBuildVersionName={ + workspace.latest_build.template_version_name + } /> @@ -362,6 +365,7 @@ type WorkspaceBreadcrumbProps = Readonly<{ templateIconUrl: string; rootTemplateUrl: string; templateVersionName: string; + latestBuildVersionName: string; templateVersionDisplayName?: string; }>; @@ -370,6 +374,7 @@ const WorkspaceBreadcrumb: FC = ({ templateIconUrl, rootTemplateUrl, templateVersionName, + latestBuildVersionName, templateVersionDisplayName = templateVersionName, }) => { return ( @@ -409,7 +414,7 @@ const WorkspaceBreadcrumb: FC = ({ to={`${rootTemplateUrl}/versions/${encodeURIComponent(templateVersionName)}`} css={{ color: "inherit" }} > - Version: {templateVersionDisplayName} + Version: {latestBuildVersionName} } avatar={ From 36eba4746ae7148c4ca7e82a97c9f697f4237836 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Mon, 9 Sep 2024 16:44:21 +0000 Subject: [PATCH 12/15] fix: update overflow styling for breadcrumb segments --- .../pages/WorkspacePage/WorkspaceTopbar.tsx | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx index 991b522a0b213..6b872de31645d 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx @@ -291,8 +291,7 @@ const OwnerBreadcrumb: FC = ({ username={ownerName} avatarURL={ownerAvatarUrl} /> - - {ownerName} + {ownerName} @@ -326,7 +325,7 @@ const OrganizationBreadcrumb: FC = ({ - {orgName} + {orgName} @@ -380,17 +379,11 @@ const WorkspaceBreadcrumb: FC = ({ return ( - + - {workspaceName} + + {workspaceName} + @@ -443,9 +436,12 @@ const styles = { flexFlow: "row nowrap", gap: "8px", maxWidth: "160px", - textOverflow: "ellipsis", - overflowX: "hidden", whiteSpace: "nowrap", cursor: "default", }, + + breadcrumbText: { + overflowX: "hidden", + textOverflow: "ellipsis", + }, } satisfies Record>; From 5e0689d34a4ac74bf37d34be8bad662bf2cc63dd Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Mon, 9 Sep 2024 16:48:44 +0000 Subject: [PATCH 13/15] fix: favor org display name --- site/src/pages/WorkspacePage/WorkspaceTopbar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx index 6b872de31645d..b8d2d3f5514ff 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx @@ -143,7 +143,7 @@ export const WorkspaceTopbar: FC = ({ <> Date: Mon, 9 Sep 2024 16:56:11 +0000 Subject: [PATCH 14/15] fix: centralize org display name logic --- site/src/pages/WorkspacePage/WorkspaceTopbar.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx index b8d2d3f5514ff..264a5bf7fda04 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx @@ -116,6 +116,8 @@ export const WorkspaceTopbar: FC = ({ (org) => org.id === workspace.organization_id, ); + const orgDisplayName = activeOrg?.display_name ?? workspace.organization_name; + const isImmutable = workspace.latest_build.status === "deleted" || workspace.latest_build.status === "deleting"; @@ -143,7 +145,7 @@ export const WorkspaceTopbar: FC = ({ <> = ({ } title={ showOrganizations - ? `See affected workspaces for ${workspace.organization_name}` + ? `See affected workspaces for ${orgDisplayName}` : "See affected workspaces" } > From 775486f31f89112756c73ee7dd9c7777c3e88bb3 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Fri, 13 Sep 2024 15:53:30 +0000 Subject: [PATCH 15/15] fix: ensure that mock query cache key and component key are properly synced for storybook --- site/src/api/queries/workspaceQuota.ts | 2 +- .../WorkspacePage/WorkspaceTopbar.stories.tsx | 31 ++++++++++++++++--- .../pages/WorkspacePage/WorkspaceTopbar.tsx | 2 +- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/site/src/api/queries/workspaceQuota.ts b/site/src/api/queries/workspaceQuota.ts index e7de47d6398bc..17b39463d6247 100644 --- a/site/src/api/queries/workspaceQuota.ts +++ b/site/src/api/queries/workspaceQuota.ts @@ -4,7 +4,7 @@ export const getWorkspaceQuotaQueryKey = ( organizationName: string, username: string, ) => { - return [organizationName, username, "workspaceQuota"]; + return ["workspaceQuota", organizationName, username]; }; export const workspaceQuota = (organizationName: string, username: string) => { diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar.stories.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar.stories.tsx index dc9e096f7cc3d..ef7c72895552b 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar.stories.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar.stories.tsx @@ -1,6 +1,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import { expect, screen, userEvent, waitFor, within } from "@storybook/test"; import { getWorkspaceQuotaQueryKey } from "api/queries/workspaceQuota"; +import type { Workspace, WorkspaceQuota } from "api/typesGenerated"; import { addHours, addMinutes } from "date-fns"; import { MockOrganization, @@ -12,9 +13,12 @@ import { import { withDashboardProvider } from "testHelpers/storybook"; import { WorkspaceTopbar } from "./WorkspaceTopbar"; -// We want a workspace without a deadline to not pollute the screenshot -const baseWorkspace = { +// We want a workspace without a deadline to not pollute the screenshot. Also +// want to make sure that the workspace is synced to our other mock values +const baseWorkspace: Workspace = { ...MockWorkspace, + organization_name: MockOrganization.name, + organization_id: MockOrganization.id, latest_build: { ...MockWorkspace.latest_build, deadline: undefined, @@ -263,8 +267,9 @@ export const WithFarAwayDeadlineRequiredByTemplate: Story = { }, }; -export const WithQuota: Story = { +export const WithQuotaNoOrgs: Story = { parameters: { + showOrganizations: false, queries: [ { key: getWorkspaceQuotaQueryKey( @@ -274,7 +279,25 @@ export const WithQuota: Story = { data: { credits_consumed: 2, budget: 40, - }, + } satisfies WorkspaceQuota, + }, + ], + }, +}; + +export const WithQuotaWithOrgs: Story = { + parameters: { + showOrganizations: true, + queries: [ + { + key: getWorkspaceQuotaQueryKey( + MockOrganization.name, + MockUser.username, + ), + data: { + credits_consumed: 2, + budget: 40, + } satisfies WorkspaceQuota, }, ], }, diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx index 264a5bf7fda04..c4a8ae30c0239 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx @@ -116,7 +116,7 @@ export const WorkspaceTopbar: FC = ({ (org) => org.id === workspace.organization_id, ); - const orgDisplayName = activeOrg?.display_name ?? workspace.organization_name; + const orgDisplayName = activeOrg?.display_name || workspace.organization_name; const isImmutable = workspace.latest_build.status === "deleted" ||