first commit
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import {
|
||||
AppSkeletonLoader as SkeletonLoader,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogTrigger,
|
||||
Label,
|
||||
showToast,
|
||||
EmptyScreen,
|
||||
} from "@calcom/ui";
|
||||
|
||||
import CreateDirectory from "./CreateDirectory";
|
||||
import DirectoryInfo from "./DirectoryInfo";
|
||||
import GroupTeamMappingTable from "./GroupTeamMappingTable";
|
||||
|
||||
const ConfigureDirectorySync = ({ organizationId }: { organizationId: number | null }) => {
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useUtils();
|
||||
const [deleteDirectoryOpen, setDeleteDirectoryOpen] = useState(false);
|
||||
|
||||
const { data, isLoading, isError, error } = trpc.viewer.dsync.get.useQuery({ organizationId });
|
||||
|
||||
const deleteMutation = trpc.viewer.dsync.delete.useMutation({
|
||||
async onSuccess() {
|
||||
showToast(t("directory_sync_deleted"), "success");
|
||||
await utils.viewer.dsync.invalidate();
|
||||
setDeleteDirectoryOpen(false);
|
||||
},
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return <SkeletonLoader />;
|
||||
}
|
||||
|
||||
const directory = data ?? null;
|
||||
|
||||
const onDeleteConfirmation = (e: Event | React.MouseEvent<HTMLElement, MouseEvent>) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!directory) {
|
||||
return;
|
||||
}
|
||||
|
||||
deleteMutation.mutate({ organizationId, directoryId: directory.id });
|
||||
};
|
||||
|
||||
if (error || isError) {
|
||||
return (
|
||||
<div>
|
||||
<EmptyScreen
|
||||
headline="Error"
|
||||
description={error.message || "Error getting dsync data"}
|
||||
Icon="triangle-alert"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!directory ? (
|
||||
<CreateDirectory orgId={organizationId} />
|
||||
) : (
|
||||
<>
|
||||
<DirectoryInfo directory={directory} />
|
||||
<div className="mt-4">
|
||||
<GroupTeamMappingTable />
|
||||
</div>
|
||||
|
||||
<hr className="border-subtle my-6" />
|
||||
<Label>{t("danger_zone")}</Label>
|
||||
{/* Delete directory sync connection */}
|
||||
<Dialog open={deleteDirectoryOpen} onOpenChange={setDeleteDirectoryOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button color="destructive" className="mt-1" StartIcon="trash">
|
||||
{t("directory_sync_delete_connection")}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent
|
||||
title={t("directory_sync_delete_title")}
|
||||
description={t("directory_sync_delete_description")}
|
||||
type="creation"
|
||||
Icon="triangle-alert">
|
||||
<>
|
||||
<div className="mb-10">
|
||||
<p className="text-default mb-4">{t("directory_sync_delete_confirmation")}</p>
|
||||
</div>
|
||||
<DialogFooter showDivider>
|
||||
<DialogClose />
|
||||
<Button
|
||||
color="primary"
|
||||
data-testid="delete-account-confirm"
|
||||
onClick={onDeleteConfirmation}
|
||||
loading={deleteMutation.isPending}>
|
||||
{t("directory_sync_delete_connection")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfigureDirectorySync;
|
||||
126
calcom/packages/features/ee/dsync/components/CreateDirectory.tsx
Normal file
126
calcom/packages/features/ee/dsync/components/CreateDirectory.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import { useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
SelectField,
|
||||
Form,
|
||||
TextField,
|
||||
DialogFooter,
|
||||
showToast,
|
||||
} from "@calcom/ui";
|
||||
|
||||
import { directoryProviders } from "../lib/directoryProviders";
|
||||
|
||||
const defaultValues = {
|
||||
name: "",
|
||||
provider: directoryProviders[0].value,
|
||||
};
|
||||
|
||||
const CreateDirectory = ({ orgId }: { orgId: number | null }) => {
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useUtils();
|
||||
const form = useForm({ defaultValues });
|
||||
const [openModal, setOpenModal] = useState(false);
|
||||
|
||||
const mutation = trpc.viewer.dsync.create.useMutation({
|
||||
async onSuccess() {
|
||||
showToast(t("directory_sync_created"), "success");
|
||||
await utils.viewer.dsync.invalidate();
|
||||
setOpenModal(false);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col sm:flex-row">
|
||||
<div>
|
||||
<p className="text-default text-sm font-normal leading-6 dark:text-gray-300">
|
||||
{t("directory_sync_title")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex-shrink-0 pt-3 sm:ml-auto sm:pl-3 sm:pt-0">
|
||||
<Button color="primary" onClick={() => setOpenModal(true)}>
|
||||
{t("configure")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog open={openModal} onOpenChange={setOpenModal}>
|
||||
<DialogContent type="creation">
|
||||
<Form
|
||||
form={form}
|
||||
handleSubmit={(values) => {
|
||||
mutation.mutate({
|
||||
...values,
|
||||
organizationId: orgId,
|
||||
});
|
||||
}}>
|
||||
<div className="mb-5 mt-1">
|
||||
<h2 className="font-semi-bold font-cal text-emphasis text-xl tracking-wide">
|
||||
{t("directory_sync_configure")}
|
||||
</h2>
|
||||
<p className="mt-1 text-sm text-gray-500">{t("directory_sync_configure_description")}</p>
|
||||
</div>
|
||||
<fieldset className="space-y-6 py-2">
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field: { value } }) => (
|
||||
<TextField
|
||||
name="title"
|
||||
label={t("directory_name")}
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
form.setValue("name", e?.target.value);
|
||||
}}
|
||||
type="text"
|
||||
required
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="provider"
|
||||
render={() => (
|
||||
<SelectField
|
||||
name="provider"
|
||||
label={t("directory_provider")}
|
||||
options={directoryProviders}
|
||||
placeholder={t("choose_directory_provider")}
|
||||
defaultValue={directoryProviders[0]}
|
||||
onChange={(option) => {
|
||||
if (option) {
|
||||
form.setValue("provider", option.value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</fieldset>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
color="secondary"
|
||||
onClick={() => {
|
||||
setOpenModal(false);
|
||||
}}
|
||||
tabIndex={-1}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button type="submit" loading={form.formState.isSubmitting || mutation.isPending}>
|
||||
{t("save")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateDirectory;
|
||||
@@ -0,0 +1,34 @@
|
||||
import { CreateANewTeamForm } from "@calcom/features/ee/teams/components";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Dialog, DialogContent } from "@calcom/ui";
|
||||
|
||||
interface CreateTeamDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
const CreateTeamDialog = (props: CreateTeamDialogProps) => {
|
||||
const { open, onOpenChange } = props;
|
||||
const { t } = useLocale();
|
||||
|
||||
const utils = trpc.useUtils();
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent type="creation" title={t("create_new_team")} description={t("team_will_be_under_org")}>
|
||||
<CreateANewTeamForm
|
||||
inDialog
|
||||
submitLabel="Create"
|
||||
onCancel={() => onOpenChange(false)}
|
||||
onSuccess={async () => {
|
||||
await utils.viewer.dsync.teamGroupMapping.get.invalidate();
|
||||
await utils.viewer.teams.list.invalidate();
|
||||
onOpenChange(false);
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateTeamDialog;
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { Directory } from "@boxyhq/saml-jackson";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, showToast, Label, Tooltip } from "@calcom/ui";
|
||||
|
||||
const DirectoryInfo = ({ directory }: { directory: Directory }) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<p className="text-default text-sm font-normal leading-6 dark:text-gray-300">
|
||||
{t("directory_sync_info_description")}
|
||||
</p>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex">
|
||||
<Label>{t("directory_scim_url")}</Label>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<code className="bg-subtle text-default w-full truncate rounded-md rounded-r-none py-[6px] pl-2 pr-2 align-middle font-mono">
|
||||
{directory.scim.endpoint}
|
||||
</code>
|
||||
<Tooltip side="top" content={t("copy_to_clipboard")}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(`${directory.scim.endpoint}`);
|
||||
showToast(t("directory_scim_url_copied"), "success");
|
||||
}}
|
||||
type="button"
|
||||
className="rounded-l-none text-base"
|
||||
StartIcon="clipboard">
|
||||
{t("copy")}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex">
|
||||
<Label>{t("directory_scim_token")}</Label>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<code className="bg-subtle text-default w-full truncate rounded-md rounded-r-none py-[6px] pl-2 pr-2 align-middle font-mono">
|
||||
{directory.scim.secret}
|
||||
</code>
|
||||
<Tooltip side="top" content={t("copy_to_clipboard")}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(`${directory.scim.secret}`);
|
||||
showToast(t("directory_scim_token_copied"), "success");
|
||||
}}
|
||||
type="button"
|
||||
className="rounded-l-none text-base"
|
||||
StartIcon="clipboard">
|
||||
{t("copy")}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DirectoryInfo;
|
||||
116
calcom/packages/features/ee/dsync/components/GroupNameCell.tsx
Normal file
116
calcom/packages/features/ee/dsync/components/GroupNameCell.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Badge, Icon, showToast, TextField } from "@calcom/ui";
|
||||
|
||||
interface GroupNameCellProps {
|
||||
groupNames: string[];
|
||||
teamId: number;
|
||||
directoryId: string;
|
||||
}
|
||||
|
||||
const GroupNameCell = ({ groupNames, teamId, directoryId }: GroupNameCellProps) => {
|
||||
const [showTextInput, setShowTextInput] = useState(false);
|
||||
const [newGroupName, setNewGroupName] = useState("");
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useUtils();
|
||||
|
||||
const createMutation = trpc.viewer.dsync.teamGroupMapping.create.useMutation({
|
||||
onSuccess: (data) => {
|
||||
utils.viewer.dsync.teamGroupMapping.get.setData(undefined, (prev) => {
|
||||
if (prev) {
|
||||
const teamIndex = prev.teamGroupMapping.findIndex((team) => team.id === teamId);
|
||||
prev.teamGroupMapping[teamIndex].groupNames.push(data.newGroupName);
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
setShowTextInput(false);
|
||||
setNewGroupName("");
|
||||
showToast(`Group added`, "success");
|
||||
},
|
||||
onError: (error) => {
|
||||
showToast(`Error adding group name${error.message}`, "error");
|
||||
},
|
||||
});
|
||||
|
||||
const deleteMutation = trpc.viewer.dsync.teamGroupMapping.delete.useMutation({
|
||||
onSuccess: (data) => {
|
||||
utils.viewer.dsync.teamGroupMapping.get.setData(undefined, (prev) => {
|
||||
if (prev) {
|
||||
const teamIndex = prev.teamGroupMapping.findIndex((team) => team.id === teamId);
|
||||
const indexToRemove = prev.teamGroupMapping[teamIndex].groupNames.indexOf(data.deletedGroupName);
|
||||
prev.teamGroupMapping[teamIndex].groupNames.splice(indexToRemove, 1);
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
showToast(`Group removed`, "success");
|
||||
},
|
||||
onError: (error) => {
|
||||
showToast(`Error removing group name${error.message}`, "error");
|
||||
},
|
||||
});
|
||||
|
||||
const addGroupName = (groupName: string) => {
|
||||
if (groupNames.some((name: string) => name === groupName)) {
|
||||
showToast(`Group name already added`, "error");
|
||||
return;
|
||||
}
|
||||
|
||||
createMutation.mutate({ teamId: teamId, name: groupName, directoryId: directoryId });
|
||||
};
|
||||
|
||||
const removeGroupName = (groupName: string) => {
|
||||
deleteMutation.mutate({
|
||||
teamId: teamId,
|
||||
groupName: groupName,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-4">
|
||||
{groupNames.map((name) => (
|
||||
<Badge variant="gray" size="lg" key={name} className="h-8 py-4">
|
||||
<div className="flex items-center space-x-2 ">
|
||||
<p>{name}</p>
|
||||
<div className="hover:bg-emphasis rounded p-1">
|
||||
<Icon name="x" className="h-4 w-4 stroke-[3px]" onClick={() => removeGroupName(name)} />
|
||||
</div>
|
||||
</div>
|
||||
</Badge>
|
||||
))}
|
||||
<Badge variant="gray" size="lg" className={classNames(!showTextInput && "hover:bg-emphasis")}>
|
||||
<div
|
||||
className="flex items-center space-x-1"
|
||||
onClick={() => {
|
||||
if (!showTextInput) setShowTextInput(true);
|
||||
}}>
|
||||
{showTextInput ? (
|
||||
<TextField
|
||||
autoFocus
|
||||
className="mb-0 h-6"
|
||||
onBlur={() => {
|
||||
if (!newGroupName) setShowTextInput(false);
|
||||
}}
|
||||
onChange={(e) => setNewGroupName(e.target.value)}
|
||||
value={newGroupName}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
addGroupName(newGroupName);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<p>{t("add_group_name")}</p>
|
||||
)}
|
||||
<div className={classNames("rounded p-1", showTextInput && "hover:bg-emphasis ml-2")}>
|
||||
<Icon name="plus" className="h-4 w-4 stroke-[3px]" onClick={() => addGroupName(newGroupName)} />
|
||||
</div>
|
||||
</div>
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupNameCell;
|
||||
@@ -0,0 +1,60 @@
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { DataTable, Button } from "@calcom/ui";
|
||||
|
||||
import CreateTeamDialog from "./CreateTeamDialog";
|
||||
import GroupNameCell from "./GroupNameCell";
|
||||
|
||||
interface TeamGroupMapping {
|
||||
name: string;
|
||||
id: number;
|
||||
groupNames: string[];
|
||||
directoryId: string;
|
||||
}
|
||||
|
||||
const GroupTeamMappingTable = () => {
|
||||
const { t } = useLocale();
|
||||
const [createTeamDialogOpen, setCreateTeamDialogOpen] = useState(false);
|
||||
|
||||
const { data } = trpc.viewer.dsync.teamGroupMapping.get.useQuery();
|
||||
|
||||
const tableContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const columns: ColumnDef<TeamGroupMapping>[] = [
|
||||
{
|
||||
id: "name",
|
||||
header: t("team"),
|
||||
cell: ({ row }) => {
|
||||
const { name } = row.original;
|
||||
|
||||
return <p>{name}</p>;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "group",
|
||||
header: t("group_name"),
|
||||
cell: ({ row }) => {
|
||||
const { id, groupNames, directoryId } = row.original;
|
||||
|
||||
return <GroupNameCell groupNames={groupNames} teamId={id} directoryId={directoryId} />;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<DataTable
|
||||
data={data ? data.teamGroupMapping : []}
|
||||
tableContainerRef={tableContainerRef}
|
||||
columns={columns}
|
||||
tableCTA={<Button onClick={() => setCreateTeamDialogOpen(true)}>Create team</Button>}
|
||||
/>
|
||||
<CreateTeamDialog open={createTeamDialogOpen} onOpenChange={setCreateTeamDialogOpen} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupTeamMappingTable;
|
||||
Reference in New Issue
Block a user