Skip to content

Commit 85cf075

Browse files
Org Settings Page Header updates & cleanup (#16850)
* wip * Adding error and warning support to Modal * Updating headings for team settings pages * copy updates
1 parent 59ff034 commit 85cf075

14 files changed

+170
-97
lines changed

components/dashboard/src/components/Alert.tsx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { ReactComponent as Exclamation2 } from "../images/exclamation2.svg";
1010
import { ReactComponent as InfoSvg } from "../images/info.svg";
1111
import { ReactComponent as XSvg } from "../images/x.svg";
1212
import { ReactComponent as Check } from "../images/check-circle.svg";
13+
import classNames from "classnames";
1314

1415
export type AlertType =
1516
// Green
@@ -20,6 +21,8 @@ export type AlertType =
2021
| "info"
2122
// Red
2223
| "error"
24+
// Dark Red
25+
| "danger"
2326
// Blue
2427
| "message";
2528

@@ -32,6 +35,7 @@ export interface AlertProps {
3235
onClose?: () => void;
3336
showIcon?: boolean;
3437
icon?: React.ReactNode;
38+
rounded?: boolean;
3539
children?: React.ReactNode;
3640
}
3741

@@ -73,6 +77,12 @@ const infoMap: Record<AlertType, AlertInfo> = {
7377
icon: <Exclamation className="w-4 h-4"></Exclamation>,
7478
iconColor: "text-red-400",
7579
},
80+
danger: {
81+
bgCls: "bg-red-600 dark:bg-red-600",
82+
txtCls: "text-white",
83+
icon: <Exclamation className="w-4 h-4"></Exclamation>,
84+
iconColor: "filter-brightness-10",
85+
},
7686
};
7787

