diff --git a/components/dashboard/src/components/Alert.tsx b/components/dashboard/src/components/Alert.tsx index fa06fdc7686d9c..20d46aed4cd986 100644 --- a/components/dashboard/src/components/Alert.tsx +++ b/components/dashboard/src/components/Alert.tsx @@ -10,6 +10,7 @@ import { ReactComponent as Exclamation2 } from "../images/exclamation2.svg"; import { ReactComponent as InfoSvg } from "../images/info.svg"; import { ReactComponent as XSvg } from "../images/x.svg"; import { ReactComponent as Check } from "../images/check-circle.svg"; +import classNames from "classnames"; export type AlertType = // Green @@ -20,6 +21,8 @@ export type AlertType = | "info" // Red | "error" + // Dark Red + | "danger" // Blue | "message"; @@ -32,6 +35,7 @@ export interface AlertProps { onClose?: () => void; showIcon?: boolean; icon?: React.ReactNode; + rounded?: boolean; children?: React.ReactNode; } @@ -73,6 +77,12 @@ const infoMap: Record = { icon: , iconColor: "text-red-400", }, + danger: { + bgCls: "bg-red-600 dark:bg-red-600", + txtCls: "text-white", + icon: , + iconColor: "filter-brightness-10", + }, }; export default function Alert(props: AlertProps) { @@ -84,11 +94,16 @@ export default function Alert(props: AlertProps) { const info = infoMap[type]; const showIcon = props.showIcon ?? true; const light = props.light ?? false; + const rounded = props.rounded ?? true; return (
{showIcon && {props.icon ?? info.icon}} {props.children} diff --git a/components/dashboard/src/components/EmptyMessage.tsx b/components/dashboard/src/components/EmptyMessage.tsx new file mode 100644 index 00000000000000..f482e0d53a9e7e --- /dev/null +++ b/components/dashboard/src/components/EmptyMessage.tsx @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2023 Gitpod GmbH. All rights reserved. + * Licensed under the GNU Affero General Public License (AGPL). + * See License.AGPL.txt in the project root for license information. + */ + +import classNames from "classnames"; +import { FC, useCallback } from "react"; +import { Button } from "./Button"; +import { Heading2, Subheading } from "./typography/headings"; + +type Props = { + title: string; + subtitle?: string; + buttonText?: string; + onClick?: () => void; + className?: string; +}; +export const EmptyMessage: FC = ({ title, subtitle, buttonText, onClick, className }) => { + const handleClick = useCallback(() => { + onClick && onClick(); + }, [onClick]); + return ( +
+
+ {title} + {subtitle} + {buttonText && } +
+
+ ); +}; diff --git a/components/dashboard/src/components/Modal.tsx b/components/dashboard/src/components/Modal.tsx index 6e8b7ae8e1975f..8b9caa668c9e4b 100644 --- a/components/dashboard/src/components/Modal.tsx +++ b/components/dashboard/src/components/Modal.tsx @@ -4,10 +4,11 @@ * See License.AGPL.txt in the project root for license information. */ -import { ReactNode, useEffect } from "react"; +import { FC, ReactNode, useEffect, useMemo } from "react"; import cn from "classnames"; import { getGitpodService } from "../service/service"; import { Heading2 } from "./typography/headings"; +import Alert from "./Alert"; type CloseModalManner = "esc" | "enter" | "x"; @@ -125,7 +126,7 @@ type ModalBodyProps = { export const ModalBody = ({ children, hideDivider = false, noScroll = false }: ModalBodyProps) => { return (
{ - return
{children}
; +export const ModalFooter: FC = ({ error, warning, children }) => { + // Inlining these to ensure error band covers modal left/right borders + const alertStyles = useMemo(() => ({ marginLeft: "-25px", marginRight: "-25px" }), []); + + const hasAlert = error || warning; + + return ( +
+ {hasAlert && ( +
+ +
+ )} +
{children}
+
+ ); +}; + +type ModalFooterAlertProps = { + error?: string; + warning?: string; +}; +const ModalFooterAlert: FC = ({ error, warning }) => { + if (error) { + return ( + + {error} + + ); + } + if (warning) { + return ( + + {warning} + + ); + } + + return null; }; diff --git a/components/dashboard/src/components/UsageBasedBillingConfig.tsx b/components/dashboard/src/components/UsageBasedBillingConfig.tsx index 864eb3705521a7..f368749a5b0427 100644 --- a/components/dashboard/src/components/UsageBasedBillingConfig.tsx +++ b/components/dashboard/src/components/UsageBasedBillingConfig.tsx @@ -29,9 +29,10 @@ type PendingStripeSubscription = { pendingSince: number }; interface Props { attributionId?: string; + hideSubheading?: boolean; } -export default function UsageBasedBillingConfig({ attributionId }: Props) { +export default function UsageBasedBillingConfig({ attributionId, hideSubheading = false }: Props) { const location = useLocation(); const currentOrg = useCurrentOrg().data; const attrId = attributionId ? AttributionId.parse(attributionId) : undefined; @@ -170,11 +171,13 @@ export default function UsageBasedBillingConfig({ attributionId }: Props) { return (
- - {attributionId && AttributionId.parse(attributionId)?.kind === "user" - ? "Manage billing for your personal account." - : "Manage billing for your organization."} - + {!hideSubheading && ( + + {attributionId && AttributionId.parse(attributionId)?.kind === "user" + ? "Manage billing for your personal account." + : "Manage billing for your organization."} + + )}
{errorMessage && ( diff --git a/components/dashboard/src/teams/GitIntegrationsPage.tsx b/components/dashboard/src/teams/GitIntegrationsPage.tsx index b14cabc570e39e..9d24ec9e35bdf0 100644 --- a/components/dashboard/src/teams/GitIntegrationsPage.tsx +++ b/components/dashboard/src/teams/GitIntegrationsPage.tsx @@ -24,11 +24,14 @@ export default function GitAuth() { export const OrgSettingsPageWrapper: FunctionComponent = ({ children }) => { const currentOrg = useCurrentOrg(); + const title = "Git Auth"; + const subtitle = "Configure Git Auth for GitLab, or Github."; + // Render as much of the page as we can in a loading state to avoid content shift if (currentOrg.isLoading) { return (
-
+
@@ -40,5 +43,9 @@ export const OrgSettingsPageWrapper: FunctionComponent = ({ children }) => { return ; } - return {children}; + return ( + + {children} + + ); }; diff --git a/components/dashboard/src/teams/OrgSettingsPage.tsx b/components/dashboard/src/teams/OrgSettingsPage.tsx index 52239da28158c1..4ce86742ff0416 100644 --- a/components/dashboard/src/teams/OrgSettingsPage.tsx +++ b/components/dashboard/src/teams/OrgSettingsPage.tsx @@ -14,10 +14,12 @@ import { useCurrentOrg } from "../data/organizations/orgs-query"; import { getTeamSettingsMenu } from "./TeamSettings"; export interface OrgSettingsPageProps { + title: string; + subtitle: string; children: React.ReactNode; } -export function OrgSettingsPage({ children }: OrgSettingsPageProps) { +export function OrgSettingsPage({ title, subtitle, children }: OrgSettingsPageProps) { const org = useCurrentOrg(); const { oidcServiceEnabled, orgGitAuthProviders } = useFeatureFlags(); @@ -32,9 +34,6 @@ export function OrgSettingsPage({ children }: OrgSettingsPageProps) { [oidcServiceEnabled, orgGitAuthProviders, org.data], ); - const title = "Organization Settings"; - const subtitle = "Manage your organization's settings."; - // Render as much of the page as we can in a loading state to avoid content shift if (org.isLoading) { return ( diff --git a/components/dashboard/src/teams/SSO.tsx b/components/dashboard/src/teams/SSO.tsx index 231cdcdcc6a926..5e4e0602248ccc 100644 --- a/components/dashboard/src/teams/SSO.tsx +++ b/components/dashboard/src/teams/SSO.tsx @@ -17,11 +17,16 @@ import copy from "../images/copy.svg"; import exclamation from "../images/exclamation.svg"; import { OrgSettingsPage } from "./OrgSettingsPage"; import { Heading2, Subheading } from "../components/typography/headings"; +import { EmptyMessage } from "../components/EmptyMessage"; export default function SSO() { const currentOrg = useCurrentOrg(); - return {currentOrg.data && }; + return ( + + {currentOrg.data && } + + ); } function OIDCClients(props: { organizationId: string }) { @@ -85,11 +90,10 @@ function OIDCClients(props: { organizationId: string }) { {modal?.mode === "edit" && <>} {modal?.mode === "delete" && <>} + OpenID Connect clients + Configure single sign-on for your organization. +
-
- Single sign sign-on with OIDC - Setup SSO for your organization. -
{clientConfigs.length !== 0 ? (
{clientConfigs.length === 0 && ( -
-
-

No OIDC Clients

-
- Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor - invidunt ut labore et dolore magna aliquyam -
- -
-
+ setModal({ mode: "new" })} + /> )} diff --git a/components/dashboard/src/teams/TeamBilling.tsx b/components/dashboard/src/teams/TeamBilling.tsx index e47f1635d50392..3160514ac166b5 100644 --- a/components/dashboard/src/teams/TeamBilling.tsx +++ b/components/dashboard/src/teams/TeamBilling.tsx @@ -27,7 +27,7 @@ type PendingPlan = Plan & { pendingSince: number }; export default function TeamBillingPage() { return ( - + ); diff --git a/components/dashboard/src/teams/TeamSettings.tsx b/components/dashboard/src/teams/TeamSettings.tsx index ac51b45412d447..71450fd45835fb 100644 --- a/components/dashboard/src/teams/TeamSettings.tsx +++ b/components/dashboard/src/teams/TeamSettings.tsx @@ -37,7 +37,7 @@ export function getTeamSettingsMenu(params: { } if (orgGitAuthProviders) { result.push({ - title: "Git Integrations", + title: "Git Auth", link: [`/settings/git`], }); } @@ -109,7 +109,7 @@ export default function TeamSettings() { return ( <> - + Organization Name This is your organization's visible name within Gitpod. For example, the name of your company. diff --git a/components/dashboard/src/teams/TeamUsageBasedBilling.tsx b/components/dashboard/src/teams/TeamUsageBasedBilling.tsx index 89c94710ccdb9c..a3f66b195dfef1 100644 --- a/components/dashboard/src/teams/TeamUsageBasedBilling.tsx +++ b/components/dashboard/src/teams/TeamUsageBasedBilling.tsx @@ -5,7 +5,6 @@ */ import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution"; -import { Heading2 } from "../components/typography/headings"; import { BillingMode } from "@gitpod/gitpod-protocol/lib/billing-mode"; import UsageBasedBillingConfig from "../components/UsageBasedBillingConfig"; import { useCurrentOrg } from "../data/organizations/orgs-query"; @@ -19,8 +18,10 @@ export default function TeamUsageBasedBilling() { return ( <> - Organization Billing - + ); } diff --git a/components/dashboard/src/teams/git-integrations/GitIntegrationListItem.tsx b/components/dashboard/src/teams/git-integrations/GitIntegrationListItem.tsx index b2f364a436c782..cda38a56f6136a 100644 --- a/components/dashboard/src/teams/git-integrations/GitIntegrationListItem.tsx +++ b/components/dashboard/src/teams/git-integrations/GitIntegrationListItem.tsx @@ -47,7 +47,7 @@ export const GitIntegrationListItem: FunctionComponent = ({ provider }) = return ( <> - +
= ({ provider }) =  
- + {provider.type} - + {provider.host} - +
{showDeleteConfirmation && ( = (props) => { console.error("no current team selected"); return; } + // Set a saving state and clear any error message setSavingProvider(true); setErrorMessage(undefined); @@ -193,9 +193,6 @@ export const GitIntegrationModal: FunctionComponent = (props) => { > {isNew ? "New Git Integration" : "Git Integration"} - {!isNew && savedProvider?.status !== "verified" && ( - You need to activate this integration. - )}
Configure an integration with a self-managed instance of GitLab, GitHub, or Bitbucket.
@@ -239,22 +236,14 @@ export const GitIntegrationModal: FunctionComponent = (props) => { onBlur={clientSecretOnBlur} />
- - {errorMessage && ( -
- exclamation mark - {errorMessage} -
- )} - - + ); diff --git a/components/dashboard/src/teams/git-integrations/GitIntegrations.tsx b/components/dashboard/src/teams/git-integrations/GitIntegrations.tsx index 3557b8a09e838e..7c777d8a5f69c1 100644 --- a/components/dashboard/src/teams/git-integrations/GitIntegrations.tsx +++ b/components/dashboard/src/teams/git-integrations/GitIntegrations.tsx @@ -6,25 +6,11 @@ import { FunctionComponent } from "react"; import { SpinnerLoader } from "../../components/Loader"; -import { Heading2, Subheading } from "../../components/typography/headings"; import { useOrgAuthProvidersQuery } from "../../data/auth-providers/org-auth-providers-query"; import { GitIntegrationsList } from "./GitIntegrationsList"; export const GitIntegrations: FunctionComponent = () => { const { data, isLoading } = useOrgAuthProvidersQuery(); - return ( -
-
-
- Git Integrations - - Manage Git integrations for self-managed instances of GitLab, GitHub, or Bitbucket. - -
-
- - {isLoading ? : } -
- ); + return
{isLoading ? : }
; }; diff --git a/components/dashboard/src/teams/git-integrations/GitIntegrationsList.tsx b/components/dashboard/src/teams/git-integrations/GitIntegrationsList.tsx index e6f51a3943f769..7aa20efb9cfb98 100644 --- a/components/dashboard/src/teams/git-integrations/GitIntegrationsList.tsx +++ b/components/dashboard/src/teams/git-integrations/GitIntegrationsList.tsx @@ -6,8 +6,9 @@ import { AuthProviderEntry } from "@gitpod/gitpod-protocol"; import { FunctionComponent, useCallback, useState } from "react"; -import { ItemsList } from "../../components/ItemsList"; -import { Heading2 } from "../../components/typography/headings"; +import { Button } from "../../components/Button"; +import { EmptyMessage } from "../../components/EmptyMessage"; +import { Item, ItemField, ItemsList } from "../../components/ItemsList"; import { GitIntegrationListItem } from "./GitIntegrationListItem"; import { GitIntegrationModal } from "./GitIntegrationModal"; @@ -23,27 +24,24 @@ export const GitIntegrationsList: FunctionComponent = ({ providers }) => return ( <> {providers.length === 0 ? ( -
-
- - No Git Integrations - -
- In addition to the default Git Providers you can authorize with a self-hosted instance of a - provider. -
- -
-
+ ) : ( <>
- +
+ + + Provider Type + Host Name + {providers.map((p) => ( ))}