Skip to content

Commit 3338f32

Browse files
authored
fix: improve provisioner details layout and show count line (coder#14749)
* きれい * とても大きい * improve storybook test * ボタン * アイコンの名前 * ジェ
1 parent 3501782 commit 3338f32

File tree

3 files changed

+118
-44
lines changed

3 files changed

+118
-44
lines changed

site/src/modules/provisioners/ProvisionerGroup.tsx

+37-16
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
2-
import Business from "@mui/icons-material/Business";
3-
import Person from "@mui/icons-material/Person";
2+
import BusinessIcon from "@mui/icons-material/Business";
3+
import PersonIcon from "@mui/icons-material/Person";
4+
import TagIcon from "@mui/icons-material/Sell";
45
import Button from "@mui/material/Button";
56
import Link from "@mui/material/Link";
67
import Tooltip from "@mui/material/Tooltip";
@@ -21,6 +22,7 @@ import {
2122
} from "components/Popover/Popover";
2223
import { Stack } from "components/Stack/Stack";
2324
import { StatusIndicator } from "components/StatusIndicator/StatusIndicator";
25+
import isEqual from "lodash/isEqual";
2426
import { type FC, useState } from "react";
2527
import { createDayString } from "utils/createDayString";
2628
import { docs } from "utils/docs";
@@ -30,14 +32,16 @@ type ProvisionerGroupType = "builtin" | "psk" | "key";
3032

3133
interface ProvisionerGroupProps {
3234
readonly buildInfo?: BuildInfoResponse;
33-
readonly keyName?: string;
35+
readonly keyName: string;
36+
readonly keyTags: Record<string, string>;
3437
readonly type: ProvisionerGroupType;
3538
readonly provisioners: readonly ProvisionerDaemon[];
3639
}
3740

3841
export const ProvisionerGroup: FC<ProvisionerGroupProps> = ({
3942
buildInfo,
4043
keyName,
44+
keyTags,
4145
type,
4246
provisioners,
4347
}) => {
@@ -61,7 +65,7 @@ export const ProvisionerGroup: FC<ProvisionerGroupProps> = ({
6165
provisioners.length === 1
6266
? "1 provisioner"
6367
: `${provisioners.length} provisioners`;
64-
const extraTags = Object.entries(firstProvisioner.tags).filter(
68+
const extraTags = Object.entries(keyTags).filter(
6569
([key]) => key !== "scope" && key !== "owner",
6670
);
6771

@@ -90,6 +94,10 @@ export const ProvisionerGroup: FC<ProvisionerGroupProps> = ({
9094
? "1 provisioner"
9195
: `${provisionersWithWarnings} provisioners`;
9296

97+
const hasMultipleTagVariants =
98+
type === "psk" &&
99+
provisioners.some((it) => !isEqual(it.tags, { scope: "organization" }));
100+
93101
return (
94102
<div
95103
css={[
@@ -153,14 +161,26 @@ export const ProvisionerGroup: FC<ProvisionerGroupProps> = ({
153161
justifyContent: "right",
154162
}}
155163
>
156-
<Tooltip title="Scope">
157-
<Pill
158-
size="lg"
159-
icon={daemonScope === "organization" ? <Business /> : <Person />}
160-
>
161-
<span css={{ textTransform: "capitalize" }}>{daemonScope}</span>
164+
{!hasMultipleTagVariants ? (
165+
<Tooltip title="Scope">
166+
<Pill
167+
size="lg"
168+
icon={
169+
daemonScope === "organization" ? (
170+
<BusinessIcon />
171+
) : (
172+
<PersonIcon />
173+
)
174+
}
175+
>
176+
<span css={{ textTransform: "capitalize" }}>{daemonScope}</span>
177+
</Pill>
178+
</Tooltip>
179+
) : (
180+
<Pill size="lg" icon={<TagIcon />}>
181+
Multiple tags
162182
</Pill>
163-
</Tooltip>
183+
)}
164184
{type === "key" &&
165185
extraTags.map(([key, value]) => (
166186
<ProvisionerTag key={key} tagName={key} tagValue={value} />
@@ -172,9 +192,9 @@ export const ProvisionerGroup: FC<ProvisionerGroupProps> = ({
172192
<div
173193
css={{
174194
padding: "0 24px 24px",
175-
display: "flex",
195+
display: "grid",
176196
gap: 12,
177-
flexWrap: "wrap",
197+
gridTemplateColumns: "repeat(auto-fill, minmax(385px, 1fr))",
178198
}}
179199
>
180200
{provisionersWithWarningInfo.map((provisioner) => (
@@ -186,7 +206,6 @@ export const ProvisionerGroup: FC<ProvisionerGroupProps> = ({
186206
border: `1px solid ${theme.palette.divider}`,
187207
fontSize: 14,
188208
padding: "14px 18px",
189-
width: 375,
190209
},
191210
provisioner.warningCount > 0 && styles.warningBorder,
192211
]}
@@ -222,7 +241,7 @@ export const ProvisionerGroup: FC<ProvisionerGroupProps> = ({
222241
)}
223242
</span>
224243
</div>
225-
{type === "psk" && (
244+
{hasMultipleTagVariants && (
226245
<PskProvisionerTags tags={provisioner.tags} />
227246
)}
228247
</Stack>
@@ -317,7 +336,8 @@ interface PskProvisionerTagsProps {
317336

318337
const PskProvisionerTags: FC<PskProvisionerTagsProps> = ({ tags }) => {
319338
const daemonScope = tags.scope || "organization";
320-
const iconScope = daemonScope === "organization" ? <Business /> : <Person />;
339+
const iconScope =
340+
daemonScope === "organization" ? <BusinessIcon /> : <PersonIcon />;
321341

322342
const extraTags = Object.entries(tags).filter(
323343
([tag]) => tag !== "scope" && tag !== "owner",
@@ -343,6 +363,7 @@ const PskProvisionerTags: FC<PskProvisionerTagsProps> = ({ tags }) => {
343363
css={{
344364
"& .MuiPaper-root": {
345365
padding: 20,
366+
minWidth: "unset",
346367
maxWidth: 340,
347368
width: "fit-content",
348369
},

site/src/pages/ManagementSettingsPage/OrganizationProvisionersPageView.stories.tsx

+46-12
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Meta, StoryObj } from "@storybook/react";
2+
import { screen, userEvent } from "@storybook/test";
23
import {
34
MockBuildInfo,
45
MockProvisioner,
@@ -38,7 +39,11 @@ export const Provisioners: Story = {
3839
],
3940
},
4041
{
41-
key: { ...MockProvisionerKey, id: "ジャイデン", name: "ジャイデン" },
42+
key: MockProvisionerPskKey,
43+
daemons: [MockProvisioner, MockProvisioner2],
44+
},
45+
{
46+
key: { ...MockProvisionerKey, id: "ジェイデン", name: "ジェイデン" },
4247
daemons: [MockProvisioner, MockProvisioner2],
4348
},
4449
{
@@ -53,21 +58,50 @@ export const Provisioners: Story = {
5358
],
5459
},
5560
{
56-
key: { ...MockProvisionerKey, id: "ケイラ", name: "ケイラ" },
57-
daemons: [
58-
{
59-
...MockProvisioner,
60-
tags: {
61-
...MockProvisioner.tags,
62-
都市: "ユタ",
63-
きっぷ: "yes",
64-
ちいさい: "no",
65-
},
61+
key: {
62+
...MockProvisionerKey,
63+
id: "ケイラ",
64+
name: "ケイラ",
65+
tags: {
66+
...MockProvisioner.tags,
67+
都市: "ユタ",
68+
きっぷ: "yes",
69+
ちいさい: "no",
6670
},
67-
],
71+
},
72+
daemons: Array.from({ length: 117 }, (_, i) => ({
73+
...MockProvisioner,
74+
id: `ケイラ-${i}`,
75+
name: `ケイラ-${i}`,
76+
})),
6877
},
6978
],
7079
},
80+
play: async ({ step }) => {
81+
await step("open all details", async () => {
82+
const expandButtons = await screen.findAllByRole("button", {
83+
name: "Show provisioner details",
84+
});
85+
for (const it of expandButtons) {
86+
await userEvent.click(it);
87+
}
88+
});
89+
90+
await step("close uninteresting/large details", async () => {
91+
const collapseButtons = await screen.findAllByRole("button", {
92+
name: "Hide provisioner details",
93+
});
94+
95+
await userEvent.click(collapseButtons[2]);
96+
await userEvent.click(collapseButtons[3]);
97+
await userEvent.click(collapseButtons[5]);
98+
});
99+
100+
await step("show version popover", async () => {
101+
const outOfDate = await screen.findByText("Out of date");
102+
await userEvent.hover(outOfDate);
103+
});
104+
},
71105
};
72106

73107
export const Empty: Story = {

site/src/pages/ManagementSettingsPage/OrganizationProvisionersPageView.tsx

+35-16
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,17 @@ export const OrganizationProvisionersPageView: FC<
2525
> = ({ buildInfo, provisioners }) => {
2626
const isEmpty = provisioners.every((group) => group.daemons.length === 0);
2727

28+
const provisionerGroupsCount = provisioners.length;
29+
const provisionersCount = provisioners.reduce(
30+
(a, group) => a + group.daemons.length,
31+
0,
32+
);
33+
2834
return (
2935
<div>
3036
<PageHeader
3137
// The deployment settings layout already has padding.
32-
css={{ paddingTop: 0 }}
38+
css={{ paddingTop: 0, paddingBottom: 12 }}
3339
actions={
3440
<Button
3541
endIcon={<OpenInNewIcon />}
@@ -42,22 +48,34 @@ export const OrganizationProvisionersPageView: FC<
4248
>
4349
<PageHeaderTitle>Provisioners</PageHeaderTitle>
4450
</PageHeader>
51+
{isEmpty ? (
52+
<EmptyState
53+
message="No provisioners"
54+
description="A provisioner is required before you can create templates and workspaces. You can connect your first provisioner by following our documentation."
55+
cta={
56+
<Button
57+
endIcon={<OpenInNewIcon />}
58+
target="_blank"
59+
href={docs("/admin/provisioners")}
60+
>
61+
Show me how to create a provisioner
62+
</Button>
63+
}
64+
/>
65+
) : (
66+
<div
67+
css={(theme) => ({
68+
margin: 0,
69+
fontSize: 12,
70+
paddingBottom: 18,
71+
color: theme.palette.text.secondary,
72+
})}
73+
>
74+
Showing {provisionerGroupsCount} groups and {provisionersCount}{" "}
75+
provisioners
76+
</div>
77+
)}
4578
<Stack spacing={4.5}>
46-
{isEmpty && (
47-
<EmptyState
48-
message="No provisioners"
49-
description="A provisioner is required before you can create templates and workspaces. You can connect your first provisioner by following our documentation."
50-
cta={
51-
<Button
52-
endIcon={<OpenInNewIcon />}
53-
target="_blank"
54-
href={docs("/admin/provisioners")}
55-
>
56-
Show me how to create a provisioner
57-
</Button>
58-
}
59-
/>
60-
)}
6179
{provisioners.map((group) => {
6280
const type = getGroupType(group.key);
6381

@@ -74,6 +92,7 @@ export const OrganizationProvisionersPageView: FC<
7492
key={group.key.id}
7593
buildInfo={buildInfo}
7694
keyName={group.key.name}
95+
keyTags={group.key.tags}
7796
type={type}
7897
provisioners={group.daemons}
7998
/>

0 commit comments

Comments
 (0)