2
0

first commit

This commit is contained in:
2024-08-09 00:39:27 +02:00
commit 79688abe2e
5698 changed files with 497838 additions and 0 deletions

View File

@@ -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;

View 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;

View File

@@ -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;

View File

@@ -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;

View 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;

View File

@@ -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;