From 910225698e914a942a366b453beecf2f4f03afc6 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Mon, 16 Sep 2024 15:16:59 -0500 Subject: [PATCH] chore: update workspaces top bar to display org name (#14596) * chore: move schedule controls to the right side of the screen * chore: add org display to workspace topbar * fix: force organizations to be readonly array * fix update type mismatch for organizations again * fix: update quota querying logic to use new endpoint * fix: add logic for handling long workspace or org names * chore: add links for workspaces by org * chore: expand tooltip styling for org * chore: expand tooltip styling for owner * refactor: split off breadcrumbs for readability * fix: display correct template version name in dropdown * fix: update overflow styling for breadcrumb segments * fix: favor org display name * fix: centralize org display name logic * fix: ensure that mock query cache key and component key are properly synced for storybook --- site/src/api/api.ts | 4 +- site/src/api/queries/workspaceQuota.ts | 16 +- .../modules/dashboard/DashboardProvider.tsx | 2 +- .../GroupsPage/GroupsPage.tsx | 7 +- .../ManagementSettingsLayout.tsx | 6 +- .../OrganizationProvisionersPage.tsx | 8 +- .../OrganizationSettingsPage.tsx | 10 +- .../TemplateRedirectController.tsx | 7 +- .../WorkspaceNotifications.tsx | 6 + .../WorkspaceScheduleControls.tsx | 11 +- .../WorkspacePage/WorkspaceTopbar.stories.tsx | 37 +- .../pages/WorkspacePage/WorkspaceTopbar.tsx | 439 ++++++++++++------ 12 files changed, 382 insertions(+), 171 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index b486dd2696..b7929107c7 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 0c44a06375..17b39463d6 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 ["workspaceQuota", organizationName, username]; +}; -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/modules/dashboard/DashboardProvider.tsx b/site/src/modules/dashboard/DashboardProvider.tsx index 958e0f199e..7744f16e5f 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 4777f289e7..ce0e3fd080 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 8692c5dcd0..4a74417e86 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/OrganizationProvisionersPage.tsx b/site/src/pages/ManagementSettingsPage/OrganizationProvisionersPage.tsx index c233826ef0..3a048db2cb 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); +}; diff --git a/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.tsx b/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.tsx index 5eb1a6e0f4..7e63bf8111 100644 --- a/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.tsx +++ b/site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.tsx @@ -55,7 +55,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 @@ -112,5 +112,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 d1052b7a9c..c4164746d1 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) => diff --git a/site/src/pages/WorkspacePage/WorkspaceNotifications/WorkspaceNotifications.tsx b/site/src/pages/WorkspacePage/WorkspaceNotifications/WorkspaceNotifications.tsx index ab6bbce85f..bcab68a9a5 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 196f742104..607ab4d86e 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 ? (