Skip to content

Commit 9102256

Browse files
authored
chore: update workspaces top bar to display org name (coder#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
1 parent 335eb05 commit 9102256

12 files changed

+382
-171
lines changed

site/src/api/api.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1684,11 +1684,13 @@ class ApiMethods {
16841684
};
16851685

16861686
getWorkspaceQuota = async (
1687+
organizationName: string,
16871688
username: string,
16881689
): Promise<TypesGen.WorkspaceQuota> => {
16891690
const response = await this.axios.get(
1690-
`/api/v2/workspace-quota/${encodeURIComponent(username)}`,
1691+
`/api/v2/organizations/${encodeURIComponent(organizationName)}/members/${encodeURIComponent(username)}/workspace-quota`,
16911692
);
1693+
16921694
return response.data;
16931695
};
16941696

site/src/api/queries/workspaceQuota.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import { API } from "api/api";
22

3-
export const getWorkspaceQuotaQueryKey = (username: string) => [
4-
username,
5-
"workspaceQuota",
6-
];
3+
export const getWorkspaceQuotaQueryKey = (
4+
organizationName: string,
5+
username: string,
6+
) => {
7+
return ["workspaceQuota", organizationName, username];
8+
};
79

8-
export const workspaceQuota = (username: string) => {
10+
export const workspaceQuota = (organizationName: string, username: string) => {
911
return {
10-
queryKey: getWorkspaceQuotaQueryKey(username),
11-
queryFn: () => API.getWorkspaceQuota(username),
12+
queryKey: getWorkspaceQuotaQueryKey(organizationName, username),
13+
queryFn: () => API.getWorkspaceQuota(organizationName, username),
1214
};
1315
};
1416

site/src/modules/dashboard/DashboardProvider.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export interface DashboardValue {
1919
entitlements: Entitlements;
2020
experiments: Experiments;
2121
appearance: AppearanceConfig;
22-
organizations: Organization[];
22+
organizations: readonly Organization[];
2323
showOrganizations: boolean;
2424
}
2525

site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPage.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,5 +99,8 @@ export const GroupsPage: FC = () => {
9999

100100
export default GroupsPage;
101101

102-
export const getOrganizationNameByDefault = (organizations: Organization[]) =>
103-
organizations.find((org) => org.is_default)?.name;
102+
export const getOrganizationNameByDefault = (
103+
organizations: readonly Organization[],
104+
) => {
105+
return organizations.find((org) => org.is_default)?.name;
106+
};

site/src/pages/ManagementSettingsPage/ManagementSettingsLayout.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ import { Outlet } from "react-router-dom";
1212
import { DeploySettingsContext } from "../DeploySettingsPage/DeploySettingsLayout";
1313
import { Sidebar } from "./Sidebar";
1414

15-
type OrganizationSettingsValue = {
16-
organizations: Organization[];
17-
};
15+
type OrganizationSettingsValue = Readonly<{
16+
organizations: readonly Organization[];
17+
}>;
1818

1919
export const useOrganizationSettings = (): OrganizationSettingsValue => {
2020
const { organizations } = useDashboard();

site/src/pages/ManagementSettingsPage/OrganizationProvisionersPage.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,5 +59,9 @@ const OrganizationProvisionersPage: FC = () => {
5959

6060
export default OrganizationProvisionersPage;
6161

62-
const getOrganizationByName = (organizations: Organization[], name: string) =>
63-
organizations.find((org) => org.name === name);
62+
const getOrganizationByName = (
63+
organizations: readonly Organization[],
64+
name: string,
65+
) => {
66+
return organizations.find((org) => org.name === name);
67+
};

site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ const OrganizationSettingsPage: FC = () => {
5555
// Redirect /organizations => /organizations/default-org, or if they cannot edit
5656
// the default org, then the first org they can edit, if any.
5757
if (!organizationName) {
58-
const editableOrg = organizations
58+
const editableOrg = [...organizations]
5959
.sort((a, b) => {
6060
// Prefer default org (it may not be first).
6161
// JavaScript will happily subtract booleans, but use numbers to keep
@@ -112,5 +112,9 @@ const OrganizationSettingsPage: FC = () => {
112112

113113
export default OrganizationSettingsPage;
114114

115-
const getOrganizationByName = (organizations: Organization[], name: string) =>
116-
organizations.find((org) => org.name === name);
115+
const getOrganizationByName = (
116+
organizations: readonly Organization[],
117+
name: string,
118+
) => {
119+
return organizations.find((org) => org.name === name);
120+
};

site/src/pages/TemplatePage/TemplateRedirectController.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,11 @@ export const TemplateRedirectController: FC = () => {
4545
return <Outlet />;
4646
};
4747

48-
const getOrganizationNameByDefault = (organizations: Organization[]) =>
49-
organizations.find((org) => org.is_default)?.name;
48+
const getOrganizationNameByDefault = (
49+
organizations: readonly Organization[],
50+
) => {
51+
return organizations.find((org) => org.is_default)?.name;
52+
};
5053

5154
// I really hate doing it this way, but React Router does not provide a better way.
5255
const removePrefix = (self: string, prefix: string) =>

site/src/pages/WorkspacePage/WorkspaceNotifications/WorkspaceNotifications.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,12 @@ export const WorkspaceNotifications: FC<WorkspaceNotificationsProps> = ({
220220
(n) => n.severity === "warning",
221221
);
222222

223+
// We have to avoid rendering out a div at all if there is no content so
224+
// that we don't introduce additional gaps via the parent flex container
225+
if (infoNotifications.length === 0 && warningNotifications.length === 0) {
226+
return null;
227+
}
228+
223229
return (
224230
<div css={styles.notificationsGroup}>
225231
{infoNotifications.length > 0 && (

site/src/pages/WorkspacePage/WorkspaceScheduleControls.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,15 @@ import {
3030
} from "utils/schedule";
3131
import { isWorkspaceOn } from "utils/workspace";
3232

33-
export interface WorkspaceScheduleContainerProps {
33+
interface WorkspaceScheduleContainerProps {
3434
children?: ReactNode;
3535
onClickIcon?: () => void;
3636
}
3737

38-
export const WorkspaceScheduleContainer: FC<
39-
WorkspaceScheduleContainerProps
40-
> = ({ children, onClickIcon }) => {
38+
const WorkspaceScheduleContainer: FC<WorkspaceScheduleContainerProps> = ({
39+
children,
40+
onClickIcon,
41+
}) => {
4142
const icon = (
4243
<TopbarIcon>
4344
<ScheduleOutlined aria-label="Schedule" />
@@ -49,6 +50,7 @@ export const WorkspaceScheduleContainer: FC<
4950
<Tooltip title="Schedule">
5051
{onClickIcon ? (
5152
<button
53+
type="button"
5254
data-testid="schedule-icon-button"
5355
onClick={onClickIcon}
5456
css={styles.scheduleIconButton}
@@ -294,6 +296,7 @@ const styles = {
294296
padding: 0,
295297
fontSize: "inherit",
296298
lineHeight: "inherit",
299+
cursor: "pointer",
297300
},
298301

299302
scheduleValue: {

site/src/pages/WorkspacePage/WorkspaceTopbar.stories.tsx

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import type { Meta, StoryObj } from "@storybook/react";
22
import { expect, screen, userEvent, waitFor, within } from "@storybook/test";
33
import { getWorkspaceQuotaQueryKey } from "api/queries/workspaceQuota";
4+
import type { Workspace, WorkspaceQuota } from "api/typesGenerated";
45
import { addHours, addMinutes } from "date-fns";
56
import {
7+
MockOrganization,
68
MockTemplate,
79
MockTemplateVersion,
810
MockUser,
@@ -11,9 +13,12 @@ import {
1113
import { withDashboardProvider } from "testHelpers/storybook";
1214
import { WorkspaceTopbar } from "./WorkspaceTopbar";
1315

14-
// We want a workspace without a deadline to not pollute the screenshot
15-
const baseWorkspace = {
16+
// We want a workspace without a deadline to not pollute the screenshot. Also
17+
// want to make sure that the workspace is synced to our other mock values
18+
const baseWorkspace: Workspace = {
1619
...MockWorkspace,
20+
organization_name: MockOrganization.name,
21+
organization_id: MockOrganization.id,
1722
latest_build: {
1823
...MockWorkspace.latest_build,
1924
deadline: undefined,
@@ -262,15 +267,37 @@ export const WithFarAwayDeadlineRequiredByTemplate: Story = {
262267
},
263268
};
264269

265-
export const WithQuota: Story = {
270+
export const WithQuotaNoOrgs: Story = {
266271
parameters: {
272+
showOrganizations: false,
267273
queries: [
268274
{
269-
key: getWorkspaceQuotaQueryKey(MockUser.username),
275+
key: getWorkspaceQuotaQueryKey(
276+
MockOrganization.name,
277+
MockUser.username,
278+
),
270279
data: {
271280
credits_consumed: 2,
272281
budget: 40,
273-
},
282+
} satisfies WorkspaceQuota,
283+
},
284+
],
285+
},
286+
};
287+
288+
export const WithQuotaWithOrgs: Story = {
289+
parameters: {
290+
showOrganizations: true,
291+
queries: [
292+
{
293+
key: getWorkspaceQuotaQueryKey(
294+
MockOrganization.name,
295+
MockUser.username,
296+
),
297+
data: {
298+
credits_consumed: 2,
299+
budget: 40,
300+
} satisfies WorkspaceQuota,
274301
},
275302
],
276303
},

0 commit comments

Comments
 (0)