7888
export default function Alert(props: AlertProps) {
@@ -84,11 +94,16 @@ export default function Alert(props: AlertProps) {
8494
const info = infoMap[type];
8595
const showIcon = props.showIcon ?? true;
8696
const light = props.light ?? false;
97+
const rounded = props.rounded ?? true;
8798
return (
8899
<div
89-
className={`flex relative whitespace-pre-wrap rounded p-4 ${info.txtCls} ${props.className || ""} ${
90-
light ? "" : info.bgCls
91-
}`}
100+
className={classNames(
101+
"flex relative whitespace-pre-wrap p-4",
102+
info.txtCls,
103+
props.className,
104+
light ? "" : info.bgCls,
105+
rounded ? "rounded" : "",
106+
)}
92107
>
93108
{showIcon && <span className={`mt-1 mr-4 h-4 w-4 ${info.iconColor}`}>{props.icon ?? info.icon}</span>}
94109
<span className="flex-1 text-left">{props.children}</span>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* Copyright (c) 2023 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License.AGPL.txt in the project root for license information.
5+
*/
6+
7+
import classNames from "classnames";
8+
import { FC, useCallback } from "react";
9+
import { Button } from "./Button";
10+
import { Heading2, Subheading } from "./typography/headings";
11+
12+
type Props = {
13+
title: string;
14+
subtitle?: string;
15+
buttonText?: string;
16+
onClick?: () => void;
17+
className?: string;
18+
};
19+
export const EmptyMessage: FC<Props> = ({ title, subtitle, buttonText, onClick, className }) => {
20+
const handleClick = useCallback(() => {
21+
onClick && onClick();
22+
}, [onClick]);
23+
return (
24+
<div
25+
className={classNames(
26+
"w-full flex justify-center mt-2 rounded-xl bg-gray-100 dark:bg-gray-900 px-4 py-14",
27+
className,
28+
)}
29+
>
30+
<div className="flex flex-col justify-center items-center text-center space-y-4">
31+
<Heading2 color="light">{title}</Heading2>
32+
<Subheading className="max-w-md">{subtitle}</Subheading>
33+
{buttonText && <Button onClick={handleClick}>{buttonText}</Button>}
34+
</div>
35+
</div>
36+
);
37+
};

components/dashboard/src/components/Modal.tsx

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44
* See License.AGPL.txt in the project root for license information.
55
*/
66

7-
import { ReactNode, useEffect } from "react";
7+
import { FC, ReactNode, useEffect, useMemo } from "react";
88
import cn from "classnames";
99
import { getGitpodService } from "../service/service";
1010
import { Heading2 } from "./typography/headings";
11+
import Alert from "./Alert";
1112

1213
type CloseModalManner = "esc" | "enter" | "x";
1314

@@ -125,7 +126,7 @@ type ModalBodyProps = {
125126
export const ModalBody = ({ children, hideDivider = false, noScroll = false }: ModalBodyProps) => {
126127
return (
127128
<div
128-
className={cn("border-gray-200 dark:border-gray-800 -mx-6 px-6 ", {
129+
className={cn("relative border-gray-200 dark:border-gray-800 -mx-6 px-6 pb-14", {
129130
"border-t border-b mt-2 py-4": !hideDivider,
130131
"overflow-y-auto": !noScroll,
131132
})}
@@ -136,8 +137,47 @@ export const ModalBody = ({ children, hideDivider = false, noScroll = false }: M
136137
};
137138

138139
type ModalFooterProps = {
140+
error?: string;
141+
warning?: string;
139142
children: ReactNode;
140143
};
141-
export const ModalFooter = ({ children }: ModalFooterProps) => {
142-
return <div className="flex justify-end mt-6 space-x-2">{children}</div>;
144+
export const ModalFooter: FC<ModalFooterProps> = ({ error, warning, children }) => {
145+
// Inlining these to ensure error band covers modal left/right borders
146+
const alertStyles = useMemo(() => ({ marginLeft: "-25px", marginRight: "-25px" }), []);
147+
148+
const hasAlert = error || warning;
149+
150+
return (
151+
<div className="relative">
152+
{hasAlert && (
153+
<div className="absolute -top-12 left-0 right-0" style={alertStyles}>
154+
<ModalFooterAlert error={error} warning={warning} />
155+
</div>
156+
)}
157+
<div className="flex justify-end mt-6 space-x-2">{children}</div>
158+
</div>
159+
);
160+
};
161+
162+
type ModalFooterAlertProps = {
163+
error?: string;
164+
warning?: string;
165+
};
166+
const ModalFooterAlert: FC<ModalFooterAlertProps> = ({ error, warning }) => {
167+
if (error) {
168+
return (
169+
<Alert type="danger" rounded={false}>
170+
{error}
171+
</Alert>
172+
);
173+
}
174+
if (warning) {
175+
return (
176+
<Alert type="warning" rounded={false}>
177+
{warning}
178+
</Alert>
179+
);
180+
}
181+
182+
return null;
143183
};

components/dashboard/src/components/UsageBasedBillingConfig.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,10 @@ type PendingStripeSubscription = { pendingSince: number };
2929

3030
interface Props {
3131
attributionId?: string;
32+
hideSubheading?: boolean;
3233
}
3334

34-
export default function UsageBasedBillingConfig({ attributionId }: Props) {
35+
export default function UsageBasedBillingConfig({ attributionId, hideSubheading = false }: Props) {
3536
const location = useLocation();
3637
const currentOrg = useCurrentOrg().data;
3738
const attrId = attributionId ? AttributionId.parse(attributionId) : undefined;
@@ -170,11 +171,13 @@ export default function UsageBasedBillingConfig({ attributionId }: Props) {
170171

171172
return (
172173
<div className="mb-16">
173-
<Subheading>
174-
{attributionId && AttributionId.parse(attributionId)?.kind === "user"
175-
? "Manage billing for your personal account."
176-
: "Manage billing for your organization."}
177-
</Subheading>
174+
{!hideSubheading && (
175+
<Subheading>
176+
{attributionId && AttributionId.parse(attributionId)?.kind === "user"
177+
? "Manage billing for your personal account."
178+
: "Manage billing for your organization."}
179+
</Subheading>
180+
)}
178181
<div className="max-w-xl flex flex-col">
179182
{errorMessage && (
180183
<Alert className="max-w-xl mt-2" closable={false} showIcon={true} type="error">

components/dashboard/src/teams/GitIntegrationsPage.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,14 @@ export default function GitAuth() {
2424
export const OrgSettingsPageWrapper: FunctionComponent = ({ children }) => {
2525
const currentOrg = useCurrentOrg();
2626

27+
const title = "Git Auth";
28+
const subtitle = "Configure Git Auth for GitLab, or Github.";
29+
2730
// Render as much of the page as we can in a loading state to avoid content shift
2831
if (currentOrg.isLoading) {
2932
return (
3033
<div className="w-full">
31-
<Header title="Organization Settings" subtitle="Manage your organization's settings." />
34+
<Header title={title} subtitle={subtitle} />
3235
<div className="w-full">
3336
<SpinnerLoader />
3437
</div>
@@ -40,5 +43,9 @@ export const OrgSettingsPageWrapper: FunctionComponent = ({ children }) => {
4043
return <Redirect to={"/"} />;
4144
}
4245

43-
return <OrgSettingsPage>{children}</OrgSettingsPage>;
46+
return (
47+
<OrgSettingsPage title={title} subtitle={subtitle}>
48+
{children}
49+
</OrgSettingsPage>
50+
);
4451
};

components/dashboard/src/teams/OrgSettingsPage.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ import { useCurrentOrg } from "../data/organizations/orgs-query";
1414
import { getTeamSettingsMenu } from "./TeamSettings";
1515

1616
export interface OrgSettingsPageProps {
17+
title: string;
18+
subtitle: string;
1719
children: React.ReactNode;
1820
}
1921

20-
export function OrgSettingsPage({ children }: OrgSettingsPageProps) {
22+
export function OrgSettingsPage({ title, subtitle, children }: OrgSettingsPageProps) {
2123
const org = useCurrentOrg();
2224
const { oidcServiceEnabled, orgGitAuthProviders } = useFeatureFlags();
2325

@@ -32,9 +34,6 @@ export function OrgSettingsPage({ children }: OrgSettingsPageProps) {
3234
[oidcServiceEnabled, orgGitAuthProviders, org.data],
3335
);
3436

35-
const title = "Organization Settings";
36-
const subtitle = "Manage your organization's settings.";
37-
3837
// Render as much of the page as we can in a loading state to avoid content shift
3938
if (org.isLoading) {
4039
return (

components/dashboard/src/teams/SSO.tsx

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,16 @@ import copy from "../images/copy.svg";
1717
import exclamation from "../images/exclamation.svg";
1818
import { OrgSettingsPage } from "./OrgSettingsPage";
1919
import { Heading2, Subheading } from "../components/typography/headings";
20+
import { EmptyMessage } from "../components/EmptyMessage";
2021

2122
export default function SSO() {
2223
const currentOrg = useCurrentOrg();
2324

24-
return <OrgSettingsPage>{currentOrg.data && <OIDCClients organizationId={currentOrg.data?.id} />}</OrgSettingsPage>;
25+
return (
26+
<OrgSettingsPage title="SSO" subtitle="Configure OpenID Connect (OIDC) single sign-on.">
27+
{currentOrg.data && <OIDCClients organizationId={currentOrg.data?.id} />}
28+
</OrgSettingsPage>
29+
);
2530
}
2631

2732
function OIDCClients(props: { organizationId: string }) {
@@ -85,11 +90,10 @@ function OIDCClients(props: { organizationId: string }) {
8590
{modal?.mode === "edit" && <></>}
8691
{modal?.mode === "delete" && <></>}
8792

93+
<Heading2>OpenID Connect clients</Heading2>
94+
<Subheading>Configure single sign-on for your organization.</Subheading>
95+
8896
<div className="flex items-start sm:justify-between mb-2">
89-
<div>
90-
<Heading2>Single sign sign-on with OIDC</Heading2>
91-
<Subheading>Setup SSO for your organization.</Subheading>
92-
</div>
9397
{clientConfigs.length !== 0 ? (
9498
<div className="mt-3 flex mt-0">
9599
<button onClick={() => setModal({ mode: "new" })} className="ml-2">
@@ -100,18 +104,12 @@ function OIDCClients(props: { organizationId: string }) {
100104
</div>
101105

102106
{clientConfigs.length === 0 && (
103-
<div className="w-full flex h-80 mt-2 rounded-xl bg-gray-100 dark:bg-gray-900">
104-
<div className="m-auto text-center">
105-
<h3 className="self-center text-gray-500 dark:text-gray-400 mb-4">No OIDC Clients</h3>
106-
<div className="text-gray-500 mb-6">
107-
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor
108-
invidunt ut labore et dolore magna aliquyam
109-
</div>
110-
<button className="self-center" onClick={() => setModal({ mode: "new" })}>
111-
New OIDC Client
112-
</button>
113-
</div>
114-
</div>
107+
<EmptyMessage
108+
title="No OIDC providers"
109+
subtitle="Enable single sign-on for your organization using an external identity provider (IdP) service that supports the OpenID Connect (OIDC) standard, such as Google."
110+
buttonText="New OIDC Client"
111+
onClick={() => setModal({ mode: "new" })}
112+
/>
115113
)}
116114

117115
<ItemsList className="pt-6">

components/dashboard/src/teams/TeamBilling.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ type PendingPlan = Plan & { pendingSince: number };
2727

2828
export default function TeamBillingPage() {
2929
return (
30-
<OrgSettingsPage>
30+
<OrgSettingsPage title={"Organization Billing"} subtitle="Manage billing for your organization.">
3131
<TeamBilling />
3232
</OrgSettingsPage>
3333
);

components/dashboard/src/teams/TeamSettings.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export function getTeamSettingsMenu(params: {
3737
}
3838
if (orgGitAuthProviders) {
3939
result.push({
40-
title: "Git Integrations",
40+
title: "Git Auth",
4141
link: [`/settings/git`],
4242
});
4343
}
@@ -109,7 +109,7 @@ export default function TeamSettings() {
109109

110110
return (
111111
<>
112-
<OrgSettingsPage>
112+
<OrgSettingsPage title="Organization Settings" subtitle="Manage your organization's settings.">
113113
<Heading2>Organization Name</Heading2>
114114
<Subheading className="max-w-2xl">
115115
This is your organization's visible name within Gitpod. For example, the name of your company.

components/dashboard/src/teams/TeamUsageBasedBilling.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
*/
66

77
import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution";
8-
import { Heading2 } from "../components/typography/headings";
98
import { BillingMode } from "@gitpod/gitpod-protocol/lib/billing-mode";
109
import UsageBasedBillingConfig from "../components/UsageBasedBillingConfig";
1110
import { useCurrentOrg } from "../data/organizations/orgs-query";
@@ -19,8 +18,10 @@ export default function TeamUsageBasedBilling() {
1918

2019
return (
2120
<>
22-
<Heading2>Organization Billing</Heading2>
23-
<UsageBasedBillingConfig attributionId={org && AttributionId.render({ kind: "team", teamId: org.id })} />
21+
<UsageBasedBillingConfig
22+
hideSubheading
23+
attributionId={org && AttributionId.render({ kind: "team", teamId: org.id })}
24+
/>
2425
</>
2526
);
2627
}

components/dashboard/src/teams/git-integrations/GitIntegrationListItem.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export const GitIntegrationListItem: FunctionComponent<Props> = ({ provider }) =
4747
return (
4848
<>
4949
<Item className="h-16">
50-
<ItemFieldIcon>
50+
<ItemFieldIcon className="w-1/12">
5151
<div
5252
className={
5353
"rounded-full w-3 h-3 text-sm align-middle m-auto " +
@@ -57,13 +57,13 @@ export const GitIntegrationListItem: FunctionComponent<Props> = ({ provider }) =
5757
&nbsp;
5858
</div>
5959
</ItemFieldIcon>
60-
<ItemField className="w-3/12 flex flex-col my-auto">
60+
<ItemField className="w-5/12 flex items-center">
6161
<span className="font-medium truncate overflow-ellipsis">{provider.type}</span>
6262
</ItemField>
63-
<ItemField className="w-7/12 flex flex-col my-auto">
63+
<ItemField className="w-5/12 flex items-center">
6464
<span className="my-auto truncate text-gray-500 overflow-ellipsis">{provider.host}</span>
6565
</ItemField>
66-
<ItemFieldContextMenu menuEntries={menuEntries} />
66+
<ItemFieldContextMenu className="w-1/12" menuEntries={menuEntries} />
6767
</Item>
6868
{showDeleteConfirmation && (
6969
<ConfirmationModal

0 commit comments

Comments
 (0)