export function CreateGitRepositoryModal()

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 />
                &nbsp;&nbsp; {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 />
                &nbsp;&nbsp; {i18n.createGitRepositoryModal.form.visibility.private.label}
              </>
            }
            description={i18n.createGitRepositoryModal.form.visibility.private.description}
            onChange={() => setPrivate(true)}
          />
        </FormGroup>
      </Form>
    </Modal>
  );
}