mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix: improve provisioner details layout and show count line (#14749)
* きれい * とても大きい * improve storybook test * ボタン * アイコンの名前 * ジェ
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
|
||||
import Business from "@mui/icons-material/Business";
|
||||
import Person from "@mui/icons-material/Person";
|
||||
import BusinessIcon from "@mui/icons-material/Business";
|
||||
import PersonIcon from "@mui/icons-material/Person";
|
||||
import TagIcon from "@mui/icons-material/Sell";
|
||||
import Button from "@mui/material/Button";
|
||||
import Link from "@mui/material/Link";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
} from "components/Popover/Popover";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import { StatusIndicator } from "components/StatusIndicator/StatusIndicator";
|
||||
import isEqual from "lodash/isEqual";
|
||||
import { type FC, useState } from "react";
|
||||
import { createDayString } from "utils/createDayString";
|
||||
import { docs } from "utils/docs";
|
||||
@@ -30,7 +32,8 @@ type ProvisionerGroupType = "builtin" | "psk" | "key";
|
||||
|
||||
interface ProvisionerGroupProps {
|
||||
readonly buildInfo?: BuildInfoResponse;
|
||||
readonly keyName?: string;
|
||||
readonly keyName: string;
|
||||
readonly keyTags: Record<string, string>;
|
||||
readonly type: ProvisionerGroupType;
|
||||
readonly provisioners: readonly ProvisionerDaemon[];
|
||||
}
|
||||
@@ -38,6 +41,7 @@ interface ProvisionerGroupProps {
|
||||
export const ProvisionerGroup: FC<ProvisionerGroupProps> = ({
|
||||
buildInfo,
|
||||
keyName,
|
||||
keyTags,
|
||||
type,
|
||||
provisioners,
|
||||
}) => {
|
||||
@@ -61,7 +65,7 @@ export const ProvisionerGroup: FC<ProvisionerGroupProps> = ({
|
||||
provisioners.length === 1
|
||||
? "1 provisioner"
|
||||
: `${provisioners.length} provisioners`;
|
||||
const extraTags = Object.entries(firstProvisioner.tags).filter(
|
||||
const extraTags = Object.entries(keyTags).filter(
|
||||
([key]) => key !== "scope" && key !== "owner",
|
||||
);
|
||||
|
||||
@@ -90,6 +94,10 @@ export const ProvisionerGroup: FC<ProvisionerGroupProps> = ({
|
||||
? "1 provisioner"
|
||||
: `${provisionersWithWarnings} provisioners`;
|
||||
|
||||
const hasMultipleTagVariants =
|
||||
type === "psk" &&
|
||||
provisioners.some((it) => !isEqual(it.tags, { scope: "organization" }));
|
||||
|
||||
return (
|
||||
<div
|
||||
css={[
|
||||
@@ -153,14 +161,26 @@ export const ProvisionerGroup: FC<ProvisionerGroupProps> = ({
|
||||
justifyContent: "right",
|
||||
}}
|
||||
>
|
||||
<Tooltip title="Scope">
|
||||
<Pill
|
||||
size="lg"
|
||||
icon={daemonScope === "organization" ? <Business /> : <Person />}
|
||||
>
|
||||
<span css={{ textTransform: "capitalize" }}>{daemonScope}</span>
|
||||
{!hasMultipleTagVariants ? (
|
||||
<Tooltip title="Scope">
|
||||
<Pill
|
||||
size="lg"
|
||||
icon={
|
||||
daemonScope === "organization" ? (
|
||||
<BusinessIcon />
|
||||
) : (
|
||||
<PersonIcon />
|
||||
)
|
||||
}
|
||||
>
|
||||
<span css={{ textTransform: "capitalize" }}>{daemonScope}</span>
|
||||
</Pill>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Pill size="lg" icon={<TagIcon />}>
|
||||
Multiple tags
|
||||
</Pill>
|
||||
</Tooltip>
|
||||
)}
|
||||
{type === "key" &&
|
||||
extraTags.map(([key, value]) => (
|
||||
<ProvisionerTag key={key} tagName={key} tagValue={value} />
|
||||
@@ -172,9 +192,9 @@ export const ProvisionerGroup: FC<ProvisionerGroupProps> = ({
|
||||
<div
|
||||
css={{
|
||||
padding: "0 24px 24px",
|
||||
display: "flex",
|
||||
display: "grid",
|
||||
gap: 12,
|
||||
flexWrap: "wrap",
|
||||
gridTemplateColumns: "repeat(auto-fill, minmax(385px, 1fr))",
|
||||
}}
|
||||
>
|
||||
{provisionersWithWarningInfo.map((provisioner) => (
|
||||
@@ -186,7 +206,6 @@ export const ProvisionerGroup: FC<ProvisionerGroupProps> = ({
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
fontSize: 14,
|
||||
padding: "14px 18px",
|
||||
width: 375,
|
||||
},
|
||||
provisioner.warningCount > 0 && styles.warningBorder,
|
||||
]}
|
||||
@@ -222,7 +241,7 @@ export const ProvisionerGroup: FC<ProvisionerGroupProps> = ({
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{type === "psk" && (
|
||||
{hasMultipleTagVariants && (
|
||||
<PskProvisionerTags tags={provisioner.tags} />
|
||||
)}
|
||||
</Stack>
|
||||
@@ -317,7 +336,8 @@ interface PskProvisionerTagsProps {
|
||||
|
||||
const PskProvisionerTags: FC<PskProvisionerTagsProps> = ({ tags }) => {
|
||||
const daemonScope = tags.scope || "organization";
|
||||
const iconScope = daemonScope === "organization" ? <Business /> : <Person />;
|
||||
const iconScope =
|
||||
daemonScope === "organization" ? <BusinessIcon /> : <PersonIcon />;
|
||||
|
||||
const extraTags = Object.entries(tags).filter(
|
||||
([tag]) => tag !== "scope" && tag !== "owner",
|
||||
@@ -343,6 +363,7 @@ const PskProvisionerTags: FC<PskProvisionerTagsProps> = ({ tags }) => {
|
||||
css={{
|
||||
"& .MuiPaper-root": {
|
||||
padding: 20,
|
||||
minWidth: "unset",
|
||||
maxWidth: 340,
|
||||
width: "fit-content",
|
||||
},
|
||||
|
||||
+46
-12
@@ -1,4 +1,5 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { screen, userEvent } from "@storybook/test";
|
||||
import {
|
||||
MockBuildInfo,
|
||||
MockProvisioner,
|
||||
@@ -38,7 +39,11 @@ export const Provisioners: Story = {
|
||||
],
|
||||
},
|
||||
{
|
||||
key: { ...MockProvisionerKey, id: "ジャイデン", name: "ジャイデン" },
|
||||
key: MockProvisionerPskKey,
|
||||
daemons: [MockProvisioner, MockProvisioner2],
|
||||
},
|
||||
{
|
||||
key: { ...MockProvisionerKey, id: "ジェイデン", name: "ジェイデン" },
|
||||
daemons: [MockProvisioner, MockProvisioner2],
|
||||
},
|
||||
{
|
||||
@@ -53,21 +58,50 @@ export const Provisioners: Story = {
|
||||
],
|
||||
},
|
||||
{
|
||||
key: { ...MockProvisionerKey, id: "ケイラ", name: "ケイラ" },
|
||||
daemons: [
|
||||
{
|
||||
...MockProvisioner,
|
||||
tags: {
|
||||
...MockProvisioner.tags,
|
||||
都市: "ユタ",
|
||||
きっぷ: "yes",
|
||||
ちいさい: "no",
|
||||
},
|
||||
key: {
|
||||
...MockProvisionerKey,
|
||||
id: "ケイラ",
|
||||
name: "ケイラ",
|
||||
tags: {
|
||||
...MockProvisioner.tags,
|
||||
都市: "ユタ",
|
||||
きっぷ: "yes",
|
||||
ちいさい: "no",
|
||||
},
|
||||
],
|
||||
},
|
||||
daemons: Array.from({ length: 117 }, (_, i) => ({
|
||||
...MockProvisioner,
|
||||
id: `ケイラ-${i}`,
|
||||
name: `ケイラ-${i}`,
|
||||
})),
|
||||
},
|
||||
],
|
||||
},
|
||||
play: async ({ step }) => {
|
||||
await step("open all details", async () => {
|
||||
const expandButtons = await screen.findAllByRole("button", {
|
||||
name: "Show provisioner details",
|
||||
});
|
||||
for (const it of expandButtons) {
|
||||
await userEvent.click(it);
|
||||
}
|
||||
});
|
||||
|
||||
await step("close uninteresting/large details", async () => {
|
||||
const collapseButtons = await screen.findAllByRole("button", {
|
||||
name: "Hide provisioner details",
|
||||
});
|
||||
|
||||
await userEvent.click(collapseButtons[2]);
|
||||
await userEvent.click(collapseButtons[3]);
|
||||
await userEvent.click(collapseButtons[5]);
|
||||
});
|
||||
|
||||
await step("show version popover", async () => {
|
||||
const outOfDate = await screen.findByText("Out of date");
|
||||
await userEvent.hover(outOfDate);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const Empty: Story = {
|
||||
|
||||
@@ -25,11 +25,17 @@ export const OrganizationProvisionersPageView: FC<
|
||||
> = ({ buildInfo, provisioners }) => {
|
||||
const isEmpty = provisioners.every((group) => group.daemons.length === 0);
|
||||
|
||||
const provisionerGroupsCount = provisioners.length;
|
||||
const provisionersCount = provisioners.reduce(
|
||||
(a, group) => a + group.daemons.length,
|
||||
0,
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader
|
||||
// The deployment settings layout already has padding.
|
||||
css={{ paddingTop: 0 }}
|
||||
css={{ paddingTop: 0, paddingBottom: 12 }}
|
||||
actions={
|
||||
<Button
|
||||
endIcon={<OpenInNewIcon />}
|
||||
@@ -42,22 +48,34 @@ export const OrganizationProvisionersPageView: FC<
|
||||
>
|
||||
<PageHeaderTitle>Provisioners</PageHeaderTitle>
|
||||
</PageHeader>
|
||||
{isEmpty ? (
|
||||
<EmptyState
|
||||
message="No provisioners"
|
||||
description="A provisioner is required before you can create templates and workspaces. You can connect your first provisioner by following our documentation."
|
||||
cta={
|
||||
<Button
|
||||
endIcon={<OpenInNewIcon />}
|
||||
target="_blank"
|
||||
href={docs("/admin/provisioners")}
|
||||
>
|
||||
Show me how to create a provisioner
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
css={(theme) => ({
|
||||
margin: 0,
|
||||
fontSize: 12,
|
||||
paddingBottom: 18,
|
||||
color: theme.palette.text.secondary,
|
||||
})}
|
||||
>
|
||||
Showing {provisionerGroupsCount} groups and {provisionersCount}{" "}
|
||||
provisioners
|
||||
</div>
|
||||
)}
|
||||
<Stack spacing={4.5}>
|
||||
{isEmpty && (
|
||||
<EmptyState
|
||||
message="No provisioners"
|
||||
description="A provisioner is required before you can create templates and workspaces. You can connect your first provisioner by following our documentation."
|
||||
cta={
|
||||
<Button
|
||||
endIcon={<OpenInNewIcon />}
|
||||
target="_blank"
|
||||
href={docs("/admin/provisioners")}
|
||||
>
|
||||
Show me how to create a provisioner
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{provisioners.map((group) => {
|
||||
const type = getGroupType(group.key);
|
||||
|
||||
@@ -74,6 +92,7 @@ export const OrganizationProvisionersPageView: FC<
|
||||
key={group.key.id}
|
||||
buildInfo={buildInfo}
|
||||
keyName={group.key.name}
|
||||
keyTags={group.key.tags}
|
||||
type={type}
|
||||
provisioners={group.daemons}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user