Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 84922e2

Browse files
authored
feat: add provisioners view to organization settings (#14501)
1 parent c3f0db3 commit 84922e2

16 files changed

+443
-205
lines changed

site/src/api/queries/organizations.ts

+7
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,13 @@ export const organizationsPermissions = (
223223
},
224224
action: "create",
225225
},
226+
viewProvisioners: {
227+
object: {
228+
resource_type: "provisioner_daemon",
229+
organization_id: organizationId,
230+
},
231+
action: "read",
232+
},
226233
});
227234

228235
// The endpoint takes a flat array, so to avoid collisions prepend each

site/src/components/Pill/Pill.tsx

+24-2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type { ThemeRole } from "theme/roles";
1414
export type PillProps = HTMLAttributes<HTMLDivElement> & {
1515
icon?: ReactNode;
1616
type?: ThemeRole;
17+
size?: "md" | "lg";
1718
};
1819

1920
const themeStyles = (type: ThemeRole) => (theme: Theme) => {
@@ -30,13 +31,25 @@ const PILL_ICON_SPACING = (PILL_HEIGHT - PILL_ICON_SIZE) / 2;
3031

3132
export const Pill: FC<PillProps> = forwardRef<HTMLDivElement, PillProps>(
3233
(props, ref) => {
33-
const { icon, type = "inactive", children, ...divProps } = props;
34+
const {
35+
icon,
36+
type = "inactive",
37+
children,
38+
size = "md",
39+
...divProps
40+
} = props;
3441
const typeStyles = useMemo(() => themeStyles(type), [type]);
3542

3643
return (
3744
<div
3845
ref={ref}
39-
css={[styles.pill, icon && styles.pillWithIcon, typeStyles]}
46+
css={[
47+
styles.pill,
48+
icon && size === "md" && styles.pillWithIcon,
49+
size === "lg" && styles.pillLg,
50+
icon && size === "lg" && styles.pillLgWithIcon,
51+
typeStyles,
52+
]}
4053
{...divProps}
4154
>
4255
{icon}
@@ -80,6 +93,15 @@ const styles = {
8093
paddingLeft: PILL_ICON_SPACING,
8194
},
8295

96+
pillLg: {
97+
gap: PILL_ICON_SPACING * 2,
98+
padding: "14px 16px",
99+
},
100+
101+
pillLgWithIcon: {
102+
paddingLeft: PILL_ICON_SPACING * 2,
103+
},
104+
83105
spinner: (theme) => ({
84106
color: theme.experimental.l1.text,
85107
// It is necessary to align it with the MUI Icons internal padding
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { useTheme } from "@emotion/react";
2+
import Business from "@mui/icons-material/Business";
3+
import Person from "@mui/icons-material/Person";
4+
import Tooltip from "@mui/material/Tooltip";
5+
import type { HealthMessage, ProvisionerDaemon } from "api/typesGenerated";
6+
import { Pill } from "components/Pill/Pill";
7+
import type { FC } from "react";
8+
import { createDayString } from "utils/createDayString";
9+
import { ProvisionerTag } from "./ProvisionerTag";
10+
11+
interface ProvisionerProps {
12+
readonly provisioner: ProvisionerDaemon;
13+
readonly warnings?: readonly HealthMessage[];
14+
}
15+
16+
export const Provisioner: FC<ProvisionerProps> = ({
17+
provisioner,
18+
warnings,
19+
}) => {
20+
const theme = useTheme();
21+
const daemonScope = provisioner.tags.scope || "organization";
22+
const iconScope = daemonScope === "organization" ? <Business /> : <Person />;
23+
24+
const extraTags = Object.entries(provisioner.tags).filter(
25+
([key]) => key !== "scope" && key !== "owner",
26+
);
27+
const isWarning = warnings && warnings.length > 0;
28+
return (
29+
<div
30+
key={provisioner.name}
31+
css={[
32+
{
33+
borderRadius: 8,
34+
border: `1px solid ${theme.palette.divider}`,
35+
fontSize: 14,
36+
},
37+
isWarning && { borderColor: theme.palette.warning.light },
38+
]}
39+
>
40+
<header
41+
css={{
42+
padding: 24,
43+
display: "flex",
44+
alignItems: "center",
45+
justifyContenxt: "space-between",
46+
gap: 24,
47+
}}
48+
>
49+
<div
50+
css={{
51+
display: "flex",
52+
alignItems: "center",
53+
gap: 24,
54+
objectFit: "fill",
55+
}}
56+
>
57+
<div css={{ lineHeight: "160%" }}>
58+
<h4 css={{ fontWeight: 500, margin: 0 }}>{provisioner.name}</h4>
59+
<span css={{ color: theme.palette.text.secondary }}>
60+
<code>{provisioner.version}</code>
61+
</span>
62+
</div>
63+
</div>
64+
<div
65+
css={{
66+
marginLeft: "auto",
67+
display: "flex",
68+
flexWrap: "wrap",
69+
gap: 12,
70+
}}
71+
>
72+
<Tooltip title="Scope">
73+
<Pill size="lg" icon={iconScope}>
74+
<span
75+
css={{
76+
":first-letter": { textTransform: "uppercase" },
77+
}}
78+
>
79+
{daemonScope}
80+
</span>
81+
</Pill>
82+
</Tooltip>
83+
{extraTags.map(([key, value]) => (
84+
<ProvisionerTag key={key} tagName={key} tagValue={value} />
85+
))}
86+
</div>
87+
</header>
88+
89+
<div
90+
css={{
91+
borderTop: `1px solid ${theme.palette.divider}`,
92+
display: "flex",
93+
alignItems: "center",
94+
justifyContent: "space-between",
95+
padding: "8px 24px",
96+
fontSize: 12,
97+
color: theme.palette.text.secondary,
98+
}}
99+
>
100+
{warnings && warnings.length > 0 ? (
101+
<div css={{ display: "flex", flexDirection: "column" }}>
102+
{warnings.map((warning) => (
103+
<span key={warning.code}>{warning.message}</span>
104+
))}
105+
</div>
106+
) : (
107+
<span>No warnings</span>
108+
)}
109+
{provisioner.last_seen_at && (
110+
<span css={{ color: theme.roles.info.text }} data-chromatic="ignore">
111+
Last seen {createDayString(provisioner.last_seen_at)}
112+
</span>
113+
)}
114+
</div>
115+
</div>
116+
);
117+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import type { Interpolation, Theme } from "@emotion/react";
2+
import CheckCircleOutlined from "@mui/icons-material/CheckCircleOutlined";
3+
import CloseIcon from "@mui/icons-material/Close";
4+
import DoNotDisturbOnOutlined from "@mui/icons-material/DoNotDisturbOnOutlined";
5+
import Sell from "@mui/icons-material/Sell";
6+
import IconButton from "@mui/material/IconButton";
7+
import { Pill } from "components/Pill/Pill";
8+
import type { ComponentProps, FC } from "react";
9+
10+
const parseBool = (s: string): { valid: boolean; value: boolean } => {
11+
switch (s.toLowerCase()) {
12+
case "true":
13+
case "yes":
14+
case "1":
15+
return { valid: true, value: true };
16+
case "false":
17+
case "no":
18+
case "0":
19+
case "":
20+
return { valid: true, value: false };
21+
default:
22+
return { valid: false, value: false };
23+
}
24+
};
25+
26+
interface ProvisionerTagProps {
27+
tagName: string;
28+
tagValue: string;
29+
/** Only used in the TemplateVersionEditor */
30+
onDelete?: (tagName: string) => void;
31+
}
32+
33+
export const ProvisionerTag: FC<ProvisionerTagProps> = ({
34+
tagName,
35+
tagValue,
36+
onDelete,
37+
}) => {
38+
const { valid, value: boolValue } = parseBool(tagValue);
39+
const kv = (
40+
<>
41+
<span css={{ fontWeight: 600 }}>{tagName}</span> <span>{tagValue}</span>
42+
</>
43+
);
44+
const content = onDelete ? (
45+
<>
46+
{kv}
47+
<IconButton
48+
aria-label={`delete-${tagName}`}
49+
size="small"
50+
color="secondary"
51+
onClick={() => {
52+
onDelete(tagName);
53+
}}
54+
>
55+
<CloseIcon fontSize="inherit" css={{ width: 14, height: 14 }} />
56+
</IconButton>
57+
</>
58+
) : (
59+
kv
60+
);
61+
if (valid) {
62+
return <BooleanPill value={boolValue}>{content}</BooleanPill>;
63+
}
64+
return (
65+
<Pill size="lg" icon={<Sell />}>
66+
{content}
67+
</Pill>
68+
);
69+
};
70+
71+
type BooleanPillProps = Omit<ComponentProps<typeof Pill>, "icon" | "value"> & {
72+
value: boolean;
73+
};
74+
75+
export const BooleanPill: FC<BooleanPillProps> = ({
76+
value,
77+
children,
78+
...divProps
79+
}) => {
80+
return (
81+
<Pill
82+
type={value ? "active" : "danger"}
83+
size="lg"
84+
icon={
85+
value ? (
86+
<CheckCircleOutlined css={styles.truePill} />
87+
) : (
88+
<DoNotDisturbOnOutlined css={styles.falsePill} />
89+
)
90+
}
91+
{...divProps}
92+
>
93+
{children}
94+
</Pill>
95+
);
96+
};
97+
98+
const styles = {
99+
truePill: (theme) => ({
100+
color: theme.roles.active.outline,
101+
}),
102+
falsePill: (theme) => ({
103+
color: theme.roles.danger.outline,
104+
}),
105+
} satisfies Record<string, Interpolation<Theme>>;

site/src/pages/HealthPage/Content.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ export const BooleanPill: FC<BooleanPillProps> = ({
195195
...divProps
196196
}) => {
197197
const theme = useTheme();
198-
const color = value ? theme.palette.success.light : theme.palette.error.light;
198+
const color = value ? theme.roles.success.outline : theme.roles.error.outline;
199199

200200
return (
201201
<Pill

0 commit comments

Comments
 (0)