θΏ™ζ˜―indexlocζδΎ›ηš„ζœεŠ‘οΌŒδΈθ¦θΎ“ε…₯任何密码
Skip to content

Improve UX for API keys and invitations #3828

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
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
55 changes: 40 additions & 15 deletions frontend/src/pages/Admin/Invitations/NewInviteModal/index.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React, { useEffect, useState } from "react";
import { X } from "@phosphor-icons/react";
import { X, Copy, Check } from "@phosphor-icons/react";
import Admin from "@/models/admin";
import Workspace from "@/models/workspace";
import showToast from "@/utils/toast";

export default function NewInviteModal({ closeModal }) {
export default function NewInviteModal({ closeModal, onSuccess }) {
const [invite, setInvite] = useState(null);
const [error, setError] = useState(null);
const [copied, setCopied] = useState(false);
Expand All @@ -18,15 +19,22 @@ export default function NewInviteModal({ closeModal }) {
role: null,
workspaceIds: selectedWorkspaceIds,
});
if (!!newInvite) setInvite(newInvite);
if (!!newInvite) {
setInvite(newInvite);
onSuccess();
}
setError(error);
};

const copyInviteLink = () => {
if (!invite) return false;
window.navigator.clipboard.writeText(
`${window.location.origin}/accept-invite/${invite.code}`
);
setCopied(true);
showToast("Invite link copied to clipboard", "success", {
clear: true,
});
};

