chore: Tidy API key settings page
This commit is contained in:
25
app/actions/definitions/apiKeys.tsx
Normal file
25
app/actions/definitions/apiKeys.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { PlusIcon } from "outline-icons";
|
||||||
|
import * as React from "react";
|
||||||
|
import stores from "~/stores";
|
||||||
|
import APIKeyNew from "~/scenes/APIKeyNew";
|
||||||
|
import { createAction } from "..";
|
||||||
|
import { SettingsSection } from "../sections";
|
||||||
|
|
||||||
|
export const createApiKey = createAction({
|
||||||
|
name: ({ t }) => t("New API key"),
|
||||||
|
analyticsName: "New API key",
|
||||||
|
section: SettingsSection,
|
||||||
|
icon: <PlusIcon />,
|
||||||
|
keywords: "create",
|
||||||
|
visible: () =>
|
||||||
|
stores.policies.abilities(stores.auth.team?.id || "").createApiKey,
|
||||||
|
perform: ({ t, event }) => {
|
||||||
|
event?.preventDefault();
|
||||||
|
event?.stopPropagation();
|
||||||
|
|
||||||
|
stores.dialogs.openModal({
|
||||||
|
title: t("New API key"),
|
||||||
|
content: <APIKeyNew onSubmit={stores.dialogs.closeAllModals} />,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -88,7 +88,7 @@ const useSettingsConfig = () => {
|
|||||||
icon: EmailIcon,
|
icon: EmailIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: t("API Tokens"),
|
name: t("API"),
|
||||||
path: settingsPath("tokens"),
|
path: settingsPath("tokens"),
|
||||||
component: ApiKeys,
|
component: ApiKeys,
|
||||||
enabled: can.createApiKey,
|
enabled: can.createApiKey,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { ApiKeyValidation } from "@shared/validations";
|
||||||
import Button from "~/components/Button";
|
import Button from "~/components/Button";
|
||||||
import Flex from "~/components/Flex";
|
import Flex from "~/components/Flex";
|
||||||
import Input from "~/components/Input";
|
import Input from "~/components/Input";
|
||||||
@@ -11,7 +12,7 @@ type Props = {
|
|||||||
onSubmit: () => void;
|
onSubmit: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
function APITokenNew({ onSubmit }: Props) {
|
function APIKeyNew({ onSubmit }: Props) {
|
||||||
const [name, setName] = React.useState("");
|
const [name, setName] = React.useState("");
|
||||||
const [isSaving, setIsSaving] = React.useState(false);
|
const [isSaving, setIsSaving] = React.useState(false);
|
||||||
const { apiKeys } = useStores();
|
const { apiKeys } = useStores();
|
||||||
@@ -26,7 +27,7 @@ function APITokenNew({ onSubmit }: Props) {
|
|||||||
await apiKeys.create({
|
await apiKeys.create({
|
||||||
name,
|
name,
|
||||||
});
|
});
|
||||||
toast.success(t("API token created"));
|
toast.success(t("API Key created"));
|
||||||
onSubmit();
|
onSubmit();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(err.message);
|
toast.error(err.message);
|
||||||
@@ -45,7 +46,7 @@ function APITokenNew({ onSubmit }: Props) {
|
|||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<Text as="p" type="secondary">
|
<Text as="p" type="secondary">
|
||||||
{t(
|
{t(
|
||||||
`Name your token something that will help you to remember it's use in the future, for example "local development", "production", or "continuous integration".`
|
`Name your key something that will help you to remember it's use in the future, for example "local development" or "continuous integration".`
|
||||||
)}
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
<Flex>
|
<Flex>
|
||||||
@@ -54,16 +55,20 @@ function APITokenNew({ onSubmit }: Props) {
|
|||||||
label={t("Name")}
|
label={t("Name")}
|
||||||
onChange={handleNameChange}
|
onChange={handleNameChange}
|
||||||
value={name}
|
value={name}
|
||||||
|
minLength={ApiKeyValidation.minNameLength}
|
||||||
|
maxLength={ApiKeyValidation.maxNameLength}
|
||||||
required
|
required
|
||||||
autoFocus
|
autoFocus
|
||||||
flex
|
flex
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Button type="submit" disabled={isSaving || !name}>
|
<Flex justify="flex-end">
|
||||||
{isSaving ? `${t("Creating")}…` : t("Create")}
|
<Button type="submit" disabled={isSaving || !name}>
|
||||||
</Button>
|
{isSaving ? `${t("Creating")}…` : t("Create")}
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default APITokenNew;
|
export default APIKeyNew;
|
||||||
@@ -3,15 +3,14 @@ import { CodeIcon } from "outline-icons";
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useTranslation, Trans } from "react-i18next";
|
import { useTranslation, Trans } from "react-i18next";
|
||||||
import ApiKey from "~/models/ApiKey";
|
import ApiKey from "~/models/ApiKey";
|
||||||
import APITokenNew from "~/scenes/APITokenNew";
|
|
||||||
import { Action } from "~/components/Actions";
|
import { Action } from "~/components/Actions";
|
||||||
import Button from "~/components/Button";
|
import Button from "~/components/Button";
|
||||||
import Heading from "~/components/Heading";
|
import Heading from "~/components/Heading";
|
||||||
import Modal from "~/components/Modal";
|
|
||||||
import PaginatedList from "~/components/PaginatedList";
|
import PaginatedList from "~/components/PaginatedList";
|
||||||
import Scene from "~/components/Scene";
|
import Scene from "~/components/Scene";
|
||||||
import Text from "~/components/Text";
|
import Text from "~/components/Text";
|
||||||
import useBoolean from "~/hooks/useBoolean";
|
import { createApiKey } from "~/actions/definitions/apiKeys";
|
||||||
|
import useActionContext from "~/hooks/useActionContext";
|
||||||
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
||||||
import usePolicy from "~/hooks/usePolicy";
|
import usePolicy from "~/hooks/usePolicy";
|
||||||
import useStores from "~/hooks/useStores";
|
import useStores from "~/hooks/useStores";
|
||||||
@@ -21,12 +20,12 @@ function ApiKeys() {
|
|||||||
const team = useCurrentTeam();
|
const team = useCurrentTeam();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { apiKeys } = useStores();
|
const { apiKeys } = useStores();
|
||||||
const [newModalOpen, handleNewModalOpen, handleNewModalClose] = useBoolean();
|
|
||||||
const can = usePolicy(team);
|
const can = usePolicy(team);
|
||||||
|
const context = useActionContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Scene
|
<Scene
|
||||||
title={t("API Tokens")}
|
title={t("API Keys")}
|
||||||
icon={<CodeIcon />}
|
icon={<CodeIcon />}
|
||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
@@ -34,19 +33,20 @@ function ApiKeys() {
|
|||||||
<Action>
|
<Action>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
value={`${t("New token")}…`}
|
value={`${t("New API key")}…`}
|
||||||
onClick={handleNewModalOpen}
|
action={createApiKey}
|
||||||
|
context={context}
|
||||||
/>
|
/>
|
||||||
</Action>
|
</Action>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Heading>{t("API Tokens")}</Heading>
|
<Heading>{t("API Keys")}</Heading>
|
||||||
<Text as="p" type="secondary">
|
<Text as="p" type="secondary">
|
||||||
<Trans
|
<Trans
|
||||||
defaults="You can create an unlimited amount of personal tokens to authenticate
|
defaults="Create personal API keys to authenticate with the API and programatically control
|
||||||
with the API. Tokens have the same permissions as your user account.
|
your workspace's data. API keys have the same permissions as your user account.
|
||||||
For more details see the <em>developer documentation</em>."
|
For more details see the <em>developer documentation</em>."
|
||||||
components={{
|
components={{
|
||||||
em: (
|
em: (
|
||||||
@@ -67,13 +67,6 @@ function ApiKeys() {
|
|||||||
<ApiKeyListItem key={apiKey.id} apiKey={apiKey} />
|
<ApiKeyListItem key={apiKey.id} apiKey={apiKey} />
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Modal
|
|
||||||
title={t("Create a token")}
|
|
||||||
onRequestClose={handleNewModalClose}
|
|
||||||
isOpen={newModalOpen}
|
|
||||||
>
|
|
||||||
<APITokenNew onSubmit={handleNewModalClose} />
|
|
||||||
</Modal>
|
|
||||||
</Scene>
|
</Scene>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
BelongsTo,
|
BelongsTo,
|
||||||
ForeignKey,
|
ForeignKey,
|
||||||
} from "sequelize-typescript";
|
} from "sequelize-typescript";
|
||||||
|
import { ApiKeyValidation } from "@shared/validations";
|
||||||
import User from "./User";
|
import User from "./User";
|
||||||
import ParanoidModel from "./base/ParanoidModel";
|
import ParanoidModel from "./base/ParanoidModel";
|
||||||
import Fix from "./decorators/Fix";
|
import Fix from "./decorators/Fix";
|
||||||
@@ -22,9 +23,9 @@ class ApiKey extends ParanoidModel<
|
|||||||
static prefix = "ol_api_";
|
static prefix = "ol_api_";
|
||||||
|
|
||||||
@Length({
|
@Length({
|
||||||
min: 3,
|
min: ApiKeyValidation.minNameLength,
|
||||||
max: 255,
|
max: ApiKeyValidation.maxNameLength,
|
||||||
msg: "Name must be between 3 and 255 characters",
|
msg: `Name must be between ${ApiKeyValidation.minNameLength} and ${ApiKeyValidation.maxNameLength} characters`,
|
||||||
})
|
})
|
||||||
@Column
|
@Column
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"New API key": "New API key",
|
||||||
"Open collection": "Open collection",
|
"Open collection": "Open collection",
|
||||||
"New collection": "New collection",
|
"New collection": "New collection",
|
||||||
"Create a collection": "Create a collection",
|
"Create a collection": "Create a collection",
|
||||||
@@ -431,7 +432,7 @@
|
|||||||
"Could not import file": "Could not import file",
|
"Could not import file": "Could not import file",
|
||||||
"Unsubscribed from document": "Unsubscribed from document",
|
"Unsubscribed from document": "Unsubscribed from document",
|
||||||
"Account": "Account",
|
"Account": "Account",
|
||||||
"API Tokens": "API Tokens",
|
"API": "API",
|
||||||
"Details": "Details",
|
"Details": "Details",
|
||||||
"Security": "Security",
|
"Security": "Security",
|
||||||
"Features": "Features",
|
"Features": "Features",
|
||||||
@@ -495,8 +496,8 @@
|
|||||||
"left a comment on": "left a comment on",
|
"left a comment on": "left a comment on",
|
||||||
"shared": "shared",
|
"shared": "shared",
|
||||||
"invited you to": "invited you to",
|
"invited you to": "invited you to",
|
||||||
"API token created": "API token created",
|
"API Key created": "API Key created",
|
||||||
"Name your token something that will help you to remember it's use in the future, for example \"local development\", \"production\", or \"continuous integration\".": "Name your token something that will help you to remember it's use in the future, for example \"local development\", \"production\", or \"continuous integration\".",
|
"Name your key something that will help you to remember it's use in the future, for example \"local development\" or \"continuous integration\".": "Name your key something that will help you to remember it's use in the future, for example \"local development\" or \"continuous integration\".",
|
||||||
"The document archive is empty at the moment.": "The document archive is empty at the moment.",
|
"The document archive is empty at the moment.": "The document archive is empty at the moment.",
|
||||||
"Collection menu": "Collection menu",
|
"Collection menu": "Collection menu",
|
||||||
"Drop documents to import": "Drop documents to import",
|
"Drop documents to import": "Drop documents to import",
|
||||||
@@ -755,10 +756,9 @@
|
|||||||
"We were unable to find the page you’re looking for.": "We were unable to find the page you’re looking for.",
|
"We were unable to find the page you’re looking for.": "We were unable to find the page you’re looking for.",
|
||||||
"Search titles only": "Search titles only",
|
"Search titles only": "Search titles only",
|
||||||
"No documents found for your search filters.": "No documents found for your search filters.",
|
"No documents found for your search filters.": "No documents found for your search filters.",
|
||||||
"New token": "New token",
|
"API Keys": "API Keys",
|
||||||
"You can create an unlimited amount of personal tokens to authenticate\n with the API. Tokens have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.": "You can create an unlimited amount of personal tokens to authenticate\n with the API. Tokens have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.",
|
"Create personal API keys to authenticate with the API and programatically control\n your workspace's data. API keys have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.": "Create personal API keys to authenticate with the API and programatically control\n your workspace's data. API keys have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.",
|
||||||
"Active": "Active",
|
"Active": "Active",
|
||||||
"Create a token": "Create a token",
|
|
||||||
"API token copied to clipboard": "API token copied to clipboard",
|
"API token copied to clipboard": "API token copied to clipboard",
|
||||||
"Copied": "Copied",
|
"Copied": "Copied",
|
||||||
"Revoking": "Revoking",
|
"Revoking": "Revoking",
|
||||||
|
|||||||
@@ -19,6 +19,13 @@ export const AttachmentValidation = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ApiKeyValidation = {
|
||||||
|
/** The minimum length of the API key name */
|
||||||
|
minNameLength: 3,
|
||||||
|
/** The maximum length of the API key name */
|
||||||
|
maxNameLength: 255,
|
||||||
|
};
|
||||||
|
|
||||||
export const CollectionValidation = {
|
export const CollectionValidation = {
|
||||||
/** The maximum length of the collection description */
|
/** The maximum length of the collection description */
|
||||||
maxDescriptionLength: 10 * 1000,
|
maxDescriptionLength: 10 * 1000,
|
||||||
|
|||||||
Reference in New Issue
Block a user