Skip to content

Org Settings and Modal alert updates #16883

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Mar 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 35 additions & 39 deletions components/dashboard/src/components/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
* See License.AGPL.txt in the project root for license information.
*/

import { FC, ReactNode, useEffect, useMemo } from "react";
import { FC, ReactNode, useEffect } from "react";
import cn from "classnames";
import { getGitpodService } from "../service/service";
import { Heading2 } from "./typography/headings";
import Alert from "./Alert";
import Alert, { AlertProps } from "./Alert";
import "./modal.css";
import classNames from "classnames";

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

Expand Down Expand Up @@ -126,7 +128,7 @@ type ModalBodyProps = {
export const ModalBody = ({ children, hideDivider = false, noScroll = false }: ModalBodyProps) => {
return (
<div
className={cn("relative border-gray-200 dark:border-gray-800 -mx-6 px-6 pb-14", {
className={cn("relative border-gray-200 dark:border-gray-800 -mx-6 px-6 pb-6", {
"border-t border-b mt-2 py-4": !hideDivider,
"overflow-y-auto": !noScroll,
})}
Expand All @@ -137,47 +139,41 @@ export const ModalBody = ({ children, hideDivider = false, noScroll = false }: M
};

type ModalFooterProps = {
error?: string;
warning?: string;
alert?: ReactNode;
children: ReactNode;
};
export const ModalFooter: FC<ModalFooterProps> = ({ 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;

export const ModalFooter: FC<ModalFooterProps> = ({ alert, children }) => {
return (
<div className="relative">
{hasAlert && (
<div className="absolute bottom-12 left-0 right-0" style={alertStyles}>
<ModalFooterAlert error={error} warning={warning} />
</div>
)}
<div className="flex justify-end mt-6 space-x-2">{children}</div>
</div>
<>
{alert}
<div
className={classNames(
// causes footer to show up on top of alert
"relative",
// make as wide as the modal so it covers the alert
"-mx-6 -mb-6 p-6",
// apply the same bg and rounded corners as the modal
"bg-white dark:bg-gray-900 rounded-b-xl",
)}
>
<div className="flex justify-end space-x-2">{children}</div>
</div>
</>
);
};

type ModalFooterAlertProps = {
error?: string;
warning?: string;
};
const ModalFooterAlert: FC<ModalFooterAlertProps> = ({ error, warning }) => {
if (error) {
return (
<Alert type="danger" rounded={false}>
{error}
</Alert>
);
}
if (warning) {
return (
<Alert type="warning" rounded={false}>
{warning}
// Wrapper around Alert to ensure it's used correctly in a Modal
export const ModalFooterAlert: FC<AlertProps> = ({ closable = true, children, ...alertProps }) => {
return (
<div
className={classNames({
"gp-modal-footer-alert border-b": !closable,
"gp-modal-footer-alert_animate absolute": closable,
})}
>
<Alert rounded={false} closable={closable} {...alertProps}>
{children}
</Alert>
);
}

return null;
</div>
);
};
43 changes: 43 additions & 0 deletions components/dashboard/src/components/modal.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* 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.
*/

@layer components {
.gp-modal-footer-alert {
margin-top: -1px;
margin-left: -25px;
margin-right: -25px;
}
.gp-modal-footer-alert_animate {
@apply bottom-2;
margin-left: 0px;
margin-right: 0px;
left: -1px;
right: -1px;
/* Start w/ a fixed height to keep it smaller than the footer */
max-height: 70px;
overflow: hidden;
animation: showModalFooterAlert 0.5s ease-out forwards;
}

/* Animates alert from behind footer */
@keyframes showModalFooterAlert {
0% {
@apply bottom-2;
}

/* Swaps max-height and overflow so it's not fixed height anymore */
50% {
max-height: inherit;
overflow: visible;
}

100% {
bottom: 85px;
max-height: inherit;
overflow: visible;
}
}
}
2 changes: 1 addition & 1 deletion components/dashboard/src/teams/GitIntegrationsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { OrgSettingsPage } from "./OrgSettingsPage";

export default function GitAuthPage() {
return (
<OrgSettingsPage title="Git Auth" subtitle="Configure Git Auth for GitLab or Github.">
<OrgSettingsPage>
<GitIntegrations />
</OrgSettingsPage>
);
Expand Down
7 changes: 4 additions & 3 deletions components/dashboard/src/teams/OrgSettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,10 @@ import { useCurrentOrg } from "../data/organizations/orgs-query";
import { getTeamSettingsMenu } from "./TeamSettings";

export interface OrgSettingsPageProps {
title: string;
subtitle: string;
children: React.ReactNode;
}

export function OrgSettingsPage({ title, subtitle, children }: OrgSettingsPageProps) {
export function OrgSettingsPage({ children }: OrgSettingsPageProps) {
const org = useCurrentOrg();
const { oidcServiceEnabled, orgGitAuthProviders } = useFeatureFlags();

Expand All @@ -34,6 +32,9 @@ export function OrgSettingsPage({ title, subtitle, children }: OrgSettingsPagePr
[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 (
Expand Down
2 changes: 1 addition & 1 deletion components/dashboard/src/teams/SSO.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { OIDCClients } from "./sso/OIDCClients";

export default function SSO() {
return (
<OrgSettingsPage title="SSO" subtitle="Configure OpenID Connect (OIDC) single sign-on.">
<OrgSettingsPage>
<OIDCClients />
</OrgSettingsPage>
);
Expand Down
2 changes: 1 addition & 1 deletion components/dashboard/src/teams/TeamBilling.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ type PendingPlan = Plan & { pendingSince: number };

export default function TeamBillingPage() {
return (
<OrgSettingsPage title={"Organization Billing"} subtitle="Manage billing for your organization.">
<OrgSettingsPage>
<TeamBilling />
</OrgSettingsPage>
);
Expand Down
2 changes: 1 addition & 1 deletion components/dashboard/src/teams/TeamSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export default function TeamSettings() {

return (
<>
<OrgSettingsPage title="Organization Settings" subtitle="Manage your organization's settings.">
<OrgSettingsPage>
<Heading2>Organization Name</Heading2>
<Subheading className="max-w-2xl">
This is your organization's visible name within Gitpod. For example, the name of your company.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export const GitIntegrationListItem: FunctionComponent<Props> = ({ provider }) =
return (
<>
<Item className="h-16">
<ItemFieldIcon className="w-1/12">
<ItemFieldIcon>
<div
className={
"rounded-full w-3 h-3 text-sm align-middle m-auto " +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@

import { AuthProviderEntry } from "@gitpod/gitpod-protocol";
import { FunctionComponent, useCallback, useMemo, useState } from "react";
import Alert from "../../components/Alert";
import { Button } from "../../components/Button";
import { InputField } from "../../components/forms/InputField";
import { SelectInputField } from "../../components/forms/SelectInputField";
import { TextInputField } from "../../components/forms/TextInputField";
import { InputWithCopy } from "../../components/InputWithCopy";
import Modal, { ModalBody, ModalFooter, ModalHeader } from "../../components/Modal";
import Modal, { ModalBody, ModalFooter, ModalFooterAlert, ModalHeader } from "../../components/Modal";
import { useInvalidateOrgAuthProvidersQuery } from "../../data/auth-providers/org-auth-providers-query";
import { useUpsertOrgAuthProviderMutation } from "../../data/auth-providers/upsert-org-auth-provider-mutation";
import { useCurrentOrg } from "../../data/organizations/orgs-query";
Expand Down Expand Up @@ -238,8 +239,20 @@ export const GitIntegrationModal: FunctionComponent<Props> = (props) => {
</div>
</ModalBody>
<ModalFooter
error={errorMessage}
warning={!isNew && savedProvider?.status !== "verified" ? "You need to activate this integration." : ""}
alert={
<>
{errorMessage ? (
<ModalFooterAlert type="danger">{errorMessage}</ModalFooterAlert>
) : (
!isNew &&
savedProvider?.status !== "verified" && (
<ModalFooterAlert type="warning" closable={false}>
You need to activate this integration.
</ModalFooterAlert>
)
)}
</>
}
>
<Button type="secondary" onClick={props.onClose}>
Cancel
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

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";

Expand All @@ -17,11 +16,5 @@ export const GitIntegrations: FunctionComponent = () => {
return <SpinnerLoader />;
}

return (
<div>
<Heading2>Git Auth configurations</Heading2>
<Subheading>Configure Git Auth for your organization.</Subheading>
<GitIntegrationsList providers={data || []} />
</div>
);
return <GitIntegrationsList providers={data || []} />;
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import { AuthProviderEntry } from "@gitpod/gitpod-protocol";
import { FunctionComponent, useCallback, useState } from "react";
import { Button } from "../../components/Button";
import { EmptyMessage } from "../../components/EmptyMessage";
import { Item, ItemField, ItemsList } from "../../components/ItemsList";
import { Item, ItemField, ItemFieldIcon, ItemsList } from "../../components/ItemsList";
import { Heading2, Subheading } from "../../components/typography/headings";
import { GitIntegrationListItem } from "./GitIntegrationListItem";
import { GitIntegrationModal } from "./GitIntegrationModal";

Expand All @@ -23,6 +24,21 @@ export const GitIntegrationsList: FunctionComponent<Props> = ({ providers }) =>

return (
<>
<div className="flex flex-col space-y-2 md:flex-row md:items-start md:justify-between md:space-y-0">
<div>
<Heading2>Git Auth configurations</Heading2>
<Subheading>Configure Git Auth for your organization.</Subheading>
</div>

{providers.length !== 0 ? (
<div className="">
<Button className="whitespace-nowrap" onClick={onCreate}>
New Integration
</Button>
</div>
) : null}
</div>

{providers.length === 0 ? (
<EmptyMessage
title="No Git Auth configurations"
Expand All @@ -31,22 +47,16 @@ export const GitIntegrationsList: FunctionComponent<Props> = ({ providers }) =>
onClick={onCreate}
/>
) : (
<>
<div className="mt-3">
<Button onClick={onCreate}>New Integration</Button>
</div>

<ItemsList className="pt-6">
<Item header={true}>
<ItemField className="w-1/12"> </ItemField>
<ItemField className="w-5/12">Provider Type</ItemField>
<ItemField className="w-6/12">Host Name</ItemField>
</Item>
{providers.map((p) => (
<GitIntegrationListItem key={p.id} provider={p} />
))}
</ItemsList>
</>
<ItemsList className="pt-6">
<Item header={true}>
<ItemFieldIcon />
<ItemField className="w-5/12">Provider Type</ItemField>
<ItemField className="w-6/12">Host Name</ItemField>
</Item>
{providers.map((p) => (
<GitIntegrationListItem key={p.id} provider={p} />
))}
</ItemsList>
)}
{showCreateModal && <GitIntegrationModal onClose={hideModal} />}
</>
Expand Down
22 changes: 18 additions & 4 deletions components/dashboard/src/teams/sso/OIDCClientConfigModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { Button } from "../../components/Button";
import { InputField } from "../../components/forms/InputField";
import { TextInputField } from "../../components/forms/TextInputField";
import { InputWithCopy } from "../../components/InputWithCopy";
import Modal, { ModalBody, ModalFooter, ModalHeader } from "../../components/Modal";
import Modal, { ModalBody, ModalFooter, ModalFooterAlert, ModalHeader } from "../../components/Modal";
import { useUpsertOIDCClientMutation } from "../../data/oidc-clients/upsert-oidc-client-mutation";
import { useCurrentOrg } from "../../data/organizations/orgs-query";
import { useOnBlurError } from "../../hooks/use-onblur-error";
Expand Down Expand Up @@ -89,8 +89,6 @@ export const OIDCClientConfigModal: FC<Props> = ({ clientConfig, onClose }) => {
}
}, [clientConfig?.id, clientId, clientSecret, isNew, isValid, issuer, onClose, org, upsertClientConfig]);

const errorMessage = upsertClientConfig.isError ? "There was a problem saving your configuration." : "";

return (
<Modal
visible
Expand Down Expand Up @@ -136,7 +134,9 @@ export const OIDCClientConfigModal: FC<Props> = ({ clientConfig, onClose }) => {
onChange={setClientSecret}
/>
</ModalBody>
<ModalFooter error={errorMessage}>
<ModalFooter
alert={upsertClientConfig.isError ? <SaveErrorAlert error={upsertClientConfig.error as Error} /> : null}
>
<Button type="secondary" onClick={onClose}>
Cancel
</Button>
Expand All @@ -147,3 +147,17 @@ export const OIDCClientConfigModal: FC<Props> = ({ clientConfig, onClose }) => {
</Modal>
);
};

type SaveErrorMessageProps = {
error?: Error;
};
const SaveErrorAlert: FC<SaveErrorMessageProps> = ({ error }) => {
const message = error?.message || "";

return (
<ModalFooterAlert type="danger">
<span>There was a problem saving your configuration.</span>
{message && <div className="leading-4 text-xs font-mono">{message}</div>}
</ModalFooterAlert>
);
};
Loading