mirror of
https://github.com/hanzoai/calendar.git
synced 2026-10-30 14:46:43 +00:00
542 lines
16 KiB
TypeScript
542 lines
16 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { useForm, Controller, useFormContext } from "react-hook-form";
|
|
|
|
import { Dialog } from "@calcom/features/components/controlled-dialog";
|
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
import type { ServiceAccountKey } from "@calcom/lib/server/serviceAccountKey";
|
|
import { serviceAccountKeySchema } from "@calcom/prisma/zod-utils";
|
|
import { trpc } from "@calcom/trpc/react";
|
|
import { Badge } from "@calcom/ui/components/badge";
|
|
import { InfoBadge } from "@calcom/ui/components/badge";
|
|
import { Button } from "@calcom/ui/components/button";
|
|
import { DialogContent, DialogFooter, ConfirmationDialogContent } from "@calcom/ui/components/dialog";
|
|
import { EmptyScreen } from "@calcom/ui/components/empty-screen";
|
|
import { Form } from "@calcom/ui/components/form";
|
|
import { TextAreaField } from "@calcom/ui/components/form";
|
|
import { TextField } from "@calcom/ui/components/form";
|
|
import { SelectField } from "@calcom/ui/components/form";
|
|
import { Switch } from "@calcom/ui/components/form";
|
|
import { SkeletonContainer, SkeletonText } from "@calcom/ui/components/skeleton";
|
|
import { DropdownActions } from "@calcom/ui/components/table";
|
|
import { showToast } from "@calcom/ui/components/toast";
|
|
|
|
interface DelegationItemProps {
|
|
delegation: {
|
|
id: string;
|
|
domain: string;
|
|
enabled: boolean;
|
|
serviceAccountClientId: string | null;
|
|
workspacePlatform: {
|
|
name: string;
|
|
slug: string;
|
|
};
|
|
};
|
|
toggleDelegation: (delegation: DelegationItemProps["delegation"]) => void;
|
|
onEdit: (delegation: DelegationItemProps["delegation"]) => void;
|
|
onDelete: (id: string) => void;
|
|
}
|
|
|
|
type WorkspacePlatform = {
|
|
id: number;
|
|
name: string;
|
|
slug: string;
|
|
};
|
|
|
|
function getWorkspacePlatformOptions(workspacePlatforms: WorkspacePlatform[]) {
|
|
return workspacePlatforms.map((platform) => ({
|
|
value: platform.slug,
|
|
label: platform.name,
|
|
}));
|
|
}
|
|
|
|
const SkeletonLoader = () => {
|
|
return (
|
|
<SkeletonContainer>
|
|
<div className="divide-subtle border-subtle space-y-6 rounded-b-lg border border-t-0 px-6 py-4">
|
|
<SkeletonText className="h-8 w-full" />
|
|
<SkeletonText className="h-8 w-full" />
|
|
</div>
|
|
</SkeletonContainer>
|
|
);
|
|
};
|
|
|
|
function DelegationListItemActions({
|
|
delegation,
|
|
toggleDelegation,
|
|
onEdit,
|
|
onDelete,
|
|
}: {
|
|
delegation: DelegationItemProps["delegation"];
|
|
toggleDelegation: DelegationItemProps["toggleDelegation"];
|
|
onEdit: (delegation: DelegationItemProps["delegation"]) => void;
|
|
onDelete: (id: string) => void;
|
|
}) {
|
|
const { t } = useLocale();
|
|
|
|
return (
|
|
<div className="flex items-center space-x-2">
|
|
<Switch checked={delegation.enabled} onCheckedChange={() => toggleDelegation(delegation)} />
|
|
<DropdownActions
|
|
actions={[
|
|
{
|
|
id: "edit",
|
|
label: t("edit"),
|
|
onClick: () => onEdit(delegation),
|
|
icon: "pencil",
|
|
},
|
|
{
|
|
id: "delete",
|
|
label: t("delete"),
|
|
disabled: true,
|
|
onClick: () => onDelete(delegation.id),
|
|
icon: "trash",
|
|
},
|
|
]}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function DelegationListItem({ delegation, toggleDelegation, onEdit, onDelete }: DelegationItemProps) {
|
|
const { t } = useLocale();
|
|
return (
|
|
<li className="border-subtle bg-default divide-subtle flex flex-col divide-y border">
|
|
<div className="flex items-center justify-between space-x-2 p-4">
|
|
<div className="flex flex-col items-start">
|
|
<div className="flex items-center space-x-2">
|
|
<span>{delegation.serviceAccountClientId}</span>
|
|
<InfoBadge content={t("add_client_id_in_google_workspace_with_below_scope")} />
|
|
</div>
|
|
<span className="text-muted mt-2">https://www.googleapis.com/auth/calendar</span>
|
|
<div className="mt-2 flex items-center space-x-2">
|
|
<Badge variant="default">{delegation.workspacePlatform.name}</Badge>
|
|
<Badge variant="gray">{delegation.domain}</Badge>
|
|
</div>
|
|
</div>
|
|
<DelegationListItemActions
|
|
delegation={delegation}
|
|
toggleDelegation={toggleDelegation}
|
|
onEdit={onEdit}
|
|
onDelete={onDelete}
|
|
/>
|
|
</div>
|
|
</li>
|
|
);
|
|
}
|
|
|
|
type CreateDelegationData = {
|
|
domain: string;
|
|
workspacePlatformSlug: string;
|
|
enabled: boolean;
|
|
serviceAccountKey: ServiceAccountKey;
|
|
};
|
|
|
|
type CreateDelegationFormData = Omit<CreateDelegationData, "serviceAccountKey"> & {
|
|
serviceAccountKey: string;
|
|
};
|
|
|
|
type EditDelegationData = {
|
|
id: string;
|
|
domain: string;
|
|
workspacePlatformSlug: string;
|
|
enabled: boolean;
|
|
};
|
|
|
|
function CreateDelegationDialog({
|
|
isOpen,
|
|
onClose,
|
|
workspacePlatforms,
|
|
handleCreate,
|
|
}: {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
workspacePlatforms: WorkspacePlatform[];
|
|
handleCreate: (data: CreateDelegationData) => void;
|
|
}) {
|
|
const { t } = useLocale();
|
|
|
|
const form = useForm<CreateDelegationFormData>();
|
|
|
|
const handleSubmit = (values: CreateDelegationFormData) => {
|
|
try {
|
|
const parsedKey = JSON.parse(values.serviceAccountKey);
|
|
const validatedKey = serviceAccountKeySchema.safeParse(parsedKey);
|
|
|
|
if (!validatedKey.success) {
|
|
form.setError("serviceAccountKey", { message: t("invalid_service_account_key") });
|
|
return;
|
|
}
|
|
|
|
handleCreate({
|
|
...values,
|
|
serviceAccountKey: validatedKey.data,
|
|
});
|
|
} catch (e) {
|
|
console.error(e);
|
|
form.setError("serviceAccountKey", { message: t("invalid_service_account_key") });
|
|
return;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
|
<DialogContent enableOverflow title={t("add_delegation_credential")}>
|
|
<Form form={form} handleSubmit={handleSubmit}>
|
|
<DelegationFormFields workspacePlatforms={workspacePlatforms} isCreate={true} />
|
|
<DialogFooter>
|
|
<Button type="button" color="secondary" onClick={onClose}>
|
|
{t("cancel")}
|
|
</Button>
|
|
<Button type="submit">{t("create")}</Button>
|
|
</DialogFooter>
|
|
</Form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|
|
|
|
function EditDelegationDialog({
|
|
isOpen,
|
|
onClose,
|
|
delegation,
|
|
workspacePlatforms,
|
|
handleEdit,
|
|
}: {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
delegation: DelegationItemProps["delegation"];
|
|
workspacePlatforms: WorkspacePlatform[];
|
|
handleEdit: (data: EditDelegationData) => void;
|
|
}) {
|
|
const { t } = useLocale();
|
|
|
|
const form = useForm<EditDelegationData>({
|
|
defaultValues: {
|
|
id: delegation.id,
|
|
domain: delegation.domain,
|
|
workspacePlatformSlug: delegation.workspacePlatform.slug,
|
|
},
|
|
});
|
|
|
|
return (
|
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
|
<DialogContent title={t("edit_delegation_credential")}>
|
|
<Form form={form} handleSubmit={handleEdit}>
|
|
<DelegationFormFields workspacePlatforms={workspacePlatforms} isCreate={false} />
|
|
<DialogFooter>
|
|
<Button type="button" color="secondary" onClick={onClose}>
|
|
{t("cancel")}
|
|
</Button>
|
|
<Button type="submit">{t("update")}</Button>
|
|
</DialogFooter>
|
|
</Form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|
|
|
|
function DelegationFormFields({
|
|
workspacePlatforms,
|
|
isCreate,
|
|
}: {
|
|
workspacePlatforms: WorkspacePlatform[];
|
|
isCreate: boolean;
|
|
}) {
|
|
const { t } = useLocale();
|
|
const form = useFormContext();
|
|
return (
|
|
<div className="space-y-4">
|
|
<TextField label={t("domain")} {...form.register("domain")} />
|
|
<Controller
|
|
name="workspacePlatformSlug"
|
|
control={form.control}
|
|
render={({ field: { value, onChange } }) => {
|
|
const platformOptions = getWorkspacePlatformOptions(workspacePlatforms);
|
|
const selectedPlatform = platformOptions.find((opt) => opt.value === value);
|
|
return (
|
|
<SelectField
|
|
required
|
|
label={t("workspace_platform")}
|
|
onChange={(option) => onChange(option?.value)}
|
|
value={selectedPlatform}
|
|
options={platformOptions}
|
|
/>
|
|
);
|
|
}}
|
|
/>
|
|
{isCreate && (
|
|
<TextAreaField
|
|
required
|
|
label={t("service_account_key")}
|
|
placeholder="{...}"
|
|
{...form.register("serviceAccountKey")}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function DelegationCredentialList() {
|
|
const { t } = useLocale();
|
|
const utils = trpc.useContext();
|
|
const { data: delegations, isLoading, error } = trpc.viewer.delegationCredential.list.useQuery();
|
|
|
|
const updateMutation = trpc.viewer.delegationCredential.update.useMutation({
|
|
onSuccess: () => utils.viewer.delegationCredential.list.invalidate(),
|
|
onError: (error) => {
|
|
showToast(error.message, "error");
|
|
},
|
|
});
|
|
|
|
const toggleEnabledMutation = trpc.viewer.delegationCredential.toggleEnabled.useMutation({
|
|
onSuccess: (data) => {
|
|
if (data) {
|
|
showToast(
|
|
data.enabled ? t("delegation_credential_enabled") : t("delegation_credential_disabled"),
|
|
"success"
|
|
);
|
|
utils.viewer.delegationCredential.list.invalidate();
|
|
}
|
|
},
|
|
onError: (error) => {
|
|
showToast(error.message, "error");
|
|
},
|
|
});
|
|
|
|
const createMutation = trpc.viewer.delegationCredential.add.useMutation({
|
|
onSuccess: () => utils.viewer.delegationCredential.list.invalidate(),
|
|
onError: (error) => {
|
|
showToast(error.message, "error");
|
|
},
|
|
});
|
|
|
|
const deleteMutation = trpc.viewer.delegationCredential.delete.useMutation({
|
|
onSuccess: () => utils.viewer.delegationCredential.list.invalidate(),
|
|
onError: (error) => {
|
|
showToast(error.message, "error");
|
|
},
|
|
});
|
|
|
|
const onDelete = (id: string) => {
|
|
deleteMutation.mutate({ id });
|
|
};
|
|
|
|
const [delegationToToggle, setDelegationToToggle] = useState<DelegationItemProps["delegation"] | null>(
|
|
null
|
|
);
|
|
|
|
const toggleDelegation = (delegation: DelegationItemProps["delegation"]) => {
|
|
setDelegationToToggle(delegation);
|
|
};
|
|
|
|
const handleToggleConfirm = () => {
|
|
if (delegationToToggle) {
|
|
toggleEnabledMutation.mutate({
|
|
id: delegationToToggle.id,
|
|
enabled: !delegationToToggle.enabled,
|
|
});
|
|
setDelegationToToggle(null);
|
|
}
|
|
};
|
|
|
|
const [createEditDialog, setCreateEditDialog] = useState<{
|
|
isOpen: boolean;
|
|
delegation: DelegationItemProps["delegation"] | null;
|
|
}>({
|
|
isOpen: false,
|
|
delegation: null,
|
|
});
|
|
|
|
const { data: workspacePlatforms, isLoading: isLoadingWorkspacePlatforms } =
|
|
trpc.viewer.delegationCredential.listWorkspacePlatforms.useQuery();
|
|
|
|
const enabledWorkspacePlatforms = workspacePlatforms?.filter((platform) => platform.enabled) || [];
|
|
|
|
const onEditClick = (delegation: DelegationItemProps["delegation"]) => {
|
|
setCreateEditDialog({ isOpen: true, delegation });
|
|
};
|
|
|
|
const onCreateClick = () => setCreateEditDialog({ isOpen: true, delegation: null });
|
|
|
|
const handleCreate = (data: CreateDelegationData) => {
|
|
createMutation.mutate({
|
|
domain: data.domain,
|
|
workspacePlatformSlug: data.workspacePlatformSlug,
|
|
serviceAccountKey: data.serviceAccountKey,
|
|
});
|
|
setCreateEditDialog({ isOpen: false, delegation: null });
|
|
};
|
|
|
|
const handleEdit = (data: EditDelegationData) => {
|
|
if (!createEditDialog.delegation) {
|
|
return;
|
|
}
|
|
updateMutation.mutate({
|
|
id: createEditDialog.delegation.id,
|
|
domain: data.domain,
|
|
workspacePlatformSlug: data.workspacePlatformSlug,
|
|
});
|
|
setCreateEditDialog({ isOpen: false, delegation: null });
|
|
};
|
|
|
|
const AddDwDButton = () => {
|
|
return (
|
|
<Button type="button" color="secondary" StartIcon="plus" className="mt-6" onClick={onCreateClick}>
|
|
{t("add_delegation_credential")}
|
|
</Button>
|
|
);
|
|
};
|
|
|
|
if (isLoading || isLoadingWorkspacePlatforms) {
|
|
return <SkeletonLoader />;
|
|
}
|
|
|
|
if (error) {
|
|
return <div>{t(error.message)}</div>;
|
|
}
|
|
|
|
if (!delegations || !workspacePlatforms) {
|
|
return <div>{t("something_went_wrong")}</div>;
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
{!!delegations?.length ? (
|
|
<>
|
|
<ul className="space-y-2 [&>*:last-child]:rounded-b-xl">
|
|
{delegations.map(
|
|
(delegation) =>
|
|
delegation && (
|
|
<DelegationListItem
|
|
key={delegation.id}
|
|
delegation={delegation}
|
|
toggleDelegation={toggleDelegation}
|
|
onEdit={onEditClick}
|
|
onDelete={onDelete}
|
|
/>
|
|
)
|
|
)}
|
|
</ul>
|
|
{/* Disable till we thoroughly test multiple delegation credentials support */}
|
|
{/* <AddDwDButton /> */}
|
|
</>
|
|
) : (
|
|
<EmptyScreen
|
|
Icon="link"
|
|
headline={t("add_delegation_credential")}
|
|
description={t("delegation_credential_description")}
|
|
className="rounded-b-lg rounded-t-none border-t-0"
|
|
buttonRaw={<AddDwDButton />}
|
|
/>
|
|
)}
|
|
|
|
{createEditDialog.delegation ? (
|
|
<EditDelegationDialog
|
|
isOpen={createEditDialog.isOpen}
|
|
onClose={() => setCreateEditDialog({ isOpen: false, delegation: null })}
|
|
delegation={createEditDialog.delegation}
|
|
workspacePlatforms={enabledWorkspacePlatforms}
|
|
handleEdit={handleEdit}
|
|
/>
|
|
) : (
|
|
<CreateDelegationDialog
|
|
isOpen={createEditDialog.isOpen}
|
|
onClose={() => setCreateEditDialog({ isOpen: false, delegation: null })}
|
|
workspacePlatforms={enabledWorkspacePlatforms}
|
|
handleCreate={handleCreate}
|
|
/>
|
|
)}
|
|
|
|
<ToggleDelegationDialog
|
|
delegation={delegationToToggle}
|
|
onClose={() => setDelegationToToggle(null)}
|
|
onConfirm={handleToggleConfirm}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function MembersThatWillBeAffectedOnDisablingDelegationCredential({
|
|
delegationCredentialId,
|
|
}: {
|
|
delegationCredentialId: string;
|
|
}) {
|
|
const { t } = useLocale();
|
|
const { data: affectedMembers, isLoading: isLoadingAffectedMembers } =
|
|
trpc.viewer.delegationCredential.getAffectedMembersForDisable.useQuery({ id: delegationCredentialId });
|
|
|
|
return (
|
|
<div className="mt-4">
|
|
<strong>{t("members_affected_by_disabling_delegation_credential")}</strong>
|
|
{isLoadingAffectedMembers ? (
|
|
<div>{t("loading")}</div>
|
|
) : affectedMembers?.length ? (
|
|
<>
|
|
<ul className="list-disc space-y-1 p-1 pl-5 sm:w-80">
|
|
{affectedMembers.slice(0, 5).map((m) => (
|
|
<li className="text-muted text-sm" key={m.email}>
|
|
{m.name ? `${m.name} (${m.email})` : m.email}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
{affectedMembers.length > 5 && (
|
|
<p className="mt-2 text-sm text-gray-500">
|
|
{t("and_count_more", { count: affectedMembers.length - 5 })}
|
|
</p>
|
|
)}
|
|
</>
|
|
) : (
|
|
<div className="mt-2">{t("no_members_affected_by_disabling_delegation_credential")}</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
const ToggleDelegationDialog = ({
|
|
delegation,
|
|
onConfirm,
|
|
onClose,
|
|
}: {
|
|
delegation: DelegationItemProps["delegation"] | null;
|
|
onConfirm: () => void;
|
|
onClose: () => void;
|
|
}) => {
|
|
const { t } = useLocale();
|
|
|
|
if (!delegation) {
|
|
return null;
|
|
}
|
|
|
|
const isDisablingDelegation = delegation.enabled;
|
|
|
|
return (
|
|
<Dialog
|
|
name="toggle-delegation"
|
|
open={!!delegation}
|
|
onOpenChange={(open) => (!open ? onClose() : undefined)}>
|
|
<ConfirmationDialogContent
|
|
title={t(isDisablingDelegation ? "disable_delegation_credential" : "enable_delegation_credential")}
|
|
confirmBtnText={t(isDisablingDelegation ? "disable" : "enable")}
|
|
cancelBtnText={t("cancel")}
|
|
variety={isDisablingDelegation ? "danger" : "success"}
|
|
onConfirm={onConfirm}>
|
|
<p className="mt-5">
|
|
{t(
|
|
isDisablingDelegation
|
|
? "disable_delegation_credential_description"
|
|
: "enable_delegation_credential_description"
|
|
)}
|
|
</p>
|
|
{isDisablingDelegation && (
|
|
<MembersThatWillBeAffectedOnDisablingDelegationCredential delegationCredentialId={delegation.id} />
|
|
)}
|
|
</ConfirmationDialogContent>
|
|
</Dialog>
|
|
);
|
|
};
|
|
|
|
export default function DelegationCredentialListPage() {
|
|
return <DelegationCredentialList />;
|
|
}
|