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

Lines changed: 7 additions & 0 deletions
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

Lines changed: 24 additions & 2 deletions
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
Lines changed: 117 additions & 0 deletions
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+
};
Lines changed: 105 additions & 0 deletions
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

Lines changed: 1 addition & 1 deletion
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy