From f90f08854b8eab00d8db28a876d602a0ec330659 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Wed, 4 Sep 2024 15:58:45 +0000 Subject: [PATCH 01/27] 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/27] 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/27] 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/27] 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 32118056be80cf9069444a1075d407a16980342e Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Thu, 5 Sep 2024 18:19:19 +0000 Subject: [PATCH 05/27] refactor: tuck main loading skeleton for filter into base definition --- site/src/components/Filter/filter.tsx | 9 ++++++--- site/src/pages/AuditPage/AuditFilter.tsx | 8 ++++---- site/src/pages/TemplatesPage/TemplatesFilter.tsx | 7 +------ site/src/pages/UsersPage/UsersFilter.tsx | 7 +------ site/src/pages/WorkspacesPage/filter/filter.tsx | 10 ++-------- 5 files changed, 14 insertions(+), 27 deletions(-) diff --git a/site/src/components/Filter/filter.tsx b/site/src/components/Filter/filter.tsx index 4ace1daab0bb8..bf1992bfb9d97 100644 --- a/site/src/components/Filter/filter.tsx +++ b/site/src/components/Filter/filter.tsx @@ -135,7 +135,7 @@ export const MenuSkeleton: FC = () => { type FilterProps = { filter: ReturnType; - skeleton: ReactNode; + optionsSkeleton: ReactNode; isLoading: boolean; learnMoreLink?: string; learnMoreLabel2?: string; @@ -150,7 +150,7 @@ export const Filter: FC = ({ filter, isLoading, error, - skeleton, + optionsSkeleton, options, learnMoreLink, learnMoreLabel2, @@ -195,7 +195,10 @@ export const Filter: FC = ({ }} > {isLoading ? ( - skeleton + <> + + {optionsSkeleton} + ) : ( <> diff --git a/site/src/pages/AuditPage/AuditFilter.tsx b/site/src/pages/AuditPage/AuditFilter.tsx index 9b7bf0bb6ba39..3be1c94713225 100644 --- a/site/src/pages/AuditPage/AuditFilter.tsx +++ b/site/src/pages/AuditPage/AuditFilter.tsx @@ -51,8 +51,8 @@ interface AuditFilterProps { } export const AuditFilter: FC = ({ filter, error, menus }) => { - // Use a smaller width if including the organization filter. - const width = menus.organization && 175; + const width = menus.organization ? 175 : undefined; + return ( = ({ filter, error, menus }) => { )} } - skeleton={ + optionsSkeleton={ <> - + diff --git a/site/src/pages/TemplatesPage/TemplatesFilter.tsx b/site/src/pages/TemplatesPage/TemplatesFilter.tsx index f463a35ff5261..d8093101beb36 100644 --- a/site/src/pages/TemplatesPage/TemplatesFilter.tsx +++ b/site/src/pages/TemplatesPage/TemplatesFilter.tsx @@ -64,12 +64,7 @@ export const TemplatesFilter: FC = ({ /> } - skeleton={ - <> - - - - } + optionsSkeleton={} /> ); }; diff --git a/site/src/pages/UsersPage/UsersFilter.tsx b/site/src/pages/UsersPage/UsersFilter.tsx index 9cdabe27c8ff1..13e7ffd5f58df 100644 --- a/site/src/pages/UsersPage/UsersFilter.tsx +++ b/site/src/pages/UsersPage/UsersFilter.tsx @@ -78,12 +78,7 @@ export const UsersFilter: FC = ({ filter, error, menus }) => { filter={filter} error={error} options={} - skeleton={ - <> - - - - } + optionsSkeleton={} /> ); }; diff --git a/site/src/pages/WorkspacesPage/filter/filter.tsx b/site/src/pages/WorkspacesPage/filter/filter.tsx index 249deec05d292..5e7aee7ab39ef 100644 --- a/site/src/pages/WorkspacesPage/filter/filter.tsx +++ b/site/src/pages/WorkspacesPage/filter/filter.tsx @@ -1,10 +1,5 @@ import { type UserFilterMenu, UserMenu } from "components/Filter/UserFilter"; -import { - Filter, - MenuSkeleton, - SearchFieldSkeleton, - type useFilter, -} from "components/Filter/filter"; +import { type useFilter, Filter, MenuSkeleton } from "components/Filter/filter"; import { useDashboard } from "modules/dashboard/useDashboard"; import type { FC } from "react"; import { docs } from "utils/docs"; @@ -97,9 +92,8 @@ export const WorkspacesFilter: FC = ({ } - skeleton={ + optionsSkeleton={ <> - {menus.user && } From cdcc907e7cd679691669ae1b9b13fe6a67bcf8e9 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Thu, 5 Sep 2024 18:21:29 +0000 Subject: [PATCH 06/27] refactor: give filter files different names to reduce confusion --- site/src/api/queries/audits.ts | 2 +- site/src/components/Filter/{filter.tsx => Filter.tsx} | 0 site/src/components/Filter/storyHelpers.ts | 2 +- site/src/pages/AuditPage/AuditFilter.tsx | 2 +- site/src/pages/AuditPage/AuditPage.tsx | 2 +- site/src/pages/TemplatesPage/TemplatesFilter.tsx | 2 +- site/src/pages/TemplatesPage/TemplatesPage.tsx | 2 +- site/src/pages/TemplatesPage/TemplatesPageView.tsx | 2 +- site/src/pages/UsersPage/UsersFilter.tsx | 2 +- site/src/pages/UsersPage/UsersPage.tsx | 2 +- site/src/pages/WorkspacesPage/WorkspacesPage.tsx | 2 +- site/src/pages/WorkspacesPage/WorkspacesPageView.tsx | 2 +- .../WorkspacesPage/filter/{filter.tsx => WorkspacesFilter.tsx} | 2 +- 13 files changed, 12 insertions(+), 12 deletions(-) rename site/src/components/Filter/{filter.tsx => Filter.tsx} (100%) rename site/src/pages/WorkspacesPage/filter/{filter.tsx => WorkspacesFilter.tsx} (99%) diff --git a/site/src/api/queries/audits.ts b/site/src/api/queries/audits.ts index 224f8b0d12815..9be370271c74d 100644 --- a/site/src/api/queries/audits.ts +++ b/site/src/api/queries/audits.ts @@ -1,6 +1,6 @@ import { API } from "api/api"; import type { AuditLogResponse } from "api/typesGenerated"; -import { useFilterParamsKey } from "components/Filter/filter"; +import { useFilterParamsKey } from "components/Filter/Filter"; import type { UsePaginatedQueryOptions } from "hooks/usePaginatedQuery"; export function paginatedAudits( diff --git a/site/src/components/Filter/filter.tsx b/site/src/components/Filter/Filter.tsx similarity index 100% rename from site/src/components/Filter/filter.tsx rename to site/src/components/Filter/Filter.tsx diff --git a/site/src/components/Filter/storyHelpers.ts b/site/src/components/Filter/storyHelpers.ts index fc820fc27caf0..92285b41e48ee 100644 --- a/site/src/components/Filter/storyHelpers.ts +++ b/site/src/components/Filter/storyHelpers.ts @@ -1,5 +1,5 @@ import { action } from "@storybook/addon-actions"; -import type { UseFilterResult } from "./filter"; +import type { UseFilterResult } from "./Filter"; import type { UseFilterMenuResult } from "./menu"; export const MockMenu: UseFilterMenuResult = { diff --git a/site/src/pages/AuditPage/AuditFilter.tsx b/site/src/pages/AuditPage/AuditFilter.tsx index 3be1c94713225..2c58f7dbce68e 100644 --- a/site/src/pages/AuditPage/AuditFilter.tsx +++ b/site/src/pages/AuditPage/AuditFilter.tsx @@ -11,7 +11,7 @@ import { MenuSkeleton, SearchFieldSkeleton, type useFilter, -} from "components/Filter/filter"; +} from "components/Filter/Filter"; import { type UseFilterMenuOptions, useFilterMenu, diff --git a/site/src/pages/AuditPage/AuditPage.tsx b/site/src/pages/AuditPage/AuditPage.tsx index b950c0ecd1716..7ff21983b891b 100644 --- a/site/src/pages/AuditPage/AuditPage.tsx +++ b/site/src/pages/AuditPage/AuditPage.tsx @@ -1,6 +1,6 @@ import { paginatedAudits } from "api/queries/audits"; import { useUserFilterMenu } from "components/Filter/UserFilter"; -import { useFilter } from "components/Filter/filter"; +import { useFilter } from "components/Filter/Filter"; import { isNonInitialPage } from "components/PaginationWidget/utils"; import { usePaginatedQuery } from "hooks/usePaginatedQuery"; import { useDashboard } from "modules/dashboard/useDashboard"; diff --git a/site/src/pages/TemplatesPage/TemplatesFilter.tsx b/site/src/pages/TemplatesPage/TemplatesFilter.tsx index d8093101beb36..45dc37d9a3da7 100644 --- a/site/src/pages/TemplatesPage/TemplatesFilter.tsx +++ b/site/src/pages/TemplatesPage/TemplatesFilter.tsx @@ -9,7 +9,7 @@ import { MenuSkeleton, SearchFieldSkeleton, type useFilter, -} from "components/Filter/filter"; +} from "components/Filter/Filter"; import { useFilterMenu } from "components/Filter/menu"; import { UserAvatar } from "components/UserAvatar/UserAvatar"; import type { FC } from "react"; diff --git a/site/src/pages/TemplatesPage/TemplatesPage.tsx b/site/src/pages/TemplatesPage/TemplatesPage.tsx index 95691e8b7b189..de09956d44d1d 100644 --- a/site/src/pages/TemplatesPage/TemplatesPage.tsx +++ b/site/src/pages/TemplatesPage/TemplatesPage.tsx @@ -1,5 +1,5 @@ import { templateExamples, templates } from "api/queries/templates"; -import { useFilter } from "components/Filter/filter"; +import { useFilter } from "components/Filter/Filter"; import { useAuthenticated } from "contexts/auth/RequireAuth"; import { useDashboard } from "modules/dashboard/useDashboard"; import type { FC } from "react"; diff --git a/site/src/pages/TemplatesPage/TemplatesPageView.tsx b/site/src/pages/TemplatesPage/TemplatesPageView.tsx index 591c3971b7e7c..a68fb2f10bec9 100644 --- a/site/src/pages/TemplatesPage/TemplatesPageView.tsx +++ b/site/src/pages/TemplatesPage/TemplatesPageView.tsx @@ -16,7 +16,7 @@ import { ExternalAvatar } from "components/Avatar/Avatar"; import { AvatarData } from "components/AvatarData/AvatarData"; import { AvatarDataSkeleton } from "components/AvatarData/AvatarDataSkeleton"; import { DeprecatedBadge } from "components/Badges/Badges"; -import type { useFilter } from "components/Filter/filter"; +import type { useFilter } from "components/Filter/Filter"; import { HelpTooltip, HelpTooltipContent, diff --git a/site/src/pages/UsersPage/UsersFilter.tsx b/site/src/pages/UsersPage/UsersFilter.tsx index 13e7ffd5f58df..76eb47bc10d3e 100644 --- a/site/src/pages/UsersPage/UsersFilter.tsx +++ b/site/src/pages/UsersPage/UsersFilter.tsx @@ -7,7 +7,7 @@ import { MenuSkeleton, SearchFieldSkeleton, type useFilter, -} from "components/Filter/filter"; +} from "components/Filter/Filter"; import { type UseFilterMenuOptions, useFilterMenu, diff --git a/site/src/pages/UsersPage/UsersPage.tsx b/site/src/pages/UsersPage/UsersPage.tsx index 363bc72627794..7556d78ea1b8a 100644 --- a/site/src/pages/UsersPage/UsersPage.tsx +++ b/site/src/pages/UsersPage/UsersPage.tsx @@ -14,7 +14,7 @@ import { import type { User } from "api/typesGenerated"; import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog"; -import { useFilter } from "components/Filter/filter"; +import { useFilter } from "components/Filter/Filter"; import { displayError, displaySuccess } from "components/GlobalSnackbar/utils"; import { isNonInitialPage } from "components/PaginationWidget/utils"; import { useAuthenticated } from "contexts/auth/RequireAuth"; diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx index cb9db0a1d8127..097cd2e66998a 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx @@ -1,7 +1,7 @@ import { templates } from "api/queries/templates"; import type { Workspace } from "api/typesGenerated"; import { useUserFilterMenu } from "components/Filter/UserFilter"; -import { useFilter } from "components/Filter/filter"; +import { useFilter } from "components/Filter/Filter"; import { useAuthenticated } from "contexts/auth/RequireAuth"; import { useEffectEvent } from "hooks/hookPolyfills"; import { usePagination } from "hooks/usePagination"; diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx index 4e4501854b185..6c58048ba7b9e 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx @@ -28,7 +28,7 @@ import type { UseQueryResult } from "react-query"; import { mustUpdateWorkspace } from "utils/workspace"; import { WorkspaceHelpTooltip } from "./WorkspaceHelpTooltip"; import { WorkspacesButton } from "./WorkspacesButton"; -import { WorkspacesFilter } from "./filter/filter"; +import { WorkspacesFilter } from "./filter/WorkspacesFilter"; export const Language = { pageTitle: "Workspaces", diff --git a/site/src/pages/WorkspacesPage/filter/filter.tsx b/site/src/pages/WorkspacesPage/filter/WorkspacesFilter.tsx similarity index 99% rename from site/src/pages/WorkspacesPage/filter/filter.tsx rename to site/src/pages/WorkspacesPage/filter/WorkspacesFilter.tsx index 5e7aee7ab39ef..bd990d0c07696 100644 --- a/site/src/pages/WorkspacesPage/filter/filter.tsx +++ b/site/src/pages/WorkspacesPage/filter/WorkspacesFilter.tsx @@ -1,5 +1,5 @@ import { type UserFilterMenu, UserMenu } from "components/Filter/UserFilter"; -import { type useFilter, Filter, MenuSkeleton } from "components/Filter/filter"; +import { type useFilter, Filter, MenuSkeleton } from "components/Filter/Filter"; import { useDashboard } from "modules/dashboard/useDashboard"; import type { FC } from "react"; import { docs } from "utils/docs"; From 99a3792edc4202c3e66aef81e5688d89f0e80e72 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Thu, 5 Sep 2024 18:25:14 +0000 Subject: [PATCH 07/27] refactor: remove separate base filter skeleton --- site/src/components/Filter/Filter.tsx | 6 +----- site/src/pages/AuditPage/AuditFilter.tsx | 7 +------ site/src/pages/TemplatesPage/TemplatesFilter.tsx | 7 +------ site/src/pages/UsersPage/UsersFilter.tsx | 7 +------ 4 files changed, 4 insertions(+), 23 deletions(-) diff --git a/site/src/components/Filter/Filter.tsx b/site/src/components/Filter/Filter.tsx index bf1992bfb9d97..314239c2ff5bd 100644 --- a/site/src/components/Filter/Filter.tsx +++ b/site/src/components/Filter/Filter.tsx @@ -125,10 +125,6 @@ const BaseSkeleton: FC = ({ children, ...skeletonProps }) => { ); }; -export const SearchFieldSkeleton: FC = () => { - return ; -}; - export const MenuSkeleton: FC = () => { return ; }; @@ -196,7 +192,7 @@ export const Filter: FC = ({ > {isLoading ? ( <> - + {optionsSkeleton} ) : ( diff --git a/site/src/pages/AuditPage/AuditFilter.tsx b/site/src/pages/AuditPage/AuditFilter.tsx index 2c58f7dbce68e..36a40b851e561 100644 --- a/site/src/pages/AuditPage/AuditFilter.tsx +++ b/site/src/pages/AuditPage/AuditFilter.tsx @@ -6,12 +6,7 @@ import { SelectFilterSearch, } from "components/Filter/SelectFilter"; import { type UserFilterMenu, UserMenu } from "components/Filter/UserFilter"; -import { - Filter, - MenuSkeleton, - SearchFieldSkeleton, - type useFilter, -} from "components/Filter/Filter"; +import { type useFilter, Filter, MenuSkeleton } from "components/Filter/Filter"; import { type UseFilterMenuOptions, useFilterMenu, diff --git a/site/src/pages/TemplatesPage/TemplatesFilter.tsx b/site/src/pages/TemplatesPage/TemplatesFilter.tsx index 45dc37d9a3da7..f564f88ee8943 100644 --- a/site/src/pages/TemplatesPage/TemplatesFilter.tsx +++ b/site/src/pages/TemplatesPage/TemplatesFilter.tsx @@ -4,12 +4,7 @@ import { SelectFilter, type SelectFilterOption, } from "components/Filter/SelectFilter"; -import { - Filter, - MenuSkeleton, - SearchFieldSkeleton, - type useFilter, -} from "components/Filter/Filter"; +import { type useFilter, Filter, MenuSkeleton } from "components/Filter/Filter"; import { useFilterMenu } from "components/Filter/menu"; import { UserAvatar } from "components/UserAvatar/UserAvatar"; import type { FC } from "react"; diff --git a/site/src/pages/UsersPage/UsersFilter.tsx b/site/src/pages/UsersPage/UsersFilter.tsx index 76eb47bc10d3e..03f05ec73f254 100644 --- a/site/src/pages/UsersPage/UsersFilter.tsx +++ b/site/src/pages/UsersPage/UsersFilter.tsx @@ -2,12 +2,7 @@ import { SelectFilter, type SelectFilterOption, } from "components/Filter/SelectFilter"; -import { - Filter, - MenuSkeleton, - SearchFieldSkeleton, - type useFilter, -} from "components/Filter/Filter"; +import { type useFilter, Filter, MenuSkeleton } from "components/Filter/Filter"; import { type UseFilterMenuOptions, useFilterMenu, From 66a55a6fc8946fe4608d5df4935793cb183ba4b8 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Thu, 5 Sep 2024 22:48:02 +0000 Subject: [PATCH 08/27] fix: update responsive logic for audit table filter --- site/src/components/Filter/Filter.tsx | 16 +++++++++++----- site/src/components/Filter/SelectFilter.tsx | 2 +- site/src/components/SearchField/SearchField.tsx | 4 ++++ site/src/pages/AuditPage/AuditFilter.tsx | 1 - 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/site/src/components/Filter/Filter.tsx b/site/src/components/Filter/Filter.tsx index 314239c2ff5bd..76a96fc0d3760 100644 --- a/site/src/components/Filter/Filter.tsx +++ b/site/src/components/Filter/Filter.tsx @@ -139,7 +139,13 @@ type FilterProps = { error?: unknown; options?: ReactNode; presets: PresetFilter[]; - breakpoint?: Breakpoint; + + /** + * The CSS media query breakpoint that defines when the UI will try + * displaying all options on one row, regardless of the number of options + * present + */ + singleRowBreakpoint?: Breakpoint; }; export const Filter: FC = ({ @@ -152,7 +158,7 @@ export const Filter: FC = ({ learnMoreLabel2, learnMoreLink2, presets, - breakpoint = "md", + singleRowBreakpoint = "lg", }) => { const theme = useTheme(); // Storing local copy of the filter query so that it can be updated more @@ -183,10 +189,10 @@ export const Filter: FC = ({ display: "flex", gap: 8, marginBottom: 16, - flexWrap: "nowrap", + flexWrap: "wrap", - [theme.breakpoints.down(breakpoint)]: { - flexWrap: "wrap", + [theme.breakpoints.up(singleRowBreakpoint)]: { + flexWrap: "nowrap", }, }} > diff --git a/site/src/components/Filter/SelectFilter.tsx b/site/src/components/Filter/SelectFilter.tsx index a06730993530b..1b55cf2585806 100644 --- a/site/src/components/Filter/SelectFilter.tsx +++ b/site/src/components/Filter/SelectFilter.tsx @@ -52,7 +52,7 @@ export const SelectFilter: FC = ({ {selectedOption?.label ?? placeholder} diff --git a/site/src/components/SearchField/SearchField.tsx b/site/src/components/SearchField/SearchField.tsx index 0c5414cdc82c1..e63820d44c8dc 100644 --- a/site/src/components/SearchField/SearchField.tsx +++ b/site/src/components/SearchField/SearchField.tsx @@ -21,6 +21,10 @@ export const SearchField: FC = ({ const theme = useTheme(); return ( onChange(e.target.value)} diff --git a/site/src/pages/AuditPage/AuditFilter.tsx b/site/src/pages/AuditPage/AuditFilter.tsx index 36a40b851e561..b3bc87fae3ed3 100644 --- a/site/src/pages/AuditPage/AuditFilter.tsx +++ b/site/src/pages/AuditPage/AuditFilter.tsx @@ -55,7 +55,6 @@ export const AuditFilter: FC = ({ filter, error, menus }) => { isLoading={menus.user.isInitializing} filter={filter} error={error} - breakpoint={menus.organization && "lg"} options={ <> From d7c9571a51b859a74567775c4eda3db30bd6ff32 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Fri, 6 Sep 2024 02:51:05 +0000 Subject: [PATCH 09/27] chore: add organizations option group to workspaces table --- site/src/components/Filter/UserFilter.tsx | 6 +++--- site/src/pages/AuditPage/AuditPage.tsx | 10 +++------- .../src/pages/WorkspacesPage/WorkspacesPage.tsx | 13 +++++++++++++ .../WorkspacesPage/filter/WorkspacesFilter.tsx | 17 +++++++++++++---- site/src/pages/WorkspacesPage/filter/menus.tsx | 17 +++++++++++++++-- 5 files changed, 47 insertions(+), 16 deletions(-) diff --git a/site/src/components/Filter/UserFilter.tsx b/site/src/components/Filter/UserFilter.tsx index 9ef7ccf6a3377..3bd5936bfcb7a 100644 --- a/site/src/components/Filter/UserFilter.tsx +++ b/site/src/components/Filter/UserFilter.tsx @@ -19,8 +19,8 @@ export const useUserFilterMenu = ({ >) => { const { user: me } = useAuthenticated(); - const addMeAsFirstOption = (options: SelectFilterOption[]) => { - options = options.filter((option) => option.value !== me.username); + const addMeAsFirstOption = (options: readonly SelectFilterOption[]) => { + const filtered = options.filter((o) => o.value !== me.username); return [ { label: me.username, @@ -33,7 +33,7 @@ export const useUserFilterMenu = ({ /> ), }, - ...options, + ...filtered, ]; }; diff --git a/site/src/pages/AuditPage/AuditPage.tsx b/site/src/pages/AuditPage/AuditPage.tsx index 7ff21983b891b..0a5c852a8b4cc 100644 --- a/site/src/pages/AuditPage/AuditPage.tsx +++ b/site/src/pages/AuditPage/AuditPage.tsx @@ -18,7 +18,7 @@ import { AuditPageView } from "./AuditPageView"; const AuditPage: FC = () => { const feats = useFeatureVisibility(); - const { experiments } = useDashboard(); + const { showOrganizations } = useDashboard(); /** * There is an implicit link between auditsQuery and filter via the @@ -70,10 +70,6 @@ const AuditPage: FC = () => { }), }); - // With the multi-organization experiment enabled, show extra organization - // info and the organization filter dropdon. - const canViewOrganizations = experiments.includes("multi-organization"); - return ( <> @@ -86,7 +82,7 @@ const AuditPage: FC = () => { isAuditLogVisible={feats.audit_log} auditsQuery={auditsQuery} error={auditsQuery.error} - showOrgDetails={canViewOrganizations} + showOrgDetails={showOrganizations} filterProps={{ filter, error: auditsQuery.error, @@ -94,7 +90,7 @@ const AuditPage: FC = () => { user: userMenu, action: actionMenu, resourceType: resourceTypeMenu, - organization: canViewOrganizations ? organizationsMenu : undefined, + organization: showOrganizations ? organizationsMenu : undefined, }, }} /> diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx index 097cd2e66998a..5d2a3f9270270 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx @@ -17,6 +17,7 @@ import { WorkspacesPageView } from "./WorkspacesPageView"; import { useBatchActions } from "./batchActions"; import { useWorkspaceUpdate, useWorkspacesData } from "./data"; import { useStatusFilterMenu, useTemplateFilterMenu } from "./filter/menus"; +import { useOrganizationsFilterMenu } from "pages/AuditPage/AuditFilter"; function useSafeSearchParams() { // Have to wrap setSearchParams because React Router doesn't make sure that @@ -175,12 +176,24 @@ const useWorkspacesFilter = ({ filter.update({ ...filter.values, status: option?.value }), }); + const { showOrganizations } = useDashboard(); + const organizationsMenu = useOrganizationsFilterMenu({ + value: filter.values.organization, + onChange: (option) => { + filter.update({ + ...filter.values, + organization: option?.value, + }); + }, + }); + return { filter, menus: { user: canFilterByUser ? userMenu : undefined, template: templateMenu, status: statusMenu, + organizations: showOrganizations ? organizationsMenu : undefined, }, }; }; diff --git a/site/src/pages/WorkspacesPage/filter/WorkspacesFilter.tsx b/site/src/pages/WorkspacesPage/filter/WorkspacesFilter.tsx index bd990d0c07696..442179d4c856d 100644 --- a/site/src/pages/WorkspacesPage/filter/WorkspacesFilter.tsx +++ b/site/src/pages/WorkspacesPage/filter/WorkspacesFilter.tsx @@ -9,6 +9,10 @@ import { type TemplateFilterMenu, TemplateMenu, } from "./menus"; +import { + type OrganizationsFilterMenu, + OrganizationsMenu, +} from "pages/AuditPage/AuditFilter"; export const workspaceFilterQuery = { me: "owner:me", @@ -65,6 +69,7 @@ type WorkspaceFilterProps = { user?: UserFilterMenu; template: TemplateFilterMenu; status: StatusFilterMenu; + organizations?: OrganizationsFilterMenu; }; }; @@ -73,7 +78,8 @@ export const WorkspacesFilter: FC = ({ error, menus, }) => { - const { entitlements } = useDashboard(); + const { entitlements, showOrganizations } = useDashboard(); + const width = showOrganizations ? 175 : undefined; const presets = entitlements.features.advanced_template_scheduling.enabled ? PRESETS_WITH_DORMANT : PRESET_FILTERS; @@ -87,9 +93,12 @@ export const WorkspacesFilter: FC = ({ learnMoreLink={docs("/workspaces#workspace-filtering")} options={ <> - {menus.user && } - - + {menus.user && } + + + {showOrganizations && menus.organizations !== undefined && ( + + )} } optionsSkeleton={ diff --git a/site/src/pages/WorkspacesPage/filter/menus.tsx b/site/src/pages/WorkspacesPage/filter/menus.tsx index 841162422c9db..50a3b5ff3cea2 100644 --- a/site/src/pages/WorkspacesPage/filter/menus.tsx +++ b/site/src/pages/WorkspacesPage/filter/menus.tsx @@ -11,6 +11,7 @@ import { } from "components/Filter/menu"; import { StatusIndicator } from "components/StatusIndicator/StatusIndicator"; import { TemplateAvatar } from "components/TemplateAvatar/TemplateAvatar"; +import type { FC } from "react"; import { getDisplayWorkspaceStatus } from "utils/workspace"; export const useTemplateFilterMenu = ({ @@ -53,9 +54,15 @@ export const useTemplateFilterMenu = ({ export type TemplateFilterMenu = ReturnType; -export const TemplateMenu = (menu: TemplateFilterMenu) => { +type TemplateMenuProps = Readonly<{ + width?: number; + menu: TemplateFilterMenu; +}>; + +export const TemplateMenu: FC = ({ width, menu }) => { return ( ; -export const StatusMenu = (menu: StatusFilterMenu) => { +type StatusMenuProps = Readonly<{ + width?: number; + menu: StatusFilterMenu; +}>; + +export const StatusMenu: FC = ({ width, menu }) => { return ( Date: Fri, 6 Sep 2024 16:28:23 +0000 Subject: [PATCH 10/27] refactor: make prop contracts more explicit --- .../pages/WorkspacesPage/WorkspacesPageView.tsx | 15 +++++++++++---- .../WorkspacesPage/filter/WorkspacesFilter.tsx | 2 +- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx index 6c58048ba7b9e..bc25572c23578 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx @@ -23,12 +23,15 @@ import { PaginationWidgetBase } from "components/PaginationWidget/PaginationWidg import { Stack } from "components/Stack/Stack"; import { TableToolbar } from "components/TableToolbar/TableToolbar"; import { WorkspacesTable } from "pages/WorkspacesPage/WorkspacesTable"; -import type { ComponentProps, FC } from "react"; +import type { FC } from "react"; import type { UseQueryResult } from "react-query"; import { mustUpdateWorkspace } from "utils/workspace"; import { WorkspaceHelpTooltip } from "./WorkspaceHelpTooltip"; import { WorkspacesButton } from "./WorkspacesButton"; -import { WorkspacesFilter } from "./filter/WorkspacesFilter"; +import { + type WorkspaceFilterProps, + WorkspacesFilter, +} from "./filter/WorkspacesFilter"; export const Language = { pageTitle: "Workspaces", @@ -47,7 +50,7 @@ export interface WorkspacesPageViewProps { workspaces?: readonly Workspace[]; checkedWorkspaces: readonly Workspace[]; count?: number; - filterProps: ComponentProps; + filterProps: WorkspaceFilterProps; page: number; limit: number; onPageChange: (page: number) => void; @@ -116,7 +119,11 @@ export const WorkspacesPageView: FC = ({ {hasError(error) && !isApiValidationError(error) && ( )} - + diff --git a/site/src/pages/WorkspacesPage/filter/WorkspacesFilter.tsx b/site/src/pages/WorkspacesPage/filter/WorkspacesFilter.tsx index 442179d4c856d..a57581fe9cd9e 100644 --- a/site/src/pages/WorkspacesPage/filter/WorkspacesFilter.tsx +++ b/site/src/pages/WorkspacesPage/filter/WorkspacesFilter.tsx @@ -62,7 +62,7 @@ const PRESETS_WITH_DORMANT: FilterPreset[] = [ }, ]; -type WorkspaceFilterProps = { +export type WorkspaceFilterProps = { filter: ReturnType; error?: unknown; menus: { From 8acc33b23ee2e0c27c9f2656fead05968f2ed99e Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Fri, 6 Sep 2024 17:12:35 +0000 Subject: [PATCH 11/27] refactor: centralize the organizations dropdown logic --- site/src/modules/tableFiltering/options.tsx | 121 ++++++++++++++++++ site/src/pages/AuditPage/AuditFilter.tsx | 115 ++--------------- .../filter/WorkspacesFilter.tsx | 10 +- 3 files changed, 135 insertions(+), 111 deletions(-) create mode 100644 site/src/modules/tableFiltering/options.tsx diff --git a/site/src/modules/tableFiltering/options.tsx b/site/src/modules/tableFiltering/options.tsx new file mode 100644 index 0000000000000..a9ec98a4dab7e --- /dev/null +++ b/site/src/modules/tableFiltering/options.tsx @@ -0,0 +1,121 @@ +/** + * @file Defines a centralized place for filter dropdown groups that are + * relevant across multiple pages within the Coder UI. + * + * @todo 2024-09-06 - Figure out how to move the user dropdown group into this + * file (or whether there are enough subtle differences that it's not worth + * centralizing the logic). We currently have two separate implementations for + * the workspaces and audits page that have a risk of getting out of sync. + */ +import { API } from "api/api"; +import { + type UseFilterMenuOptions, + useFilterMenu, +} from "components/Filter/menu"; +import { + SelectFilter, + type SelectFilterOption, + SelectFilterSearch, +} from "components/Filter/SelectFilter"; +import { UserAvatar } from "components/UserAvatar/UserAvatar"; +import type { FC } from "react"; + +// Organization helpers //////////////////////////////////////////////////////// + +export const useOrganizationsFilterMenu = ({ + value, + onChange, +}: Pick, "value" | "onChange">) => { + return useFilterMenu({ + onChange, + value, + id: "organizations", + getSelectedOption: async () => { + if (value) { + const organizations = await API.getOrganizations(); + const organization = organizations.find((o) => o.name === value); + if (organization) { + return { + label: organization.display_name || organization.name, + value: organization.name, + startIcon: ( + + ), + }; + } + } + return null; + }, + getOptions: async () => { + // Only show the organizations for which you can view audit logs. + const organizations = await API.getOrganizations(); + const permissions = await API.checkAuthorization({ + checks: Object.fromEntries( + organizations.map((organization) => [ + organization.id, + { + object: { + resource_type: "audit_log", + organization_id: organization.id, + }, + action: "read", + }, + ]), + ), + }); + return organizations + .filter((organization) => permissions[organization.id]) + .map((organization) => ({ + label: organization.display_name || organization.name, + value: organization.name, + startIcon: ( + + ), + })); + }, + }); +}; + +export type OrganizationsFilterMenu = ReturnType< + typeof useOrganizationsFilterMenu +>; + +interface OrganizationsMenuProps { + menu: OrganizationsFilterMenu; + width?: number; +} + +export const OrganizationsMenu: FC = ({ + menu, + width, +}) => { + return ( + + } + width={width} + /> + ); +}; diff --git a/site/src/pages/AuditPage/AuditFilter.tsx b/site/src/pages/AuditPage/AuditFilter.tsx index b3bc87fae3ed3..1aa7c65455bd4 100644 --- a/site/src/pages/AuditPage/AuditFilter.tsx +++ b/site/src/pages/AuditPage/AuditFilter.tsx @@ -1,18 +1,19 @@ -import { API } from "api/api"; import { AuditActions, ResourceTypes } from "api/typesGenerated"; +import { Filter, MenuSkeleton, type useFilter } from "components/Filter/Filter"; +import { + type UseFilterMenuOptions, + useFilterMenu, +} from "components/Filter/menu"; import { SelectFilter, type SelectFilterOption, - SelectFilterSearch, } from "components/Filter/SelectFilter"; import { type UserFilterMenu, UserMenu } from "components/Filter/UserFilter"; -import { type useFilter, Filter, MenuSkeleton } from "components/Filter/Filter"; -import { - type UseFilterMenuOptions, - useFilterMenu, -} from "components/Filter/menu"; -import { UserAvatar } from "components/UserAvatar/UserAvatar"; import capitalize from "lodash/capitalize"; +import { + type OrganizationsFilterMenu, + OrganizationsMenu, +} from "modules/tableFiltering/options"; import type { FC } from "react"; import { docs } from "utils/docs"; @@ -174,101 +175,3 @@ const ResourceTypeMenu: FC = ({ menu, width }) => { /> ); }; - -export const useOrganizationsFilterMenu = ({ - value, - onChange, -}: Pick, "value" | "onChange">) => { - return useFilterMenu({ - onChange, - value, - id: "organizations", - getSelectedOption: async () => { - if (value) { - const organizations = await API.getOrganizations(); - const organization = organizations.find((o) => o.name === value); - if (organization) { - return { - label: organization.display_name || organization.name, - value: organization.name, - startIcon: ( - - ), - }; - } - } - return null; - }, - getOptions: async () => { - // Only show the organizations for which you can view audit logs. - const organizations = await API.getOrganizations(); - const permissions = await API.checkAuthorization({ - checks: Object.fromEntries( - organizations.map((organization) => [ - organization.id, - { - object: { - resource_type: "audit_log", - organization_id: organization.id, - }, - action: "read", - }, - ]), - ), - }); - return organizations - .filter((organization) => permissions[organization.id]) - .map((organization) => ({ - label: organization.display_name || organization.name, - value: organization.name, - startIcon: ( - - ), - })); - }, - }); -}; - -export type OrganizationsFilterMenu = ReturnType< - typeof useOrganizationsFilterMenu ->; - -interface OrganizationsMenuProps { - menu: OrganizationsFilterMenu; - width?: number; -} - -export const OrganizationsMenu: FC = ({ - menu, - width, -}) => { - return ( - - } - width={width} - /> - ); -}; diff --git a/site/src/pages/WorkspacesPage/filter/WorkspacesFilter.tsx b/site/src/pages/WorkspacesPage/filter/WorkspacesFilter.tsx index a57581fe9cd9e..ed0d28fd1e110 100644 --- a/site/src/pages/WorkspacesPage/filter/WorkspacesFilter.tsx +++ b/site/src/pages/WorkspacesPage/filter/WorkspacesFilter.tsx @@ -1,6 +1,10 @@ +import { Filter, MenuSkeleton, type useFilter } from "components/Filter/Filter"; import { type UserFilterMenu, UserMenu } from "components/Filter/UserFilter"; -import { type useFilter, Filter, MenuSkeleton } from "components/Filter/Filter"; import { useDashboard } from "modules/dashboard/useDashboard"; +import { + type OrganizationsFilterMenu, + OrganizationsMenu, +} from "modules/tableFiltering/options"; import type { FC } from "react"; import { docs } from "utils/docs"; import { @@ -9,10 +13,6 @@ import { type TemplateFilterMenu, TemplateMenu, } from "./menus"; -import { - type OrganizationsFilterMenu, - OrganizationsMenu, -} from "pages/AuditPage/AuditFilter"; export const workspaceFilterQuery = { me: "owner:me", From 5a0c523ca9752c4061bc0f081512ef89eebd2231 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Fri, 6 Sep 2024 17:25:36 +0000 Subject: [PATCH 12/27] fix: update imports and formatting --- site/src/modules/tableFiltering/options.tsx | 8 ++++---- site/src/pages/AuditPage/AuditFilter.tsx | 8 ++++---- site/src/pages/AuditPage/AuditPage.tsx | 9 +++------ site/src/pages/TemplatesPage/TemplatesFilter.tsx | 2 +- site/src/pages/UsersPage/UsersFilter.tsx | 2 +- site/src/pages/WorkspacesPage/WorkspacesPage.tsx | 4 ++-- 6 files changed, 15 insertions(+), 18 deletions(-) diff --git a/site/src/modules/tableFiltering/options.tsx b/site/src/modules/tableFiltering/options.tsx index a9ec98a4dab7e..3eac5b92a184a 100644 --- a/site/src/modules/tableFiltering/options.tsx +++ b/site/src/modules/tableFiltering/options.tsx @@ -8,15 +8,15 @@ * the workspaces and audits page that have a risk of getting out of sync. */ import { API } from "api/api"; -import { - type UseFilterMenuOptions, - useFilterMenu, -} from "components/Filter/menu"; import { SelectFilter, type SelectFilterOption, SelectFilterSearch, } from "components/Filter/SelectFilter"; +import { + type UseFilterMenuOptions, + useFilterMenu, +} from "components/Filter/menu"; import { UserAvatar } from "components/UserAvatar/UserAvatar"; import type { FC } from "react"; diff --git a/site/src/pages/AuditPage/AuditFilter.tsx b/site/src/pages/AuditPage/AuditFilter.tsx index 1aa7c65455bd4..2a256d3f05317 100644 --- a/site/src/pages/AuditPage/AuditFilter.tsx +++ b/site/src/pages/AuditPage/AuditFilter.tsx @@ -1,14 +1,14 @@ import { AuditActions, ResourceTypes } from "api/typesGenerated"; import { Filter, MenuSkeleton, type useFilter } from "components/Filter/Filter"; -import { - type UseFilterMenuOptions, - useFilterMenu, -} from "components/Filter/menu"; import { SelectFilter, type SelectFilterOption, } from "components/Filter/SelectFilter"; import { type UserFilterMenu, UserMenu } from "components/Filter/UserFilter"; +import { + type UseFilterMenuOptions, + useFilterMenu, +} from "components/Filter/menu"; import capitalize from "lodash/capitalize"; import { type OrganizationsFilterMenu, diff --git a/site/src/pages/AuditPage/AuditPage.tsx b/site/src/pages/AuditPage/AuditPage.tsx index 0a5c852a8b4cc..68f566b4bf054 100644 --- a/site/src/pages/AuditPage/AuditPage.tsx +++ b/site/src/pages/AuditPage/AuditPage.tsx @@ -1,19 +1,16 @@ import { paginatedAudits } from "api/queries/audits"; -import { useUserFilterMenu } from "components/Filter/UserFilter"; import { useFilter } from "components/Filter/Filter"; +import { useUserFilterMenu } from "components/Filter/UserFilter"; import { isNonInitialPage } from "components/PaginationWidget/utils"; import { usePaginatedQuery } from "hooks/usePaginatedQuery"; import { useDashboard } from "modules/dashboard/useDashboard"; import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility"; +import { useOrganizationsFilterMenu } from "modules/tableFiltering/options"; import type { FC } from "react"; import { Helmet } from "react-helmet-async"; import { useSearchParams } from "react-router-dom"; import { pageTitle } from "utils/page"; -import { - useActionFilterMenu, - useOrganizationsFilterMenu, - useResourceTypeFilterMenu, -} from "./AuditFilter"; +import { useActionFilterMenu, useResourceTypeFilterMenu } from "./AuditFilter"; import { AuditPageView } from "./AuditPageView"; const AuditPage: FC = () => { diff --git a/site/src/pages/TemplatesPage/TemplatesFilter.tsx b/site/src/pages/TemplatesPage/TemplatesFilter.tsx index f564f88ee8943..40de4c5532054 100644 --- a/site/src/pages/TemplatesPage/TemplatesFilter.tsx +++ b/site/src/pages/TemplatesPage/TemplatesFilter.tsx @@ -1,10 +1,10 @@ import { API } from "api/api"; import type { Organization } from "api/typesGenerated"; +import { Filter, MenuSkeleton, type useFilter } from "components/Filter/Filter"; import { SelectFilter, type SelectFilterOption, } from "components/Filter/SelectFilter"; -import { type useFilter, Filter, MenuSkeleton } from "components/Filter/Filter"; import { useFilterMenu } from "components/Filter/menu"; import { UserAvatar } from "components/UserAvatar/UserAvatar"; import type { FC } from "react"; diff --git a/site/src/pages/UsersPage/UsersFilter.tsx b/site/src/pages/UsersPage/UsersFilter.tsx index 03f05ec73f254..dd6083652d56b 100644 --- a/site/src/pages/UsersPage/UsersFilter.tsx +++ b/site/src/pages/UsersPage/UsersFilter.tsx @@ -1,8 +1,8 @@ +import { Filter, MenuSkeleton, type useFilter } from "components/Filter/Filter"; import { SelectFilter, type SelectFilterOption, } from "components/Filter/SelectFilter"; -import { type useFilter, Filter, MenuSkeleton } from "components/Filter/Filter"; import { type UseFilterMenuOptions, useFilterMenu, diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx index 5d2a3f9270270..abade141d5183 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx @@ -1,11 +1,12 @@ import { templates } from "api/queries/templates"; import type { Workspace } from "api/typesGenerated"; -import { useUserFilterMenu } from "components/Filter/UserFilter"; import { useFilter } from "components/Filter/Filter"; +import { useUserFilterMenu } from "components/Filter/UserFilter"; import { useAuthenticated } from "contexts/auth/RequireAuth"; import { useEffectEvent } from "hooks/hookPolyfills"; import { usePagination } from "hooks/usePagination"; import { useDashboard } from "modules/dashboard/useDashboard"; +import { useOrganizationsFilterMenu } from "modules/tableFiltering/options"; import { type FC, useEffect, useState } from "react"; import { Helmet } from "react-helmet-async"; import { useQuery } from "react-query"; @@ -17,7 +18,6 @@ import { WorkspacesPageView } from "./WorkspacesPageView"; import { useBatchActions } from "./batchActions"; import { useWorkspaceUpdate, useWorkspacesData } from "./data"; import { useStatusFilterMenu, useTemplateFilterMenu } from "./filter/menus"; -import { useOrganizationsFilterMenu } from "pages/AuditPage/AuditFilter"; function useSafeSearchParams() { // Have to wrap setSearchParams because React Router doesn't make sure that From 68290172f04d1349d7b387c504deac0c8a8b3a52 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Fri, 6 Sep 2024 19:13:28 +0000 Subject: [PATCH 13/27] 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 14/27] 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 15/27] 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 16/27] 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 17/27] 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 18/27] 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 19/27] 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 20/27] 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 21/27] 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 22/27] 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 ba8515d7ad9e7c48a59cfef93e394d86c2f66a5d Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Thu, 12 Sep 2024 15:32:35 +0000 Subject: [PATCH 23/27] fix: make sure skeletons stay synced with org feature toggles --- site/src/pages/AuditPage/AuditFilter.tsx | 2 +- site/src/pages/WorkspacesPage/filter/WorkspacesFilter.tsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/site/src/pages/AuditPage/AuditFilter.tsx b/site/src/pages/AuditPage/AuditFilter.tsx index 2a256d3f05317..448a59e88dc8d 100644 --- a/site/src/pages/AuditPage/AuditFilter.tsx +++ b/site/src/pages/AuditPage/AuditFilter.tsx @@ -71,7 +71,7 @@ export const AuditFilter: FC = ({ filter, error, menus }) => { - + {menus.organization && } } /> diff --git a/site/src/pages/WorkspacesPage/filter/WorkspacesFilter.tsx b/site/src/pages/WorkspacesPage/filter/WorkspacesFilter.tsx index ed0d28fd1e110..c695f92647699 100644 --- a/site/src/pages/WorkspacesPage/filter/WorkspacesFilter.tsx +++ b/site/src/pages/WorkspacesPage/filter/WorkspacesFilter.tsx @@ -106,6 +106,7 @@ export const WorkspacesFilter: FC = ({ {menus.user && } + {showOrganizations && } } /> From 775486f31f89112756c73ee7dd9c7777c3e88bb3 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Fri, 13 Sep 2024 15:53:30 +0000 Subject: [PATCH 24/27] 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" || From d40167f7d47ed6991b3f08703d38d23ce5a728fd Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Fri, 13 Sep 2024 15:56:04 +0000 Subject: [PATCH 25/27] docs: clean up wording on SearchField comment --- site/src/components/SearchField/SearchField.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/site/src/components/SearchField/SearchField.tsx b/site/src/components/SearchField/SearchField.tsx index e63820d44c8dc..15a164e00bb08 100644 --- a/site/src/components/SearchField/SearchField.tsx +++ b/site/src/components/SearchField/SearchField.tsx @@ -21,9 +21,8 @@ export const SearchField: FC = ({ const theme = useTheme(); return ( Date: Mon, 16 Sep 2024 20:20:47 +0000 Subject: [PATCH 26/27] fix: shrink mix width threshold for search field --- site/src/components/SearchField/SearchField.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/components/SearchField/SearchField.tsx b/site/src/components/SearchField/SearchField.tsx index 15a164e00bb08..16e0c064d8386 100644 --- a/site/src/components/SearchField/SearchField.tsx +++ b/site/src/components/SearchField/SearchField.tsx @@ -23,7 +23,7 @@ export const SearchField: FC = ({ onChange(e.target.value)} From 6dbfe067d7bbf63f872e80051483221710f7c534 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Mon, 16 Sep 2024 15:35:54 -0500 Subject: [PATCH 27/27] chore: add navigation test for workspace details page (#14629) * chore: add tests for WorkspacePage cross-page navigation * fix: update story to use mock organizations menu --- site/src/contexts/auth/RequireAuth.tsx | 22 ++++- .../WorkspacePage/WorkspacePage.test.tsx | 87 ++++++++++++++++++- .../pages/WorkspacePage/WorkspaceTopbar.tsx | 6 +- .../WorkspacesPageView.stories.tsx | 1 + site/src/testHelpers/renderHelpers.tsx | 12 ++- 5 files changed, 116 insertions(+), 12 deletions(-) diff --git a/site/src/contexts/auth/RequireAuth.tsx b/site/src/contexts/auth/RequireAuth.tsx index 6d66045b9756a..e558b66c802de 100644 --- a/site/src/contexts/auth/RequireAuth.tsx +++ b/site/src/contexts/auth/RequireAuth.tsx @@ -1,14 +1,30 @@ import { API } from "api/api"; import { isApiError } from "api/errors"; import { Loader } from "components/Loader/Loader"; -import { ProxyProvider } from "contexts/ProxyContext"; -import { DashboardProvider } from "modules/dashboard/DashboardProvider"; +import { ProxyProvider as ProductionProxyProvider } from "contexts/ProxyContext"; +import { DashboardProvider as ProductionDashboardProvider } from "modules/dashboard/DashboardProvider"; import { type FC, useEffect } from "react"; import { Navigate, Outlet, useLocation } from "react-router-dom"; import { embedRedirect } from "utils/redirect"; import { type AuthContextValue, useAuthContext } from "./AuthProvider"; -export const RequireAuth: FC = () => { +type RequireAuthProps = Readonly<{ + ProxyProvider?: typeof ProductionProxyProvider; + DashboardProvider?: typeof ProductionDashboardProvider; +}>; + +/** + * Wraps any component and ensures that the user has been authenticated before + * they can access the component's contents. + * + * In production, it is assumed that this component will not be called with any + * props at all. But to make testing easier, you can call this component with + * specific providers to mock them out. + */ +export const RequireAuth: FC = ({ + DashboardProvider = ProductionDashboardProvider, + ProxyProvider = ProductionProxyProvider, +}) => { const location = useLocation(); const { signOut, isSigningOut, isSignedOut, isSignedIn, isLoading } = useAuthContext(); diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx index c6b69348e1a8d..2c113b933483e 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx @@ -3,10 +3,19 @@ import userEvent from "@testing-library/user-event"; import * as apiModule from "api/api"; import type { TemplateVersionParameter, Workspace } from "api/typesGenerated"; import EventSourceMock from "eventsourcemock"; +import { + DashboardContext, + type DashboardProvider, +} from "modules/dashboard/DashboardProvider"; import { http, HttpResponse } from "msw"; +import type { FC } from "react"; +import { type Location, useLocation } from "react-router-dom"; import { + MockAppearanceConfig, MockDeploymentConfig, + MockEntitlements, MockFailedWorkspace, + MockOrganization, MockOutdatedWorkspace, MockStartingWorkspace, MockStoppedWorkspace, @@ -18,14 +27,22 @@ import { MockWorkspaceBuild, MockWorkspaceBuildDelete, } from "testHelpers/entities"; -import { renderWithAuth } from "testHelpers/renderHelpers"; +import { + type RenderWithAuthOptions, + renderWithAuth, +} from "testHelpers/renderHelpers"; import { server } from "testHelpers/server"; import { WorkspacePage } from "./WorkspacePage"; const { API, MissingBuildParameters } = apiModule; +type RenderWorkspacePageOptions = Omit; + // Renders the workspace page and waits for it be loaded -const renderWorkspacePage = async (workspace: Workspace) => { +const renderWorkspacePage = async ( + workspace: Workspace, + options: RenderWorkspacePageOptions = {}, +) => { jest.spyOn(API, "getWorkspaceByOwnerAndName").mockResolvedValue(workspace); jest.spyOn(API, "getTemplate").mockResolvedValueOnce(MockTemplate); jest.spyOn(API, "getTemplateVersionRichParameters").mockResolvedValueOnce([]); @@ -40,6 +57,7 @@ const renderWorkspacePage = async (workspace: Workspace) => { }); renderWithAuth(, { + ...options, route: `/@${workspace.owner_name}/${workspace.name}`, path: "/:username/:workspace", }); @@ -527,4 +545,69 @@ describe("WorkspacePage", () => { ); }); }); + + describe("Navigation to other pages", () => { + it("Shows a quota link when quota budget is greater than 0. Link navigates user to /workspaces route with the URL params populated with the corresponding organization", async () => { + jest.spyOn(API, "getWorkspaceQuota").mockResolvedValueOnce({ + budget: 25, + credits_consumed: 2, + }); + + const MockDashboardProvider: typeof DashboardProvider = ({ + children, + }) => ( + + {children} + + ); + + let destinationLocation!: Location; + const MockWorkspacesPage: FC = () => { + destinationLocation = useLocation(); + return null; + }; + + const workspace: Workspace = { + ...MockWorkspace, + organization_name: MockOrganization.name, + }; + + await renderWorkspacePage(workspace, { + mockAuthProviders: { + DashboardProvider: MockDashboardProvider, + }, + extraRoutes: [ + { + path: "/workspaces", + element: , + }, + ], + }); + + const quotaLink = await screen.findByRole("link", { + name: /\d+ credits of \d+/i, + }); + + const orgName = encodeURIComponent(MockOrganization.name); + expect( + quotaLink.href.endsWith(`/workspaces?filter=organization:${orgName}`), + ).toBe(true); + + const user = userEvent.setup(); + await user.click(quotaLink); + + expect(destinationLocation.pathname).toBe("/workspaces"); + expect(destinationLocation.search).toBe( + `?filter=organization:${orgName}`, + ); + }); + }); }); diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx index c4a8ae30c0239..7ab39b6df0cba 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx @@ -147,11 +147,7 @@ export const WorkspaceTopbar: FC = ({ )} diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx index 02f34d0189691..ef639d087fb5a 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx @@ -98,6 +98,7 @@ const defaultFilterProps = getDefaultFilterProps({ user: MockMenu, template: MockMenu, status: MockMenu, + organizations: MockMenu, }, values: { owner: MockUser.username, diff --git a/site/src/testHelpers/renderHelpers.tsx b/site/src/testHelpers/renderHelpers.tsx index a6a41fd2a26ee..30aa6b7e89e10 100644 --- a/site/src/testHelpers/renderHelpers.tsx +++ b/site/src/testHelpers/renderHelpers.tsx @@ -4,9 +4,11 @@ import { waitFor, } from "@testing-library/react"; import { AppProviders } from "App"; +import type { ProxyProvider } from "contexts/ProxyContext"; import { ThemeProvider } from "contexts/ThemeProvider"; import { RequireAuth } from "contexts/auth/RequireAuth"; import { DashboardLayout } from "modules/dashboard/DashboardLayout"; +import type { DashboardProvider } from "modules/dashboard/DashboardProvider"; import { ManagementSettingsLayout } from "pages/ManagementSettingsPage/ManagementSettingsLayout"; import { TemplateSettingsLayout } from "pages/TemplateSettingsPage/TemplateSettingsLayout"; import { WorkspaceSettingsLayout } from "pages/WorkspaceSettingsPage/WorkspaceSettingsLayout"; @@ -83,6 +85,11 @@ export type RenderWithAuthOptions = { nonAuthenticatedRoutes?: RouteObject[]; // In case you want to render a layout inside of it children?: RouteObject["children"]; + + mockAuthProviders?: Readonly<{ + DashboardProvider?: typeof DashboardProvider; + ProxyProvider?: typeof ProxyProvider; + }>; }; export function renderWithAuth( @@ -92,12 +99,13 @@ export function renderWithAuth( route = "/", extraRoutes = [], nonAuthenticatedRoutes = [], + mockAuthProviders = {}, children, }: RenderWithAuthOptions = {}, ) { const routes: RouteObject[] = [ { - element: , + element: , children: [{ path, element, children }, ...extraRoutes], }, ...nonAuthenticatedRoutes, @@ -108,8 +116,8 @@ export function renderWithAuth( ); return { - user: MockUser, ...renderResult, + user: MockUser, }; }