const handleWorkspaceSelection = (workspaceId) => {
Expand Down Expand Up @@ -79,12 +87,30 @@ export default function NewInviteModal({ closeModal }) {
<div className="space-y-4">
{error && <p className="text-red-400 text-sm">Error: {error}</p>}
{invite && (
<input
type="url"
defaultValue={`${window.location.origin}/accept-invite/${invite.code}`}
disabled={true}
className="border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
/>
<div className="relative">
<input
type="url"
defaultValue={`${window.location.origin}/accept-invite/${invite.code}`}
disabled={true}
className="border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg outline-none block w-full p-2.5 pr-10"
/>
<button
type="button"
onClick={copyInviteLink}
disabled={copied}
className="absolute right-2 top-1/2 -translate-y-1/2 p-1 rounded-md hover:bg-theme-modal-border transition-all duration-300"
>
{copied ? (
<Check
size={20}
className="text-green-400"
weight="bold"
/>
) : (
<Copy size={20} className="text-white" weight="bold" />
)}
</button>
</div>
)}
<p className="text-white text-opacity-60 text-xs md:text-sm">
After creation you will be able to copy the invite and send it
Expand Down Expand Up @@ -126,13 +152,13 @@ export default function NewInviteModal({ closeModal }) {
</div>
)}

<div className="flex justify-between items-center mt-6 pt-6 border-t border-theme-modal-border">
<div className="flex justify-end items-center mt-6 pt-6 border-t border-theme-modal-border">
{!invite ? (
<>
<button
onClick={closeModal}
type="button"
className="transition-all duration-300 text-white hover:bg-zinc-700 px-4 py-2 rounded-lg text-sm"
className="transition-all duration-300 text-white hover:bg-zinc-700 px-4 py-2 rounded-lg text-sm mr-2"
>
Cancel
</button>
Expand All @@ -145,12 +171,11 @@ export default function NewInviteModal({ closeModal }) {
</>
) : (
<button
onClick={copyInviteLink}
onClick={closeModal}
type="button"
disabled={copied}
className="w-full transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm"
className="transition-all duration-300 text-white hover:bg-zinc-700 px-4 py-2 rounded-lg text-sm"
>
{copied ? "Copied Link" : "Copy Invite Link"}
Close
</button>
)}
</div>
Expand Down
120 changes: 59 additions & 61 deletions frontend/src/pages/Admin/Invitations/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { isMobile } from "react-device-detect";
import * as Skeleton from "react-loading-skeleton";
import "react-loading-skeleton/dist/skeleton.css";
import { EnvelopeSimple } from "@phosphor-icons/react";
import usePrefersDarkMode from "@/hooks/usePrefersDarkMode";
import Admin from "@/models/admin";
import InviteRow from "./InviteRow";
import NewInviteModal from "./NewInviteModal";
Expand All @@ -14,6 +13,18 @@ import CTAButton from "@/components/lib/CTAButton";

export default function AdminInvites() {
const { isOpen, openModal, closeModal } = useModal();
const [loading, setLoading] = useState(true);
const [invites, setInvites] = useState([]);

const fetchInvites = async () => {
const _invites = await Admin.invites();
setInvites(_invites);
setLoading(false);
};

useEffect(() => {
fetchInvites();
}, []);

return (
<div className="w-screen h-screen overflow-hidden bg-theme-bg-container flex">
Expand Down Expand Up @@ -44,71 +55,58 @@ export default function AdminInvites() {
</CTAButton>
</div>
<div className="overflow-x-auto mt-6">
<InvitationsContainer />
{loading ? (
<Skeleton.default
height="80vh"
width="100%"
highlightColor="var(--theme-bg-primary)"
baseColor="var(--theme-bg-secondary)"
count={1}
className="w-full p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm"
containerClassName="flex w-full"
/>
) : (
<table className="w-full text-xs text-left rounded-lg min-w-[640px] border-spacing-0">
<thead className="text-theme-text-secondary text-xs leading-[18px] font-bold uppercase border-white/10 border-b">
<tr>
<th scope="col" className="px-6 py-3 rounded-tl-lg">
Status
</th>
<th scope="col" className="px-6 py-3">
Accepted By
</th>
<th scope="col" className="px-6 py-3">
Created By
</th>
<th scope="col" className="px-6 py-3">
Created
</th>
<th scope="col" className="px-6 py-3 rounded-tr-lg">
{" "}
</th>
</tr>
</thead>
<tbody>
{invites.length === 0 ? (
<tr className="bg-transparent text-theme-text-secondary text-sm font-medium">
<td colSpan="5" className="px-6 py-4 text-center">
No invitations found
</td>
</tr>
) : (
invites.map((invite) => (
<InviteRow key={invite.id} invite={invite} />
))
)}
</tbody>
</table>
)}
</div>
</div>
<ModalWrapper isOpen={isOpen}>
<NewInviteModal closeModal={closeModal} />
<NewInviteModal closeModal={closeModal} onSuccess={fetchInvites} />
</ModalWrapper>
</div>
</div>
);
}

function InvitationsContainer() {
const darkMode = usePrefersDarkMode();
const [loading, setLoading] = useState(true);
const [invites, setInvites] = useState([]);

useEffect(() => {
async function fetchInvites() {
const _invites = await Admin.invites();
setInvites(_invites);
setLoading(false);
}
fetchInvites();
}, []);

if (loading) {
return (
<Skeleton.default
height="80vh"
width="100%"
highlightColor="var(--theme-bg-primary)"
baseColor="var(--theme-bg-secondary)"
count={1}
className="w-full p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm"
containerClassName="flex w-full"
/>
);
}

return (
<table className="w-full text-xs text-left rounded-lg min-w-[640px] border-spacing-0">
<thead className="text-theme-text-secondary text-xs leading-[18px] font-bold uppercase border-white/10 border-b">
<tr>
<th scope="col" className="px-6 py-3 rounded-tl-lg">
Status
</th>
<th scope="col" className="px-6 py-3">
Accepted By
</th>
<th scope="col" className="px-6 py-3">
Created By
</th>
<th scope="col" className="px-6 py-3">
Created
</th>
<th scope="col" className="px-6 py-3 rounded-tr-lg">
{" "}
</th>
</tr>
</thead>
<tbody>
{invites.map((invite) => (
<InviteRow key={invite.id} invite={invite} />
))}
</tbody>
</table>
);
}
58 changes: 42 additions & 16 deletions frontend/src/pages/GeneralSettings/ApiKeys/NewApiKeyModal/index.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React, { useEffect, useState } from "react";
import { X } from "@phosphor-icons/react";
import { X, Copy, Check } from "@phosphor-icons/react";
import Admin from "@/models/admin";
import paths from "@/utils/paths";
import { userFromStorage } from "@/utils/request";
import System from "@/models/system";
import showToast from "@/utils/toast";

export default function NewApiKeyModal({ closeModal }) {
export default function NewApiKeyModal({ closeModal, onSuccess }) {
const [apiKey, setApiKey] = useState(null);
const [error, setError] = useState(null);
const [copied, setCopied] = useState(false);
Expand All @@ -17,14 +18,22 @@ export default function NewApiKeyModal({ closeModal }) {
const Model = !!user ? Admin : System;

const { apiKey: newApiKey, error } = await Model.generateApiKey();
if (!!newApiKey) setApiKey(newApiKey);
if (!!newApiKey) {
setApiKey(newApiKey);
onSuccess();
}
setError(error);
};

const copyApiKey = () => {
if (!apiKey) return false;
window.navigator.clipboard.writeText(apiKey.secret);
setCopied(true);
showToast("API key copied to clipboard", "success", {
clear: true,
});
};

useEffect(() => {
function resetStatus() {
if (!copied) return false;
Expand Down Expand Up @@ -57,12 +66,30 @@ export default function NewApiKeyModal({ closeModal }) {
<div className="space-y-6 max-h-[60vh] overflow-y-auto pr-2">
{error && <p className="text-red-400 text-sm">Error: {error}</p>}
{apiKey && (
<input
type="text"
defaultValue={`${apiKey.secret}`}
disabled={true}
className="border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
/>
<div className="relative">
<input
type="text"
defaultValue={`${apiKey.secret}`}
disabled={true}
className="border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg outline-none block w-full p-2.5 pr-10"
/>
<button
type="button"
onClick={copyApiKey}
disabled={copied}
className="absolute right-2 top-1/2 -translate-y-1/2 p-1 rounded-md hover:bg-theme-modal-border transition-all duration-300"
>
{copied ? (
<Check
size={20}
className="text-green-400"
weight="bold"
/>
) : (
<Copy size={20} className="text-white" weight="bold" />
)}
</button>
</div>
)}
<p className="text-white text-opacity-60 text-xs md:text-sm">
Once created the API key can be used to programmatically access
Expand All @@ -77,31 +104,30 @@ export default function NewApiKeyModal({ closeModal }) {
Read the API documentation &rarr;
</a>
</div>
<div className="flex justify-between items-center mt-6 pt-6 border-t border-theme-modal-border">
<div className="flex justify-end items-center mt-6 pt-6 border-t border-theme-modal-border">
{!apiKey ? (
<>
<button
onClick={closeModal}
type="button"
className="transition-all duration-300 text-white hover:bg-zinc-700 px-4 py-2 rounded-lg text-sm"
className="transition-all duration-300 text-white hover:bg-zinc-700 px-4 py-2 rounded-lg text-sm mr-2"
>
Cancel
</button>
<button
type="submit"
className="transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm"
>
Create API key
Create API Key
</button>
</>
) : (
<button
onClick={copyApiKey}
onClick={closeModal}
type="button"
disabled={copied}
className="w-full transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm"
className="transition-all duration-300 text-white hover:bg-zinc-700 px-4 py-2 rounded-lg text-sm"
>
{copied ? "Copied API key" : "Copy API key"}
Close
</button>
)}
</div>
Expand Down
Loading