in packages/online-editor/src/editor/Toolbar/GitIntegration/CreateGitRepositoryModal.tsx [61:374]
export function CreateGitRepositoryModal(props: {
workspace: WorkspaceDescriptor;
isOpen: boolean;
onClose: () => void;
onSuccess?: (args: { url: string }) => void;
}) {
const workspaces = useWorkspaces();
const { authSession, gitConfig, authInfo } = useAuthSession(props.workspace.gitAuthSessionId);
const authProvider = useAuthProvider(authSession);
const bitbucketClient = useBitbucketClient(authSession);
const gitHubClient = useGitHubClient(authSession);
const [isPrivate, setPrivate] = useState(false);
const [isLoading, setLoading] = useState(false);
const [error, setError] = useState<string | undefined>(undefined);
const [name, setName] = useState(getSuggestedRepositoryName(props.workspace.name));
const { i18n } = useOnlineI18n();
const [selectedOrganization, setSelectedOrganization] = useState<SelectOptionObjectType>();
const {
alerts: { createRepositorySuccessAlert, errorAlert },
} = useGitIntegration();
useEffect(() => {
setName(getSuggestedRepositoryName(props.workspace.name));
}, [props.workspace.name]);
const createBitbucketRepository = useCallback(async (): Promise<CreateRepositoryResponse> => {
if (selectedOrganization?.kind !== "organization") {
throw new Error("No workspace was selected for Bitbucket Repository.");
}
const repoResponse = await bitbucketClient.createRepo({ name, workspace: selectedOrganization.value, isPrivate });
if (!repoResponse.ok) {
throw new Error(
`Bitbucket repository creation request failed with: ${repoResponse.status} ${repoResponse.statusText}`
);
}
const repo = await repoResponse.json();
if (!repo.links || !repo.links.clone || !Array.isArray(repo.links.clone)) {
throw new Error("Unexpected contents of the Bitbucket repository creation response.");
}
const cloneLinks: any[] = repo.links.clone;
const cloneUrl = cloneLinks.filter((e) => {
return (e.name = "https" && e.href.startsWith("https"));
})[0].href;
return { cloneUrl, htmlUrl: repo.links.html.href };
}, [bitbucketClient, isPrivate, name, selectedOrganization]);
const createGitHubRepository = useCallback(async (): Promise<CreateRepositoryResponse> => {
const repo =
selectedOrganization?.kind === "organization"
? await gitHubClient.repos.createInOrg({
name,
private: isPrivate,
org: selectedOrganization.value,
})
: await gitHubClient.repos.createForAuthenticatedUser({ name, private: isPrivate });
if (!repo.data.clone_url) {
throw new Error("Repo creation failed.");
}
// The cloneUrl host is replaced with the authProvider domain because when GitHub is being proxied it will return
// the original URL (with github.com) instead of the proxy URL.
// This won't affect usaged when GitHub is not being proxied.
const host = new URL(repo.data.clone_url).host;
const cloneUrl = repo.data.clone_url.replace(host, authProvider?.domain ?? host);
return { cloneUrl, htmlUrl: repo.data.html_url };
}, [selectedOrganization, gitHubClient.repos, name, isPrivate, authProvider?.domain]);
const pushEmptyCommitIntoBitbucket = useCallback(async (): Promise<void> => {
if (selectedOrganization?.kind !== "organization") {
throw new Error("No workspace was selected for Bitbucket Repository.");
}
// need an empty commit push through REST API first
await bitbucketClient
.pushEmptyCommit({
repository: name,
workspace: selectedOrganization.value,
branch: props.workspace.origin.branch,
})
.then((response) => {
if (!response.ok) {
throw new Error(`Initial commit push failed: ${response.status} ${response.statusText}`);
}
});
}, [bitbucketClient, name, props.workspace.origin.branch, selectedOrganization]);
const create = useCallback(async () => {
try {
if (!authInfo || !gitConfig || !isSupportedGitAuthProviderType(authProvider?.type)) {
return;
}
const insecurelyDisableTlsCertificateValidation =
authProvider?.group === AuthProviderGroup.GIT && authProvider.insecurelyDisableTlsCertificateValidation;
setError(undefined);
setLoading(true);
const createRepositoryCommand: () => Promise<CreateRepositoryResponse> = switchExpression(authProvider?.type, {
bitbucket: createBitbucketRepository,
github: createGitHubRepository,
});
if (!createRepositoryCommand) {
throw new Error("Undefined create repository command for auth type " + authProvider?.type);
}
const { cloneUrl, htmlUrl: websiteUrl } = await createRepositoryCommand();
const initializeEmptyRepositoryCommand = switchExpression(authProvider?.type, {
bitbucket: pushEmptyCommitIntoBitbucket,
default: async (): Promise<void> => {},
});
await initializeEmptyRepositoryCommand();
await workspaces.addRemote({
workspaceId: props.workspace.workspaceId,
url: cloneUrl,
name: GIT_ORIGIN_REMOTE_NAME,
force: true,
});
await workspaces.createSavePoint({
workspaceId: props.workspace.workspaceId,
gitConfig,
});
await workspaces.push({
workspaceId: props.workspace.workspaceId,
remote: GIT_ORIGIN_REMOTE_NAME,
ref: props.workspace.origin.branch,
remoteRef: `refs/heads/${props.workspace.origin.branch}`,
force: switchExpression(authProvider?.type, {
github: false,
bitbucket: true,
}),
authInfo,
insecurelyDisableTlsCertificateValidation,
});
await workspaces.initGitOnWorkspace({
workspaceId: props.workspace.workspaceId,
remoteUrl: new URL(cloneUrl),
branch: props.workspace.origin.branch,
insecurelyDisableTlsCertificateValidation,
});
await workspaces.renameWorkspace({
workspaceId: props.workspace.workspaceId,
newName: new URL(websiteUrl).pathname.substring(1),
});
props.onClose();
createRepositorySuccessAlert.show({ url: websiteUrl });
props.onSuccess?.({ url: websiteUrl });
} catch (err) {
errorAlert.show();
setError(err);
throw err;
} finally {
setLoading(false);
}
}, [
authInfo,
gitConfig,
authProvider,
createBitbucketRepository,
createGitHubRepository,
pushEmptyCommitIntoBitbucket,
workspaces,
props,
createRepositorySuccessAlert,
errorAlert,
]);
const isNameValid = useMemo(() => {
return name.match(/^[._\-\w\d]+$/g);
}, [name]);
const validated = useMemo(() => {
if (isNameValid) {
return ValidatedOptions.success;
} else {
return ValidatedOptions.error;
}
}, [isNameValid]);
if (!authProvider?.type || !isSupportedGitAuthProviderType(authProvider?.type)) {
return <></>;
}
return (
<Modal
variant={ModalVariant.medium}
aria-label={i18n.createGitRepositoryModal[authProvider.type].createRepository}
isOpen={props.isOpen}
onClose={() => {
setError(undefined);
props.onClose();
}}
title={i18n.createGitRepositoryModal[authProvider.type].createRepository}
titleIconVariant={switchExpression(authProvider.type, {
bitbucket: BitbucketIcon,
github: GithubIcon,
})}
description={i18n.createGitRepositoryModal[authProvider.type].description(props.workspace.name)}
actions={[
<Button
isLoading={isLoading}
key="create"
variant="primary"
onClick={create}
isDisabled={switchExpression(authProvider.type, {
bitbucket: !isNameValid || selectedOrganization === undefined,
github: !isNameValid,
})}
>
{i18n.createGitRepositoryModal.form.buttonCreate}
</Button>,
]}
>
<br />
<Form
style={{ padding: "0 16px 0 16px" }}
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
return create();
}}
>
{error && (
<FormAlert>
<Alert
variant="danger"
title={i18n.createGitRepositoryModal[authProvider.type].error.formAlert(error)}
isInline={true}
/>
<br />
</FormAlert>
)}
<FormGroup label={i18n.createGitRepositoryModal[authProvider.type].form.select.label} fieldId="organization">
<LoadOrganizationsSelect workspace={props.workspace} onSelect={setSelectedOrganization} />
<FormHelperText>
<HelperText>
<HelperTextItem variant="default">
{i18n.createGitRepositoryModal[authProvider.type].form.select.description}
</HelperTextItem>
</HelperText>
</FormHelperText>
</FormGroup>
<FormGroup
label={i18n.createGitRepositoryModal.form.nameField.label}
isRequired={true}
fieldId="repository-name"
>
<TextInput
id={"repo-name"}
validated={validated}
isRequired={true}
placeholder={i18n.createGitRepositoryModal.form.nameField.label}
value={name}
onChange={(_event, val) => setName(val)}
/>
{validated === "error" ? (
<FormHelperText>
<HelperText>
<HelperTextItem variant="error">{i18n.createGitRepositoryModal.form.nameField.hint}</HelperTextItem>
</HelperText>
</FormHelperText>
) : (
<FormHelperText>
<HelperText>
<HelperTextItem variant="success"></HelperTextItem>
</HelperText>
</FormHelperText>
)}
</FormGroup>
<Divider inset={{ default: "inset3xl" }} />
<FormGroup fieldId="repo-visibility">
<Radio
isChecked={!isPrivate}
id={"repository-public"}
name={"repository-public"}
label={
<>
<UsersIcon />
{i18n.createGitRepositoryModal.form.visibility.public.label}
</>
}
description={i18n.createGitRepositoryModal.form.visibility.public.description}
onChange={() => setPrivate(false)}
/>
<br />
<Radio
isChecked={isPrivate}
id={"repository-private"}
name={"repository-private"}
label={
<>
<LockIcon />
{i18n.createGitRepositoryModal.form.visibility.private.label}
</>
}
description={i18n.createGitRepositoryModal.form.visibility.private.description}
onChange={() => setPrivate(true)}
/>
</FormGroup>
</Form>
</Modal>
);
}