feat: github importer (#722)

* chore: github importer first step completed

* refactor: github importer code refactored

* chore: github importer functionality completed

* fix: import data step saved data
This commit is contained in:
Aaryan Khandelwal 2023-04-06 00:51:15 +05:30 committed by GitHub
parent 6b8b981e1d
commit c9d8a8dbd1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 1211 additions and 718 deletions

View file

@ -1,5 +1,8 @@
import { FC, useRef, useState } from "react";
// ui
import { PrimaryButton } from "components/ui";
type Props = {
workspaceSlug: string | undefined;
workspaceIntegration: any;
@ -39,21 +42,11 @@ export const GithubAuth: FC<Props> = ({ workspaceSlug, workspaceIntegration }) =
return (
<div>
{workspaceIntegration && workspaceIntegration?.id ? (
<button
type="button"
className={`cursor-not-allowed rounded-sm bg-theme bg-opacity-80 px-3 py-1.5 text-sm text-white transition-colors`}
>
Successfully Connected
</button>
<PrimaryButton disabled>Successfully Connected</PrimaryButton>
) : (
<button
onClick={startAuth}
type="button"
className={`rounded-sm bg-theme px-3 py-1.5 text-sm text-white transition-colors hover:bg-opacity-80`}
disabled={authLoader}
>
<PrimaryButton onClick={startAuth} loading={authLoader}>
{authLoader ? "Connecting..." : "Connect"}
</button>
</PrimaryButton>
)}
</div>
);

View file

@ -1,74 +0,0 @@
import { FC } from "react";
// components
import { IIntegrationData, GithubAuth } from "components/integration";
// types
import { IAppIntegrations } from "types";
type Props = {
state: IIntegrationData;
provider: string | undefined;
handleState: (key: string, valve: any) => void;
workspaceSlug: string | undefined;
allIntegrations: IAppIntegrations[] | undefined;
allIntegrationsError: Error | undefined;
allWorkspaceIntegrations: any | undefined;
allWorkspaceIntegrationsError: Error | undefined;
};
export const GithubConfigure: FC<Props> = ({
state,
handleState,
workspaceSlug,
provider,
allIntegrations,
allIntegrationsError,
allWorkspaceIntegrations,
allWorkspaceIntegrationsError,
}) => {
// current integration from all the integrations available
const integration =
allIntegrations &&
allIntegrations.length > 0 &&
allIntegrations.find((_integration) => _integration.provider === provider);
// current integration from workspace integrations
const workspaceIntegration =
integration &&
allWorkspaceIntegrations &&
allWorkspaceIntegrations.length > 0 &&
allWorkspaceIntegrations.find(
(_integration: any) => _integration.integration_detail.id === integration.id
);
console.log("integration", integration);
console.log("workspaceIntegration", workspaceIntegration);
return (
<div className="space-y-5">
<div className="flex items-center gap-2 py-5">
<div className="w-full">
<div className="font-medium">Configure</div>
<div className="text-sm text-gray-600">Set up your Github import</div>
</div>
<div className="flex-shrink-0">
<GithubAuth workspaceSlug={workspaceSlug} workspaceIntegration={workspaceIntegration} />
</div>
</div>
<div className="flex items-center justify-end">
<button
type="button"
className={`rounded-sm bg-theme px-3 py-1.5 text-sm text-white transition-colors hover:bg-opacity-80 ${
workspaceIntegration && workspaceIntegration?.id
? `bg-opacity-100`
: `cursor-not-allowed bg-opacity-80`
}`}
onClick={() => handleState("state", "import-import-data")}
disabled={workspaceIntegration && workspaceIntegration?.id ? false : true}
>
Next
</button>
</div>
</div>
);
};

View file

@ -1,27 +0,0 @@
import { FC } from "react";
// types
import { IIntegrationData } from "components/integration";
type Props = { state: IIntegrationData; handleState: (key: string, valve: any) => void };
export const GithubConfirm: FC<Props> = ({ state, handleState }) => (
<>
<div>Confirm</div>
<div className="mt-5 flex items-center justify-between">
<button
type="button"
className={`rounded-sm bg-gray-300 px-3 py-1.5 text-sm transition-colors hover:bg-opacity-80`}
onClick={() => handleState("state", "migrate-users")}
>
Back
</button>
<button
type="button"
className={`rounded-sm bg-theme px-3 py-1.5 text-sm text-white transition-colors hover:bg-opacity-80`}
onClick={() => handleState("state", "migrate-confirm")}
>
Next
</button>
</div>
</>
);

View file

@ -0,0 +1,66 @@
import { FC } from "react";
import { useRouter } from "next/router";
// components
import { GithubAuth, TIntegrationSteps } from "components/integration";
// ui
import { PrimaryButton } from "components/ui";
// types
import { IAppIntegrations, IWorkspaceIntegrations } from "types";
type Props = {
provider: string | undefined;
handleStepChange: (value: TIntegrationSteps) => void;
appIntegrations: IAppIntegrations[] | undefined;
workspaceIntegrations: IWorkspaceIntegrations[] | undefined;
};
export const GithubImportConfigure: FC<Props> = ({
handleStepChange,
provider,
appIntegrations,
workspaceIntegrations,
}) => {
const router = useRouter();
const { workspaceSlug } = router.query;
// current integration from all the integrations available
const integration =
appIntegrations &&
appIntegrations.length > 0 &&
appIntegrations.find((i) => i.provider === provider);
// current integration from workspace integrations
const workspaceIntegration =
integration &&
workspaceIntegrations &&
workspaceIntegrations.length > 0 &&
workspaceIntegrations.find((i: any) => i.integration_detail.id === integration.id);
return (
<div className="space-y-6">
<div className="flex items-center gap-2 py-5">
<div className="w-full">
<div className="font-medium">Configure</div>
<div className="text-sm text-gray-600">Set up your GitHub import.</div>
</div>
<div className="flex-shrink-0">
<GithubAuth
workspaceSlug={workspaceSlug as string}
workspaceIntegration={workspaceIntegration}
/>
</div>
</div>
<div className="flex items-center justify-end">
<PrimaryButton
onClick={() => handleStepChange("import-data")}
disabled={workspaceIntegration && workspaceIntegration?.id ? false : true}
>
Next
</PrimaryButton>
</div>
</div>
);
};

View file

@ -0,0 +1,27 @@
import { FC } from "react";
// react-hook-form
import { UseFormWatch } from "react-hook-form";
// ui
import { PrimaryButton, SecondaryButton } from "components/ui";
// types
import { TFormValues, TIntegrationSteps } from "components/integration";
type Props = {
handleStepChange: (value: TIntegrationSteps) => void;
watch: UseFormWatch<TFormValues>;
};
export const GithubImportConfirm: FC<Props> = ({ handleStepChange, watch }) => (
<div className="mt-6">
<h4 className="font-medium">
You are about to import issues from {watch("github").full_name}. Click on {'"'}Confirm &
Import{'" '}
to complete the process.
</h4>
<div className="mt-6 flex items-center justify-between">
<SecondaryButton onClick={() => handleStepChange("import-users")}>Back</SecondaryButton>
<PrimaryButton type="submit">Confirm & Import</PrimaryButton>
</div>
</div>
);

View file

@ -1,27 +1,127 @@
import { FC } from "react";
import { FC, useState } from "react";
// react-hook-form
import { Control, Controller, UseFormWatch } from "react-hook-form";
// hooks
import useProjects from "hooks/use-projects";
// components
import { SelectRepository, TFormValues, TIntegrationSteps } from "components/integration";
// ui
import { CustomSearchSelect, PrimaryButton, SecondaryButton } from "components/ui";
// helpers
import { truncateText } from "helpers/string.helper";
// types
import { IIntegrationData } from "components/integration";
import { IWorkspaceIntegrations } from "types";
type Props = { state: IIntegrationData; handleState: (key: string, valve: any) => void };
type Props = {
handleStepChange: (value: TIntegrationSteps) => void;
integration: IWorkspaceIntegrations | false | undefined;
control: Control<TFormValues, any>;
watch: UseFormWatch<TFormValues>;
};
export const GithubImportData: FC<Props> = ({ state, handleState }) => (
<div>
<div>Import Data</div>
<div className="mt-5 flex items-center justify-between">
<button
type="button"
className={`rounded-sm bg-gray-300 px-3 py-1.5 text-sm transition-colors hover:bg-opacity-80`}
onClick={() => handleState("state", "import-configure")}
>
Back
</button>
<button
type="button"
className={`rounded-sm bg-theme px-3 py-1.5 text-sm text-white transition-colors hover:bg-opacity-80`}
onClick={() => handleState("state", "migrate-issues")}
>
Next
</button>
export const GithubImportData: FC<Props> = ({ handleStepChange, integration, control, watch }) => {
const { projects } = useProjects();
const options =
projects.map((project) => ({
value: project.id,
query: project.name,
content: <p>{truncateText(project.name, 25)}</p>,
})) ?? [];
return (
<div className="mt-6">
<div className="space-y-8">
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-8">
<h4 className="font-semibold">Select Repository</h4>
<p className="text-gray-500 text-xs">
Select the repository that you want the issues to be imported from.
</p>
</div>
<div className="col-span-12 sm:col-span-4">
{integration && (
<Controller
control={control}
name="github"
render={({ field: { value, onChange } }) => (
<SelectRepository
integration={integration}
value={value ? value.id : null}
label={value ? `${value.full_name}` : "Select Repository"}
onChange={onChange}
characterLimit={50}
/>
)}
/>
)}
</div>
</div>
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-8">
<h4 className="font-semibold">Select Project</h4>
<p className="text-gray-500 text-xs">Select the project to import the issues to.</p>
</div>
<div className="col-span-12 sm:col-span-4">
{projects && (
<Controller
control={control}
name="project"
render={({ field: { value, onChange } }) => (
<CustomSearchSelect
value={value}
label={value ? projects.find((p) => p.id === value)?.name : "Select Project"}
onChange={onChange}
options={options}
optionsClassName="w-full"
/>
)}
/>
)}
</div>
</div>
<div className="grid grid-cols-12 gap-4 sm:gap-16">
<div className="col-span-12 sm:col-span-8">
<h4 className="font-semibold">Sync Issues</h4>
<p className="text-gray-500 text-xs">Set whether you want to sync the issues or not.</p>
</div>
<div className="col-span-12 sm:col-span-4">
<Controller
control={control}
name="sync"
render={({ field: { value, onChange } }) => (
<button
type="button"
className={`relative inline-flex h-3.5 w-6 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
value ? "bg-green-500" : "bg-gray-200"
}`}
role="switch"
aria-checked={value ? true : false}
onClick={() => onChange(!value)}
>
<span className="sr-only">Show empty groups</span>
<span
aria-hidden="true"
className={`inline-block h-2.5 w-2.5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
value ? "translate-x-2.5" : "translate-x-0"
}`}
/>
</button>
)}
/>
</div>
</div>
</div>
<div className="mt-6 flex items-center justify-end gap-2">
<SecondaryButton onClick={() => handleStepChange("import-configure")}>Back</SecondaryButton>
<PrimaryButton
onClick={() => handleStepChange("repo-details")}
disabled={!watch("github") || !watch("project")}
>
Next
</PrimaryButton>
</div>
</div>
</div>
);
);
};

View file

@ -0,0 +1,59 @@
import { FC } from "react";
import { useRouter } from "next/router";
// react-hook-form
import { UseFormWatch } from "react-hook-form";
// ui
import { PrimaryButton, SecondaryButton } from "components/ui";
// types
import {
IUserDetails,
SingleUserSelect,
TFormValues,
TIntegrationSteps,
} from "components/integration";
type Props = {
handleStepChange: (value: TIntegrationSteps) => void;
users: IUserDetails[];
setUsers: React.Dispatch<React.SetStateAction<IUserDetails[]>>;
watch: UseFormWatch<TFormValues>;
};
export const GithubImportUsers: FC<Props> = ({ handleStepChange, users, setUsers, watch }) => {
const router = useRouter();
const isInvalid = users.filter((u) => u.import !== false && u.email === "").length > 0;
return (
<div className="mt-6">
<div>
<div className="grid grid-cols-3 gap-2 text-sm mb-2 font-medium">
<div>Name</div>
<div>Import as...</div>
<div className="text-right">
{users.filter((u) => u.import !== false).length} users selected
</div>
</div>
<div className="space-y-2">
{watch("collaborators").map((collaborator, index) => (
<SingleUserSelect
key={collaborator.id}
collaborator={collaborator}
index={index}
users={users}
setUsers={setUsers}
/>
))}
</div>
</div>
<div className="mt-6 flex items-center justify-end gap-2">
<SecondaryButton onClick={() => handleStepChange("repo-details")}>Back</SecondaryButton>
<PrimaryButton onClick={() => handleStepChange("import-confirm")} disabled={isInvalid}>
Next
</PrimaryButton>
</div>
</div>
);
};

View file

@ -1,27 +0,0 @@
import { FC } from "react";
// types
import { IIntegrationData } from "components/integration";
type Props = { state: IIntegrationData; handleState: (key: string, valve: any) => void };
export const GithubIssuesSelect: FC<Props> = ({ state, handleState }) => (
<div>
<div>Issues Select</div>
<div className="mt-5 flex items-center justify-between">
<button
type="button"
className={`rounded-sm bg-gray-300 px-3 py-1.5 text-sm transition-colors hover:bg-opacity-80`}
onClick={() => handleState("state", "import-import-data")}
>
Back
</button>
<button
type="button"
className={`rounded-sm bg-theme px-3 py-1.5 text-sm text-white transition-colors hover:bg-opacity-80`}
onClick={() => handleState("state", "migrate-users")}
>
Next
</button>
</div>
</div>
);

View file

@ -0,0 +1,105 @@
import { FC, useEffect } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
// react-hook-form
import { UseFormSetValue } from "react-hook-form";
// services
import GithubIntegrationService from "services/integration/github.service";
// ui
import { Loader, PrimaryButton, SecondaryButton } from "components/ui";
// types
import { IUserDetails, TFormValues, TIntegrationSteps } from "components/integration";
// fetch-keys
import { GITHUB_REPOSITORY_INFO } from "constants/fetch-keys";
type Props = {
selectedRepo: any;
handleStepChange: (value: TIntegrationSteps) => void;
setUsers: React.Dispatch<React.SetStateAction<IUserDetails[]>>;
setValue: UseFormSetValue<TFormValues>;
};
export const GithubRepoDetails: FC<Props> = ({
selectedRepo,
handleStepChange,
setUsers,
setValue,
}) => {
const router = useRouter();
const { workspaceSlug } = router.query;
const { data: repoInfo } = useSWR(
workspaceSlug && selectedRepo
? GITHUB_REPOSITORY_INFO(workspaceSlug as string, selectedRepo.name)
: null,
workspaceSlug && selectedRepo
? () =>
GithubIntegrationService.getGithubRepoInfo(workspaceSlug as string, {
owner: selectedRepo.owner.login,
repo: selectedRepo.name,
})
: null
);
useEffect(() => {
if (!repoInfo) return;
setValue("collaborators", repoInfo.collaborators);
const fetchedUsers = repoInfo.collaborators.map((collaborator) => ({
username: collaborator.login,
import: "map",
email: "",
}));
setUsers(fetchedUsers);
}, [repoInfo, setUsers, setValue]);
return (
<div className="mt-6">
{repoInfo ? (
repoInfo.issue_count > 0 ? (
<div className="flex items-center justify-between gap-4">
<div>
<div className="font-medium">Repository Details</div>
<div className="text-sm text-gray-600">Import completed. We have found:</div>
</div>
<div className="flex gap-16 mt-4">
<div className="text-center flex-shrink-0">
<p className="text-3xl font-bold">{repoInfo.issue_count}</p>
<h6 className="text-sm text-gray-500">Issues</h6>
</div>
<div className="text-center flex-shrink-0">
<p className="text-3xl font-bold">{repoInfo.labels}</p>
<h6 className="text-sm text-gray-500">Labels</h6>
</div>
<div className="text-center flex-shrink-0">
<p className="text-3xl font-bold">{repoInfo.collaborators.length}</p>
<h6 className="text-sm text-gray-500">Users</h6>
</div>
</div>
</div>
) : (
<div>
<h5>We didn{"'"}t find any issue in this repository.</h5>
</div>
)
) : (
<Loader>
<Loader.Item height="70px" />
</Loader>
)}
<div className="mt-6 flex items-center justify-end gap-2">
<SecondaryButton onClick={() => handleStepChange("import-data")}>Back</SecondaryButton>
<PrimaryButton
onClick={() => handleStepChange("import-users")}
disabled={!repoInfo || repoInfo.issue_count === 0}
>
Next
</PrimaryButton>
</div>
</div>
);
};

View file

@ -1,168 +1,271 @@
import { FC, useState } from "react";
// next imports
import Link from "next/link";
import Image from "next/image";
// icons
import GithubLogo from "public/logos/github-square.png";
import { CogIcon, CloudUploadIcon, UsersIcon, ImportLayersIcon, CheckIcon } from "components/icons";
import { ArrowLeftIcon } from "@heroicons/react/24/outline";
import { useRouter } from "next/router";
import { mutate } from "swr";
// react-hook-form
import { useForm } from "react-hook-form";
// services
import GithubIntegrationService from "services/integration/github.service";
// hooks
import useToast from "hooks/use-toast";
// components
import {
GithubConfigure,
GithubImportConfigure,
GithubImportData,
GithubIssuesSelect,
GithubUsersSelect,
GithubConfirm,
GithubRepoDetails,
GithubImportUsers,
GithubImportConfirm,
} from "components/integration";
// icons
import { CogIcon, CloudUploadIcon, UsersIcon, CheckIcon } from "components/icons";
import { ArrowLeftIcon, ListBulletIcon } from "@heroicons/react/24/outline";
// images
import GithubLogo from "public/logos/github-square.png";
// types
import { IAppIntegrations } from "types";
import {
IAppIntegrations,
IGithubRepoCollaborator,
IGithubServiceImportFormData,
IWorkspaceIntegrations,
} from "types";
// fetch-keys
import { IMPORTER_SERVICES_LIST } from "constants/fetch-keys";
type Props = {
workspaceSlug: string | undefined;
provider: string | undefined;
allIntegrations: IAppIntegrations[] | undefined;
allIntegrationsError: Error | undefined;
allWorkspaceIntegrations: any | undefined;
allWorkspaceIntegrationsError: Error | undefined;
allIntegrationImporters: any | undefined;
allIntegrationImportersError: Error | undefined;
appIntegrations: IAppIntegrations[] | undefined;
workspaceIntegrations: IWorkspaceIntegrations[] | undefined;
};
export type TIntegrationSteps =
| "import-configure"
| "import-data"
| "repo-details"
| "import-users"
| "import-confirm";
export interface IIntegrationData {
state: string;
state: TIntegrationSteps;
}
export interface IUserDetails {
username: string;
import: any;
email: string;
}
export type TFormValues = {
github: any;
project: string | null;
sync: boolean;
collaborators: IGithubRepoCollaborator[];
users: IUserDetails[];
};
const defaultFormValues = {
github: null,
project: null,
sync: false,
};
const integrationWorkflowData = [
{
title: "Configure",
key: "import-configure",
icon: CogIcon,
},
{
title: "Import Data",
key: "import-data",
icon: CloudUploadIcon,
},
{ title: "Issues", key: "repo-details", icon: ListBulletIcon },
{
title: "Users",
key: "import-users",
icon: UsersIcon,
},
{
title: "Confirm",
key: "import-confirm",
icon: CheckIcon,
},
];
export const GithubIntegrationRoot: FC<Props> = ({
workspaceSlug,
provider,
allIntegrations,
allIntegrationsError,
allWorkspaceIntegrations,
allWorkspaceIntegrationsError,
allIntegrationImporters,
allIntegrationImportersError,
appIntegrations,
workspaceIntegrations,
}) => {
const integrationWorkflowData = [
{
title: "Configure",
key: "import-configure",
icon: CogIcon,
},
{
title: "Import Data",
key: "import-import-data",
icon: CloudUploadIcon,
},
{ title: "Issues", key: "migrate-issues", icon: UsersIcon },
{
title: "Users",
key: "migrate-users",
icon: ImportLayersIcon,
},
{
title: "Confirm",
key: "migrate-confirm",
icon: CheckIcon,
},
];
const [currentStep, setCurrentStep] = useState<IIntegrationData>({
state: "import-configure",
});
const [users, setUsers] = useState<IUserDetails[]>([]);
const router = useRouter();
const { workspaceSlug } = router.query;
const { setToastAlert } = useToast();
const { handleSubmit, control, setValue, watch } = useForm<TFormValues>({
defaultValues: defaultFormValues,
});
const activeIntegrationState = () => {
const currentElementIndex = integrationWorkflowData.findIndex(
(_item) => _item?.key === integrationData?.state
(i) => i?.key === currentStep?.state
);
return currentElementIndex;
};
const [integrationData, setIntegrationData] = useState<IIntegrationData>({
state: "import-configure",
});
const handleIntegrationData = (key: string = "state", value: string) => {
setIntegrationData((previousData) => ({ ...previousData, [key]: value }));
const handleStepChange = (value: TIntegrationSteps) => {
setCurrentStep((prevData) => ({ ...prevData, state: value }));
};
// current integration from all the integrations available
const integration =
appIntegrations &&
appIntegrations.length > 0 &&
appIntegrations.find((i) => i.provider === provider);
// current integration from workspace integrations
const workspaceIntegration =
integration &&
workspaceIntegrations?.find((i: any) => i.integration_detail.id === integration.id);
const createGithubImporterService = async (formData: TFormValues) => {
if (!formData.github || !formData.project) return;
const payload: IGithubServiceImportFormData = {
metadata: {
owner: formData.github.owner.login,
name: formData.github.name,
repository_id: formData.github.id,
url: formData.github.html_url,
},
data: {
users: users,
},
config: {
sync: formData.sync,
},
project_id: formData.project,
};
await GithubIntegrationService.createGithubServiceImport(workspaceSlug as string, payload)
.then(() => {
router.push(`/${workspaceSlug}/settings/import-export`);
mutate(IMPORTER_SERVICES_LIST(workspaceSlug as string));
})
.catch(() =>
setToastAlert({
type: "error",
title: "Error!",
message: "Import was unsuccessful. Please try again.",
})
);
};
return (
<div className="space-y-4">
<Link href={`/${workspaceSlug}/settings/import-export`}>
<div className="inline-flex cursor-pointer items-center gap-2 text-sm font-medium text-gray-600 hover:text-gray-900">
<div>
<ArrowLeftIcon className="h-3 w-3" />
<form onSubmit={handleSubmit(createGithubImporterService)}>
<div className="space-y-2">
<Link href={`/${workspaceSlug}/settings/import-export`}>
<div className="inline-flex cursor-pointer items-center gap-2 text-sm font-medium text-gray-600 hover:text-gray-900">
<div>
<ArrowLeftIcon className="h-3 w-3" />
</div>
<div>Cancel import & go back</div>
</div>
<div>Back</div>
</div>
</Link>
</Link>
<div className="space-y-4 rounded border border-gray-200 bg-white p-4">
<div className="flex items-center gap-2">
<div className="h-10 w-10 flex-shrink-0">
<Image src={GithubLogo} alt="GithubLogo" />
</div>
<div className="flex h-full w-full items-center justify-center">
{integrationWorkflowData.map((_integration, _idx) => (
<>
<div
key={_integration?.key}
className={`flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full border
${
_idx <= activeIntegrationState()
? `border-[#3F76FF] bg-[#3F76FF] text-white ${
_idx === activeIntegrationState()
? `border-opacity-100 bg-opacity-100`
: `border-opacity-80 bg-opacity-80`
}`
: `border-gray-300`
}
`}
>
<_integration.icon
width={`18px`}
height={`18px`}
color={_idx <= activeIntegrationState() ? "#ffffff" : "#d1d5db"}
/>
</div>
{_idx < integrationWorkflowData.length - 1 && (
<div className="space-y-4 rounded-[10px] border border-gray-200 bg-white p-4">
<div className="flex items-center gap-2">
<div className="h-10 w-10 flex-shrink-0">
<Image src={GithubLogo} alt="GithubLogo" />
</div>
<div className="flex h-full w-full items-center justify-center">
{integrationWorkflowData.map((integration, index) => (
<>
<div
key={_idx}
className={`border-b px-7 ${
_idx <= activeIntegrationState() - 1 ? `border-[#3F76FF]` : `border-gray-300`
key={integration.key}
className={`flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full border ${
index <= activeIntegrationState()
? `border-[#3F76FF] bg-[#3F76FF] text-white ${
index === activeIntegrationState()
? "border-opacity-100 bg-opacity-100"
: "border-opacity-80 bg-opacity-80"
}`
: "border-gray-300"
}`}
>
{" "}
<integration.icon
width="18px"
height="18px"
color={index <= activeIntegrationState() ? "#ffffff" : "#d1d5db"}
/>
</div>
)}
</>
))}
{index < integrationWorkflowData.length - 1 && (
<div
key={index}
className={`border-b px-7 ${
index <= activeIntegrationState() - 1
? `border-[#3F76FF]`
: `border-gray-300`
}`}
>
{" "}
</div>
)}
</>
))}
</div>
</div>
</div>
<div className="relative w-full space-y-4 overflow-hidden">
<div className="w-full">
{integrationData?.state === "import-configure" && (
<GithubConfigure
state={integrationData}
handleState={handleIntegrationData}
workspaceSlug={workspaceSlug}
provider={provider}
allIntegrations={allIntegrations}
allIntegrationsError={allIntegrationsError}
allWorkspaceIntegrations={allWorkspaceIntegrations}
allWorkspaceIntegrationsError={allWorkspaceIntegrationsError}
/>
)}
{integrationData?.state === "import-import-data" && (
<GithubImportData state={integrationData} handleState={handleIntegrationData} />
)}
{integrationData?.state === "migrate-issues" && (
<GithubIssuesSelect state={integrationData} handleState={handleIntegrationData} />
)}
{integrationData?.state === "migrate-users" && (
<GithubUsersSelect state={integrationData} handleState={handleIntegrationData} />
)}
{integrationData?.state === "migrate-confirm" && (
<GithubConfirm state={integrationData} handleState={handleIntegrationData} />
)}
<div className="relative w-full space-y-4">
<div className="w-full">
{currentStep?.state === "import-configure" && (
<GithubImportConfigure
handleStepChange={handleStepChange}
provider={provider}
appIntegrations={appIntegrations}
workspaceIntegrations={workspaceIntegrations}
/>
)}
{currentStep?.state === "import-data" && (
<GithubImportData
handleStepChange={handleStepChange}
integration={workspaceIntegration}
control={control}
watch={watch}
/>
)}
{currentStep?.state === "repo-details" && (
<GithubRepoDetails
selectedRepo={watch("github")}
handleStepChange={handleStepChange}
setUsers={setUsers}
setValue={setValue}
/>
)}
{currentStep?.state === "import-users" && (
<GithubImportUsers
handleStepChange={handleStepChange}
users={users}
setUsers={setUsers}
watch={watch}
/>
)}
{currentStep?.state === "import-confirm" && (
<GithubImportConfirm handleStepChange={handleStepChange} watch={watch} />
)}
</div>
</div>
</div>
</div>
</div>
</form>
);
};

View file

@ -0,0 +1,95 @@
import React from "react";
import { useRouter } from "next/router";
import useSWRInfinite from "swr/infinite";
// services
import projectService from "services/project.service";
// ui
import { CustomSearchSelect } from "components/ui";
// helpers
import { truncateText } from "helpers/string.helper";
// types
import { IWorkspaceIntegrations } from "types";
type Props = {
integration: IWorkspaceIntegrations;
value: any;
label: string;
onChange: (repo: any) => void;
characterLimit?: number;
};
export const SelectRepository: React.FC<Props> = ({
integration,
value,
label,
onChange,
characterLimit = 25,
}) => {
const router = useRouter();
const { workspaceSlug } = router.query;
const getKey = (pageIndex: number) => {
if (!workspaceSlug || !integration) return;
return `${
process.env.NEXT_PUBLIC_API_BASE_URL
}/api/workspaces/${workspaceSlug}/workspace-integrations/${
integration.id
}/github-repositories/?page=${++pageIndex}`;
};
const fetchGithubRepos = async (url: string) => {
const data = await projectService.getGithubRepositories(url);
return data;
};
const {
data: paginatedData,
size,
setSize,
isValidating,
} = useSWRInfinite(getKey, fetchGithubRepos);
const userRepositories = (paginatedData ?? []).map((data) => data.repositories).flat();
const totalCount = paginatedData && paginatedData.length > 0 ? paginatedData[0].total_count : 0;
const options =
userRepositories.map((repo) => ({
value: repo.id,
query: repo.full_name,
content: <p>{truncateText(repo.full_name, characterLimit)}</p>,
})) ?? [];
return (
<CustomSearchSelect
value={value}
options={options}
onChange={(val: string) => {
const repo = userRepositories.find((repo) => repo.id === val);
onChange(repo);
}}
label={label}
footerOption={
<>
{userRepositories && options.length < totalCount && (
<button
type="button"
className="w-full p-1 text-center text-[0.6rem] text-gray-500 hover:bg-hover-gray"
onClick={() => setSize(size + 1)}
disabled={isValidating}
>
{isValidating ? "Loading..." : "Click to load more..."}
</button>
)}
</>
}
position="right"
optionsClassName="w-full"
/>
);
};

View file

@ -0,0 +1,133 @@
import Image from "next/image";
import { useRouter } from "next/router";
import useSWR from "swr";
// services
import workspaceService from "services/workspace.service";
// ui
import { Avatar, CustomSearchSelect, CustomSelect, Input } from "components/ui";
// types
import { IGithubRepoCollaborator } from "types";
import { IUserDetails } from "./root";
// fetch-keys
import { WORKSPACE_MEMBERS } from "constants/fetch-keys";
type Props = {
collaborator: IGithubRepoCollaborator;
index: number;
users: IUserDetails[];
setUsers: React.Dispatch<React.SetStateAction<IUserDetails[]>>;
};
const importOptions = [
{
key: "map",
label: "Map to existing",
},
{
key: "invite",
label: "Invite by email",
},
{
key: false,
label: "Do not import",
},
];
export const SingleUserSelect: React.FC<Props> = ({ collaborator, index, users, setUsers }) => {
const router = useRouter();
const { workspaceSlug } = router.query;
const { data: members } = useSWR(
workspaceSlug ? WORKSPACE_MEMBERS(workspaceSlug as string) : null,
workspaceSlug ? () => workspaceService.workspaceMembers(workspaceSlug as string) : null
);
const options =
members?.map((member) => ({
value: member.member.email,
query:
(member.member.first_name && member.member.first_name !== ""
? member.member.first_name
: member.member.email) +
" " +
member.member.last_name ?? "",
content: (
<div className="flex items-center gap-2">
<Avatar user={member.member} />
{member.member.first_name && member.member.first_name !== ""
? member.member.first_name + "(" + member.member.email + ")"
: member.member.email}
</div>
),
})) ?? [];
return (
<div className="bg-gray-50 px-2 py-3 rounded-md grid grid-cols-3 items-center gap-2">
<div className="flex items-center gap-2">
<div className="relative h-8 w-8 rounded flex-shrink-0">
<Image
src={collaborator.avatar_url}
layout="fill"
objectFit="cover"
className="rounded"
alt={`${collaborator.login} GitHub user`}
/>
</div>
<p className="text-sm">{collaborator.login}</p>
</div>
<div>
<CustomSelect
value={users[index].import}
label={
<div className="text-xs">
{importOptions.find((o) => o.key === users[index].import)?.label}
</div>
}
onChange={(val: any) => {
const newUsers = [...users];
newUsers[index].import = val;
newUsers[index].email = "";
setUsers(newUsers);
}}
optionsClassName="w-full"
noChevron
>
{importOptions.map((option) => (
<CustomSelect.Option key={option.label} value={option.key}>
<div>{option.label}</div>
</CustomSelect.Option>
))}
</CustomSelect>
</div>
{users[index].import === "invite" && (
<Input
type="email"
name={`userEmail${index}`}
value={users[index].email}
onChange={(e) => {
const newUsers = [...users];
newUsers[index].email = e.target.value;
setUsers(newUsers);
}}
placeholder="Enter email of the user"
className="py-1 border-gray-200 text-xs"
/>
)}
{users[index].import === "map" && members && (
<CustomSearchSelect
value={users[index].email}
label={users[index].email !== "" ? users[index].email : "Select user from project"}
options={options}
onChange={(val: string) => {
const newUsers = [...users];
newUsers[index].email = val;
setUsers(newUsers);
}}
optionsClassName="w-full"
/>
)}
</div>
);
};

View file

@ -1,27 +0,0 @@
import { FC } from "react";
// types
import { IIntegrationData } from "components/integration";
type Props = { state: IIntegrationData; handleState: (key: string, valve: any) => void };
export const GithubUsersSelect: FC<Props> = ({ state, handleState }) => (
<div>
<div>Users Select</div>
<div className="mt-5 flex items-center justify-between">
<button
type="button"
className={`rounded-sm bg-gray-300 px-3 py-1.5 text-sm transition-colors hover:bg-opacity-80`}
onClick={() => handleState("state", "migrate-issues")}
>
Back
</button>
<button
type="button"
className={`rounded-sm bg-theme px-3 py-1.5 text-sm text-white transition-colors hover:bg-opacity-80`}
onClick={() => handleState("state", "migrate-confirm")}
>
Next
</button>
</div>
</div>
);

View file

@ -1,162 +1,198 @@
import { FC } from "react";
// next imports
import { FC, useState } from "react";
import Link from "next/link";
import Image from "next/image";
import { useRouter } from "next/router";
import { mutate } from "swr";
// icons
import { ArrowRightIcon } from "components/icons";
import GithubLogo from "public/logos/github-square.png";
import { ChevronDownIcon } from "@heroicons/react/24/outline";
// components
import { Loader } from "components/ui";
import { GithubIntegrationRoot } from "components/integration";
// icons
import { ArrowPathIcon } from "@heroicons/react/24/outline";
// ui
import { Loader, PrimaryButton, SecondaryButton } from "components/ui";
// images
import GithubLogo from "public/logos/github-square.png";
// helpers
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
// types
import { IAppIntegrations } from "types";
import { IAppIntegrations, IImporterService, IWorkspaceIntegrations } from "types";
// fetch-keys
import { IMPORTER_SERVICES_LIST } from "constants/fetch-keys";
type Props = {
workspaceSlug: string | undefined;
provider: string | undefined;
allIntegrations: IAppIntegrations[] | undefined;
allIntegrationsError: Error | undefined;
allWorkspaceIntegrations: any | undefined;
allWorkspaceIntegrationsError: Error | undefined;
allIntegrationImporters: any | undefined;
allIntegrationImportersError: Error | undefined;
appIntegrations: IAppIntegrations[] | undefined;
workspaceIntegrations: IWorkspaceIntegrations[] | undefined;
importerServices: IImporterService[] | undefined;
};
const importersList: { [key: string]: string } = {
github: "GitHub",
};
const IntegrationGuide: FC<Props> = ({
workspaceSlug,
provider,
allIntegrations,
allIntegrationsError,
allWorkspaceIntegrations,
allWorkspaceIntegrationsError,
allIntegrationImporters,
allIntegrationImportersError,
}) => (
<div className="space-y-5">
{!provider && (
<>
<div className="text-2xl font-semibold">Import</div>
appIntegrations,
workspaceIntegrations,
importerServices,
}) => {
const [refreshing, setRefreshing] = useState(false);
<div className="flex items-center gap-2">
<div className="h-full w-full space-y-1">
<div className="text-lg font-medium">Relocation Guide</div>
<div className="text-sm">
You can now transfer all the issues that youve created in other tracking services.
This tool will guide you to relocate the issue to Plane.
const router = useRouter();
const { workspaceSlug } = router.query;
return (
<div className="space-y-5">
{!provider && (
<>
<div className="flex items-center gap-2">
<div className="h-full w-full space-y-1">
<div className="text-lg font-medium">Relocation Guide</div>
<div className="text-sm">
You can now transfer all the issues that you{"'"}ve created in other tracking
services. This tool will guide you to relocate the issue to Plane.
</div>
</div>
<div className="flex flex-shrink-0 cursor-pointer items-center gap-2 text-sm font-medium text-[#3F76FF] hover:text-opacity-80">
<div>Read More</div>
<div>
<ArrowRightIcon width={"18px"} color={"#3F76FF"} />
</div>
</div>
</div>
<div className="flex flex-shrink-0 cursor-pointer items-center gap-2 text-sm font-medium text-[#3F76FF] hover:text-opacity-80">
<div>Read More</div>
<div>
<ArrowRightIcon width={"18px"} color={"#3F76FF"} />
</div>
</div>
</div>
<div>
{allIntegrations && !allIntegrationsError ? (
<>
{allIntegrations && allIntegrations.length > 0 ? (
<>
{allIntegrations.map((_integration, _idx) => (
<div
key={_idx}
className="space-y-4 rounded border border-gray-200 bg-white p-4"
>
<div className="flex items-center gap-4 whitespace-nowrap">
<div className="h-[40px] w-[40px] flex-shrink-0">
{_integration?.provider === "github" && (
<Image src={GithubLogo} alt="GithubLogo" />
)}
</div>
<div className="w-full space-y-1">
<div className="flex items-center gap-2 font-medium">
<div>{_integration?.title}</div>
<div className="rounded-full border border-gray-200 bg-gray-200 px-3 text-[12px]">
0
</div>
</div>
<div className="text-sm text-gray-500">
Activate GitHub integrations on individual projects to sync with
specific repositories.
</div>
</div>
<div className="flex-shrink-0">
<Link href={`/${workspaceSlug}/settings/import-export?provider=github`}>
<button
type="button"
className="w-full rounded bg-[#3F76FF] py-1.5 px-4 text-center text-sm text-white hover:bg-opacity-90"
>
Integrate Now
</button>
</Link>
</div>
<div className="flex h-[24px] w-[24px] flex-shrink-0 cursor-pointer items-center justify-center rounded-sm bg-gray-100 hover:bg-gray-200">
<ChevronDownIcon className="h-4 w-4" />
</div>
</div>
<div>
{allIntegrationImporters && !allIntegrationImportersError ? (
<>
{allIntegrationImporters && allIntegrationImporters.length > 0 ? (
<></>
) : (
<div className="py-2 text-sm text-gray-800">
Previous Imports not available.
</div>
)}
</>
) : (
<div>
<Loader className="grid grid-cols-1 gap-3">
{["", ""].map((_integration, _idx) => (
<Loader.Item height="40px" width="100%" />
))}
</Loader>
</div>
<div>
{appIntegrations ? (
appIntegrations.length > 0 ? (
appIntegrations.map((integration, index) => (
<div key={index} className="rounded-[10px] border bg-white p-4">
<div className="flex items-center gap-4 whitespace-nowrap">
<div className="h-[40px] w-[40px] flex-shrink-0">
{integration?.provider === "github" && (
<Image src={GithubLogo} alt="GithubLogo" />
)}
</div>
<div className="w-full space-y-1">
<div className="flex items-center gap-2 font-medium">
<h3>{integration?.title}</h3>
</div>
<div className="text-sm text-gray-500">
Activate GitHub integrations on individual projects to sync with specific
repositories.
</div>
</div>
<div className="flex-shrink-0">
<Link href={`/${workspaceSlug}/settings/import-export?provider=github`}>
<PrimaryButton>Integrate Now</PrimaryButton>
</Link>
</div>
</div>
))}
</>
<h3 className="mt-6 mb-2 font-medium text-lg flex gap-2">
Previous Imports
<button
type="button"
className="flex-shrink-0 flex items-center gap-1 outline-none text-xs py-1 px-1.5 bg-gray-100 rounded"
onClick={() => {
setRefreshing(true);
mutate(IMPORTER_SERVICES_LIST(workspaceSlug as string)).then(() =>
setRefreshing(false)
);
}}
>
<ArrowPathIcon className={`h-3 w-3 ${refreshing ? "animate-spin" : ""}`} />{" "}
{refreshing ? "Refreshing..." : "Refresh status"}
</button>
</h3>
{importerServices ? (
importerServices.length > 0 ? (
<div className="space-y-2">
<div className="divide-y">
{importerServices.map((service) => (
<div key={service.id} className="py-3">
<h4 className="text-sm flex items-center gap-2">
<span>
Import from{" "}
<span className="font-medium">
{importersList[service.service]}
</span>{" "}
to{" "}
<span className="font-medium">
{service.project_detail.name}
</span>
</span>
<span
className={`capitalize px-2 py-0.5 text-xs rounded ${
service.status === "completed"
? "bg-green-100 text-green-500"
: service.status === "processing"
? "bg-yellow-100 text-yellow-500"
: service.status === "failed"
? "bg-red-100 text-red-500"
: ""
}`}
>
{refreshing ? "Refreshing..." : service.status}
</span>
</h4>
<div className="text-gray-500 text-xs mt-2 flex items-center gap-2">
<span>{renderShortDateWithYearFormat(service.created_at)}</span>|
<span>
Imported by{" "}
{service.initiated_by_detail.first_name &&
service.initiated_by_detail.first_name !== ""
? service.initiated_by_detail.first_name +
" " +
service.initiated_by_detail.last_name
: service.initiated_by_detail.email}
</span>
</div>
</div>
))}
</div>
</div>
) : (
<div className="py-2 text-sm text-gray-800">
No previous imports available.
</div>
)
) : (
<Loader className="grid grid-cols-1 gap-3 mt-6">
<Loader.Item height="40px" width="100%" />
<Loader.Item height="40px" width="100%" />
<Loader.Item height="40px" width="100%" />
<Loader.Item height="40px" width="100%" />
</Loader>
)}
</div>
))
) : (
<div className="py-5 text-center text-sm text-gray-800">
Integrations not available.
</div>
)}
</>
) : (
<div>
)
) : (
<Loader className="grid grid-cols-1 gap-3">
{["", ""].map((_integration, _idx) => (
<Loader.Item height="34px" width="100%" />
))}
<Loader.Item height="34px" width="100%" />
<Loader.Item height="34px" width="100%" />
</Loader>
</div>
)}
</div>
</>
)}
)}
</div>
</>
)}
{provider && (
<>
{provider === "github" && (
<GithubIntegrationRoot
workspaceSlug={workspaceSlug}
provider={provider}
allIntegrations={allIntegrations}
allIntegrationsError={allIntegrationsError}
allWorkspaceIntegrations={allWorkspaceIntegrations}
allWorkspaceIntegrationsError={allWorkspaceIntegrationsError}
allIntegrationImporters={allIntegrationImporters}
allIntegrationImportersError={allIntegrationImportersError}
/>
)}
</>
)}
</div>
);
{provider && provider === "github" && (
<GithubIntegrationRoot
provider={provider}
appIntegrations={appIntegrations}
workspaceIntegrations={workspaceIntegrations}
/>
)}
</div>
);
};
export default IntegrationGuide;

View file

@ -7,8 +7,10 @@ export * from "./github/auth";
// layout
export * from "./github/root";
// components
export * from "./github/configure";
export * from "./github/import-configure";
export * from "./github/import-data";
export * from "./github/issues-select";
export * from "./github/users-select";
export * from "./github/confirm";
export * from "./github/repo-details";
export * from "./github/import-users";
export * from "./github/import-confirm";
export * from "./github/select-repository";
export * from "./github/single-user-select";