Skip to content
Snippets Groups Projects
Unverified Commit c1e4d515 authored by Ryan Laurie's avatar Ryan Laurie Committed by GitHub
Browse files

API Keys UI follow-ups (#37618)

* ui followups for api keys

* make auth cards take up less vertical space

* filter groups users
parent d3796f60
No related branches found
No related tags found
No related merge requests found
import { t } from "ttag";
import _ from "underscore";
import { updateIn } from "icepick";
import { LOGIN, LOGIN_GOOGLE } from "metabase/auth/actions";
import {
......@@ -24,54 +25,61 @@ import JwtAuthCard from "./containers/JwtAuthCard";
import SamlAuthCard from "./containers/SamlAuthCard";
PLUGIN_ADMIN_SETTINGS_UPDATES.push(sections =>
updateIn(sections, ["authentication", "settings"], settings => [
...settings,
{
key: "saml-enabled",
description: null,
noHeader: true,
widget: SamlAuthCard,
getHidden: () => !hasPremiumFeature("sso_saml"),
},
{
key: "jwt-enabled",
description: null,
noHeader: true,
widget: JwtAuthCard,
getHidden: () => !hasPremiumFeature("sso_jwt"),
},
{
key: "enable-password-login",
display_name: t`Enable Password Authentication`,
description: t`When enabled, users can additionally log in with email and password.`,
type: "boolean",
getHidden: (_settings, derivedSettings) =>
!hasPremiumFeature("disable_password_login") ||
(!derivedSettings["google-auth-enabled"] &&
!derivedSettings["ldap-enabled"] &&
!derivedSettings["saml-enabled"] &&
!derivedSettings["jwt-enabled"]),
},
{
key: "send-new-sso-user-admin-email?",
display_name: t`Notify admins of new SSO users`,
description: t`When enabled, administrators will receive an email the first time a user uses Single Sign-On.`,
type: "boolean",
getHidden: (_, derivedSettings) =>
!hasAnySsoPremiumFeature() ||
(!derivedSettings["google-auth-enabled"] &&
!derivedSettings["ldap-enabled"] &&
!derivedSettings["saml-enabled"] &&
!derivedSettings["jwt-enabled"]),
},
{
key: "session-timeout",
display_name: t`Session timeout`,
description: t`Time before inactive users are logged out.`,
widget: SessionTimeoutSetting,
getHidden: () => !hasPremiumFeature("session_timeout_config"),
},
]),
updateIn(sections, ["authentication", "settings"], settings => {
const [apiKeySettings, otherSettings] = _.partition(
settings,
s => s.key === "api-keys",
);
return [
...otherSettings,
{
key: "saml-enabled",
description: null,
noHeader: true,
widget: SamlAuthCard,
getHidden: () => !hasPremiumFeature("sso_saml"),
},
{
key: "jwt-enabled",
description: null,
noHeader: true,
widget: JwtAuthCard,
getHidden: () => !hasPremiumFeature("sso_jwt"),
},
...apiKeySettings,
{
key: "enable-password-login",
display_name: t`Enable Password Authentication`,
description: t`When enabled, users can additionally log in with email and password.`,
type: "boolean",
getHidden: (_settings, derivedSettings) =>
!hasPremiumFeature("disable_password_login") ||
(!derivedSettings["google-auth-enabled"] &&
!derivedSettings["ldap-enabled"] &&
!derivedSettings["saml-enabled"] &&
!derivedSettings["jwt-enabled"]),
},
{
key: "send-new-sso-user-admin-email?",
display_name: t`Notify admins of new SSO users`,
description: t`When enabled, administrators will receive an email the first time a user uses Single Sign-On.`,
type: "boolean",
getHidden: (_, derivedSettings) =>
!hasAnySsoPremiumFeature() ||
(!derivedSettings["google-auth-enabled"] &&
!derivedSettings["ldap-enabled"] &&
!derivedSettings["saml-enabled"] &&
!derivedSettings["jwt-enabled"]),
},
{
key: "session-timeout",
display_name: t`Session timeout`,
description: t`Time before inactive users are logged out.`,
widget: SessionTimeoutSetting,
getHidden: () => !hasPremiumFeature("session_timeout_config"),
},
];
}),
);
PLUGIN_ADMIN_SETTINGS_UPDATES.push(sections => ({
......
......@@ -60,10 +60,14 @@ function GroupMembersTable({
onPreviousPage,
reload,
}: GroupMembersTableProps) {
const { loading, value: apiKeys } = useAsync(
() => ApiKeysApi.list() as Promise<ApiKey[]>,
[group.id],
);
const { loading, value: apiKeys } = useAsync(async () => {
const apiKeys = await (ApiKeysApi.list() as Promise<ApiKey[]>);
const filteredApiKeys = apiKeys?.filter(
(apiKey: ApiKey) => apiKey.group.id === group.id,
);
return filteredApiKeys ?? [];
}, [group.id]);
// you can't remove people from Default and you can't remove the last user from Admin
const isCurrentUser = ({ id }: Partial<IUser>) => id === currentUserId;
......
import { t } from "ttag";
import { useEffect, useState } from "react";
import { Link } from "react-router";
import Button from "metabase/core/components/Button";
import { ApiKeysApi } from "metabase/services";
import {
CardBadge,
CardDescription,
CardHeader,
CardRoot,
CardTitle,
} from "./AuthCard/AuthCard.styled";
import { AuthCardBody } from "./AuthCard/AuthCard";
export const ApiKeysAuthCard = () => {
const [keyCount, setKeyCount] = useState(0);
......@@ -19,20 +12,16 @@ export const ApiKeysAuthCard = () => {
}, []);
const isConfigured = keyCount > 0;
return (
<CardRoot>
<CardHeader>
<CardTitle>{t`API Keys`}</CardTitle>
{isConfigured && (
<CardBadge isEnabled data-testid="card-badge">
{keyCount === 1 ? t`1 API Key` : t`${keyCount} API Keys`}
</CardBadge>
)}
</CardHeader>
<CardDescription>{t`Create keys to authenticate API calls.`}</CardDescription>
<Button as={Link} to={`/admin/settings/authentication/api-keys`}>
{isConfigured ? t`Manage` : t`Set up`}
</Button>
</CardRoot>
<AuthCardBody
type="api-keys"
title={t`API Keys`}
description={t`Create keys to authenticate API calls.`}
isConfigured={isConfigured}
isEnabled
badgeText={keyCount === 1 ? t`1 API Key` : t`${keyCount} API Keys`}
buttonText={isConfigured ? t`Manage` : t`Set up`}
/>
);
};
......@@ -3,25 +3,22 @@ import { color } from "metabase/lib/colors";
import EntityMenu from "metabase/components/EntityMenu";
export const CardRoot = styled.div`
width: 31.25rem;
padding: 2rem;
border: 1px solid ${color("border")};
border-radius: 0.5rem;
box-shadow: 0 2px 2px ${color("shadow")};
background-color: ${color("white")};
flex: 1;
max-width: 52rem;
border-bottom: 1px solid ${color("border")};
padding-bottom: 2rem;
`;
export const CardHeader = styled.div`
display: flex;
align-items: center;
align-items: flex-end;
gap: 1rem;
margin-bottom: 1rem;
margin-bottom: 0.25rem;
`;
export const CardTitle = styled.div`
color: ${color("text-dark")};
font-size: 1.5rem;
line-height: 2rem;
font-weight: bold;
`;
......@@ -29,7 +26,8 @@ export const CardDescription = styled.div`
color: ${color("text-dark")};
font-size: 0.875rem;
line-height: 1.5rem;
margin-bottom: 1.5rem;
margin-bottom: 1rem;
max-width: 40rem;
`;
interface CardBadgeProps {
......
......@@ -2,8 +2,8 @@ import type { ReactNode } from "react";
import { useCallback, useMemo, useState } from "react";
import { t } from "ttag";
import { Link } from "react-router";
import { Button } from "metabase/ui";
import { isNotNull } from "metabase/lib/types";
import Button from "metabase/core/components/Button";
import Modal from "metabase/components/Modal";
import ModalContent from "metabase/components/ModalContent";
import {
......@@ -88,32 +88,39 @@ interface AuthCardBodyProps {
description: string;
isEnabled: boolean;
isConfigured: boolean;
badgeText?: string;
buttonText?: string;
children?: ReactNode;
}
const AuthCardBody = ({
export const AuthCardBody = ({
type,
title,
description,
isEnabled,
isConfigured,
badgeText,
buttonText,
children,
}: AuthCardBodyProps) => {
const badgeContent = badgeText ?? (isEnabled ? t`Active` : t`Paused`);
const buttonLabel = buttonText ?? (isConfigured ? t`Edit` : t`Set up`);
return (
<CardRoot>
<CardHeader>
<CardTitle>{title}</CardTitle>
{isConfigured && (
<CardBadge isEnabled={isEnabled}>
{isEnabled ? t`Active` : t`Paused`}
<CardBadge isEnabled={isEnabled} data-testid="card-badge">
{badgeContent}
</CardBadge>
)}
{children}
</CardHeader>
<CardDescription>{description}</CardDescription>
<Button as={Link} to={`/admin/settings/authentication/${type}`}>
{isConfigured ? t`Edit` : t`Set up`}
</Button>
<Link to={`/admin/settings/authentication/${type}`}>
<Button>{buttonLabel}</Button>
</Link>
</CardRoot>
);
};
......@@ -166,7 +173,12 @@ const AuthCardModal = ({
title={t`Deactivate ${name}?`}
footer={[
<Button key="cancel" onClick={onClose}>{t`Cancel`}</Button>,
<Button key="submit" danger onClick={onDeactivate}>
<Button
key="submit"
onClick={onDeactivate}
variant="filled"
color="error"
>
{t`Deactivate`}
</Button>,
]}
......
import { t } from "ttag";
import { jt, t } from "ttag";
import { useEffect, useState } from "react";
import { useAsyncFn } from "react-use";
......@@ -18,18 +18,26 @@ import { Ellipsified } from "metabase/core/components/Ellipsified";
import { CreateApiKeyModal } from "./CreateApiKeyModal";
import { EditApiKeyModal } from "./EditApiKeyModal";
import { DeleteApiKeyModal } from "./DeleteApiKeyModal";
import { formatMaskedKey } from "./utils";
type Modal = null | "create" | "edit" | "delete";
function formatMaskedKey(maskedKey: string) {
return maskedKey.substring(0, 4) + "...";
}
function EmptyTableWarning() {
function EmptyTableWarning({ onCreate }: { onCreate: () => void }) {
return (
<Stack mt="xl" align="center" justify="center" spacing="sm">
<Title>{t`No API keys here yet`}</Title>
<Text color="text-medium">{t`You can create an API key to make API calls programatically.`}</Text>
<Text color="text.1">{jt`You can ${(
<Button
key="create-key-button"
variant="subtle"
onClick={onCreate}
p="0"
m="0"
>
{t`create an api key`}
</Button>
)} to make API calls programatically.`}</Text>
</Stack>
);
}
......@@ -55,8 +63,8 @@ function ApiKeysTable({
<th>{t`Key name`}</th>
<th>{t`Group`}</th>
<th>{t`Key`}</th>
<th>{t`Last Modified By`}</th>
<th>{t`Last Modified On`}</th>
<th>{t`Last modified by`}</th>
<th>{t`Last modified on`}</th>
<th />
</tr>
</thead>
......@@ -99,7 +107,9 @@ function ApiKeysTable({
</tbody>
</table>
<LoadingAndErrorWrapper loading={loading} error={error}>
{apiKeys?.length === 0 && <EmptyTableWarning />}
{apiKeys?.length === 0 && (
<EmptyTableWarning onCreate={() => setModal("create")} />
)}
</LoadingAndErrorWrapper>
</Stack>
);
......@@ -156,7 +166,7 @@ export const ManageApiKeys = () => {
<ApiKeysTable
loading={loading}
error={error}
apiKeys={apiKeys}
apiKeys={apiKeys?.sort((a, b) => a.name.localeCompare(b.name))}
setActiveApiKey={setActiveApiKey}
setModal={setModal}
/>
......
import * as Yup from "yup";
export function formatMaskedKey(maskedKey: string) {
return maskedKey.substring(0, 7) + "...";
}
export const API_KEY_VALIDATION_SCHEMA = Yup.object({
name: Yup.string().required(),
group_id: Yup.number().required(),
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment