|
|
@@ -1,4 +1,4 @@
|
|
|
-import React, { useContext, useEffect, useState } from "react";
|
|
|
+import React, { useContext, useEffect, useRef, useState } from "react";
|
|
|
import styled from "styled-components";
|
|
|
import github from "assets/github.png";
|
|
|
|
|
|
@@ -8,12 +8,8 @@ import { Context } from "shared/Context";
|
|
|
|
|
|
import Loading from "../Loading";
|
|
|
import SearchBar from "../SearchBar";
|
|
|
-
|
|
|
-interface GithubAppAccessData {
|
|
|
- has_access: boolean;
|
|
|
- username?: string;
|
|
|
- accounts?: string[];
|
|
|
-}
|
|
|
+import DynamicLink from "components/DynamicLink";
|
|
|
+import { useOutsideAlerter } from "shared/hooks/useOutsideAlerter";
|
|
|
|
|
|
type Props = {
|
|
|
actionConfig: ActionConfigType | null;
|
|
|
@@ -30,87 +26,103 @@ const RepoList: React.FC<Props> = ({
|
|
|
readOnly,
|
|
|
filteredRepos,
|
|
|
}) => {
|
|
|
+ const [providers, setProviders] = useState([]);
|
|
|
+ const [currentProvider, setCurrentProvider] = useState(null);
|
|
|
const [repos, setRepos] = useState<RepoType[]>([]);
|
|
|
const [repoLoading, setRepoLoading] = useState(true);
|
|
|
const [selectedRepo, setSelectedRepo] = useState(null);
|
|
|
const [repoError, setRepoError] = useState(false);
|
|
|
- const [accessLoading, setAccessLoading] = useState(true);
|
|
|
- const [accessError, setAccessError] = useState(false);
|
|
|
- const [accessData, setAccessData] = useState<GithubAppAccessData>({
|
|
|
- has_access: false,
|
|
|
- });
|
|
|
const [searchFilter, setSearchFilter] = useState(null);
|
|
|
- const { currentProject } = useContext(Context);
|
|
|
+ const [hasProviders, setHasProviders] = useState(true);
|
|
|
+ const { currentProject, setCurrentError } = useContext(Context);
|
|
|
|
|
|
- const loadData = async () => {
|
|
|
- try {
|
|
|
- const { data } = await api.getGithubAccounts("<token>", {}, {});
|
|
|
+ useEffect(() => {
|
|
|
+ let isSubscribed = true;
|
|
|
+ api
|
|
|
+ .getGitProviders("<token>", {}, { project_id: currentProject.id })
|
|
|
+ .then((res) => {
|
|
|
+ const data = res.data;
|
|
|
+ if (!isSubscribed) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!Array.isArray(data)) {
|
|
|
+ setHasProviders(false);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ setProviders(data);
|
|
|
+ setCurrentProvider(data[0]);
|
|
|
+ })
|
|
|
+ .catch((err) => {
|
|
|
+ setHasProviders(false);
|
|
|
+ setCurrentError(err);
|
|
|
+ });
|
|
|
|
|
|
- setAccessData(data);
|
|
|
- setAccessLoading(false);
|
|
|
- } catch (error) {
|
|
|
- setAccessError(true);
|
|
|
- setAccessLoading(false);
|
|
|
- }
|
|
|
+ return () => {
|
|
|
+ isSubscribed = false;
|
|
|
+ };
|
|
|
+ }, []);
|
|
|
|
|
|
- let ids: number[] = [];
|
|
|
+ const loadGithubRepos = async (repoId: number) => {
|
|
|
+ try {
|
|
|
+ const res = await api.getGitRepoList<
|
|
|
+ { FullName: string; Kind: "github" }[]
|
|
|
+ >("<token>", {}, { project_id: currentProject.id, git_repo_id: repoId });
|
|
|
|
|
|
- if (!userId && userId !== 0) {
|
|
|
- ids = await api
|
|
|
- .getGitRepos("token", {}, { project_id: currentProject.id })
|
|
|
- .then((res) => res.data);
|
|
|
- } else {
|
|
|
- setRepoLoading(false);
|
|
|
- setRepoError(true);
|
|
|
- return;
|
|
|
- }
|
|
|
+ const repos = res.data.map((repo) => ({ ...repo, GHRepoID: repoId }));
|
|
|
+ return repos;
|
|
|
+ } catch (error) {}
|
|
|
+ };
|
|
|
|
|
|
- const repoListPromises = ids.map((id) =>
|
|
|
- api.getGitRepoList(
|
|
|
+ const loadGitlabRepos = async (integrationId: number) => {
|
|
|
+ try {
|
|
|
+ const res = await api.getGitlabRepos<string[]>(
|
|
|
"<token>",
|
|
|
{},
|
|
|
- { project_id: currentProject.id, git_repo_id: id }
|
|
|
- )
|
|
|
- );
|
|
|
-
|
|
|
- try {
|
|
|
- const resolvedRepoList = await Promise.allSettled(repoListPromises);
|
|
|
-
|
|
|
- const repos: RepoType[][] = resolvedRepoList.map((repo) =>
|
|
|
- repo.status === "fulfilled" ? repo.value.data : []
|
|
|
+ { project_id: currentProject.id, integration_id: integrationId }
|
|
|
);
|
|
|
+ const repos: RepoType[] = res.data.map((repo) => ({
|
|
|
+ FullName: repo,
|
|
|
+ Kind: "gitlab",
|
|
|
+ GitIntegrationId: integrationId,
|
|
|
+ }));
|
|
|
+ return repos;
|
|
|
+ } catch (error) {}
|
|
|
+ };
|
|
|
|
|
|
- const names = new Set();
|
|
|
- // note: would be better to use .flat() here but you need es2019 for
|
|
|
- setRepos(
|
|
|
- repos
|
|
|
- .map((arr, idx) =>
|
|
|
- arr.map((el) => {
|
|
|
- el.GHRepoID = ids[idx];
|
|
|
- return el;
|
|
|
- })
|
|
|
- )
|
|
|
- .reduce((acc, val) => acc.concat(val), [])
|
|
|
- .reduce((acc, val) => {
|
|
|
- if (!names.has(val.FullName)) {
|
|
|
- names.add(val.FullName);
|
|
|
- return acc.concat(val);
|
|
|
- } else {
|
|
|
- return acc;
|
|
|
- }
|
|
|
- }, [])
|
|
|
- );
|
|
|
- setRepoLoading(false);
|
|
|
- } catch (err) {
|
|
|
- setRepoLoading(false);
|
|
|
- setRepoError(true);
|
|
|
+ const loadRepos = (provider: any) => {
|
|
|
+ if (provider.provider === "github") {
|
|
|
+ return loadGithubRepos(provider.installation_id);
|
|
|
+ } else {
|
|
|
+ return loadGitlabRepos(provider.integration_id);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
- // TODO: Try to unhook before unmount
|
|
|
useEffect(() => {
|
|
|
- loadData();
|
|
|
- }, []);
|
|
|
+ let isSubscribed = true;
|
|
|
+ if (!currentProvider) {
|
|
|
+ return () => {
|
|
|
+ isSubscribed = false;
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ setRepoLoading(true);
|
|
|
+
|
|
|
+ loadRepos(currentProvider)
|
|
|
+ .then((repos) => {
|
|
|
+ if (isSubscribed) {
|
|
|
+ setRepos(repos);
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .catch((err) => {
|
|
|
+ setRepos([]);
|
|
|
+ console.log(err);
|
|
|
+ })
|
|
|
+ .finally(() => {
|
|
|
+ setRepoLoading(false);
|
|
|
+ });
|
|
|
+ }, [currentProvider]);
|
|
|
|
|
|
// clear out actionConfig and SelectedRepository if new search is performed
|
|
|
useEffect(() => {
|
|
|
@@ -119,20 +131,38 @@ const RepoList: React.FC<Props> = ({
|
|
|
image_repo_uri: null,
|
|
|
git_branch: null,
|
|
|
git_repo_id: 0,
|
|
|
+ kind: "github",
|
|
|
});
|
|
|
setSelectedRepo(null);
|
|
|
}, [searchFilter]);
|
|
|
|
|
|
const setRepo = (x: RepoType) => {
|
|
|
- let updatedConfig = actionConfig;
|
|
|
- updatedConfig.git_repo = x.FullName;
|
|
|
- updatedConfig.git_repo_id = x.GHRepoID;
|
|
|
+ let repoConfig: any;
|
|
|
+ if (x.Kind === "gitlab") {
|
|
|
+ repoConfig = {
|
|
|
+ kind: "gitlab",
|
|
|
+ git_repo: x.FullName,
|
|
|
+ gitlab_integration_id: x.GitIntegrationId,
|
|
|
+ };
|
|
|
+ } else {
|
|
|
+ repoConfig = {
|
|
|
+ kind: "github",
|
|
|
+ git_repo: x.FullName,
|
|
|
+ git_repo_id: x.GHRepoID,
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ const updatedConfig = {
|
|
|
+ ...actionConfig,
|
|
|
+ ...repoConfig,
|
|
|
+ };
|
|
|
+
|
|
|
setActionConfig(updatedConfig);
|
|
|
setSelectedRepo(x.FullName);
|
|
|
};
|
|
|
|
|
|
const renderRepoList = () => {
|
|
|
- if (repoLoading || accessLoading) {
|
|
|
+ if (repoLoading) {
|
|
|
return (
|
|
|
<LoadingWrapper>
|
|
|
<Loading />
|
|
|
@@ -140,26 +170,29 @@ const RepoList: React.FC<Props> = ({
|
|
|
);
|
|
|
} else if (repoError) {
|
|
|
return <LoadingWrapper>Error loading repos.</LoadingWrapper>;
|
|
|
- } else if (repos.length == 0) {
|
|
|
- if (accessError) {
|
|
|
+ } else if (!Array.isArray(repos) || repos.length === 0) {
|
|
|
+ if (currentProvider.provider === "gitlab") {
|
|
|
return (
|
|
|
<LoadingWrapper>
|
|
|
- No connected Github repos found.
|
|
|
- <A href={"/api/integrations/github-app/oauth"}>
|
|
|
- Authorize Porter to view your repositories.
|
|
|
+ GitLab could not be reached.
|
|
|
+ <A
|
|
|
+ to={`${window.location.origin}/api/projects/${currentProject.id}/oauth/gitlab?integration_id=${currentProvider.integration_id}`}
|
|
|
+ >
|
|
|
+ Connect your GitLab account to Porter
|
|
|
</A>
|
|
|
+ or select another Git provider.
|
|
|
</LoadingWrapper>
|
|
|
);
|
|
|
- }
|
|
|
-
|
|
|
- if (accessData.accounts?.length === 0) {
|
|
|
+ } else {
|
|
|
return (
|
|
|
<LoadingWrapper>
|
|
|
No connected Github repos found. You can
|
|
|
- <A href={"/api/integrations/github-app/install"}>
|
|
|
+ <A
|
|
|
+ to={`${window.location.origin}/api/integrations/github-app/install`}
|
|
|
+ >
|
|
|
Install Porter in more repositories
|
|
|
</A>
|
|
|
- .
|
|
|
+ or select another git provider.
|
|
|
</LoadingWrapper>
|
|
|
);
|
|
|
}
|
|
|
@@ -191,7 +224,11 @@ const RepoList: React.FC<Props> = ({
|
|
|
readOnly={readOnly}
|
|
|
disabled={shouldDisable}
|
|
|
>
|
|
|
- <img src={github} alt={"github icon"} />
|
|
|
+ {repo.Kind === "github" ? (
|
|
|
+ <img src={github} alt={"github icon"} />
|
|
|
+ ) : (
|
|
|
+ <i className="devicon-gitlab-plain colored" />
|
|
|
+ )}
|
|
|
{repo.FullName}
|
|
|
{shouldDisable && ` - This repo was already added`}
|
|
|
</RepoName>
|
|
|
@@ -206,11 +243,19 @@ const RepoList: React.FC<Props> = ({
|
|
|
} else {
|
|
|
return (
|
|
|
<>
|
|
|
- <SearchBar
|
|
|
- setSearchFilter={setSearchFilter}
|
|
|
- disabled={repoError || repoLoading || accessError || accessLoading}
|
|
|
- prompt={"Search repos..."}
|
|
|
- />
|
|
|
+ <div style={{ display: "flex", marginBottom: "10px" }}>
|
|
|
+ <ProviderSelector
|
|
|
+ values={providers}
|
|
|
+ currentValue={currentProvider}
|
|
|
+ onChange={setCurrentProvider}
|
|
|
+ />
|
|
|
+ <SearchBar
|
|
|
+ setSearchFilter={setSearchFilter}
|
|
|
+ disabled={repoError || repoLoading}
|
|
|
+ prompt={"Search repos . . ."}
|
|
|
+ fullWidth
|
|
|
+ />
|
|
|
+ </div>
|
|
|
<RepoListWrapper>
|
|
|
<ExpandedWrapper>{renderRepoList()}</ExpandedWrapper>
|
|
|
</RepoListWrapper>
|
|
|
@@ -219,11 +264,175 @@ const RepoList: React.FC<Props> = ({
|
|
|
}
|
|
|
};
|
|
|
|
|
|
+ if (!hasProviders) {
|
|
|
+ return (
|
|
|
+ <>
|
|
|
+ <RepoListWrapper>
|
|
|
+ <ExpandedWrapper>
|
|
|
+ <LoadingWrapper>
|
|
|
+ <div
|
|
|
+ style={{
|
|
|
+ display: "flex",
|
|
|
+ flexDirection: "column",
|
|
|
+ alignItems: "center",
|
|
|
+ justifyContent: "center",
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <div>A connected Git provider wasn't found.</div>
|
|
|
+ <div>
|
|
|
+ You can
|
|
|
+ <A
|
|
|
+ to={`${window.location.origin}/api/integrations/github-app/install`}
|
|
|
+ >
|
|
|
+ connect a GitHub repo
|
|
|
+ </A>
|
|
|
+ or
|
|
|
+ <A to={"/integrations"}>add a GitLab instance</A>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </LoadingWrapper>
|
|
|
+ </ExpandedWrapper>
|
|
|
+ </RepoListWrapper>
|
|
|
+ </>
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
return <>{renderExpanded()}</>;
|
|
|
};
|
|
|
|
|
|
export default RepoList;
|
|
|
|
|
|
+const ProviderSelector = (props: {
|
|
|
+ values: any[];
|
|
|
+ currentValue: any;
|
|
|
+ onChange: (provider: any) => void;
|
|
|
+}) => {
|
|
|
+ const wrapperRef = useRef();
|
|
|
+ const { values, currentValue, onChange } = props;
|
|
|
+ const [isOpen, setIsOpen] = useState(false);
|
|
|
+ const icon = `devicon-${currentValue?.provider}-plain colored`;
|
|
|
+ useOutsideAlerter(wrapperRef, () => {
|
|
|
+ setIsOpen(false);
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!currentValue) {
|
|
|
+ return (
|
|
|
+ <ProviderSelectorStyles.Wrapper>
|
|
|
+ <Loading />
|
|
|
+ </ProviderSelectorStyles.Wrapper>
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <>
|
|
|
+ <ProviderSelectorStyles.Wrapper ref={wrapperRef} isOpen={isOpen}>
|
|
|
+ <ProviderSelectorStyles.Icon className={icon} />
|
|
|
+
|
|
|
+ <ProviderSelectorStyles.Button
|
|
|
+ onClick={() => setIsOpen((prev) => !prev)}
|
|
|
+ >
|
|
|
+ {currentValue?.name || currentValue?.instance_url}
|
|
|
+ </ProviderSelectorStyles.Button>
|
|
|
+ <i className="material-icons">arrow_drop_down</i>
|
|
|
+ {isOpen ? (
|
|
|
+ <>
|
|
|
+ <ProviderSelectorStyles.OptionWrapper>
|
|
|
+ {values.map((provider) => {
|
|
|
+ return (
|
|
|
+ <ProviderSelectorStyles.Option
|
|
|
+ onClick={() => {
|
|
|
+ setIsOpen(false);
|
|
|
+ onChange(provider);
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <ProviderSelectorStyles.Icon
|
|
|
+ className={`devicon-${provider?.provider}-plain colored`}
|
|
|
+ />
|
|
|
+ <ProviderSelectorStyles.Text>
|
|
|
+ {provider?.name || provider?.instance_url}
|
|
|
+ </ProviderSelectorStyles.Text>
|
|
|
+ </ProviderSelectorStyles.Option>
|
|
|
+ );
|
|
|
+ })}
|
|
|
+ </ProviderSelectorStyles.OptionWrapper>
|
|
|
+ </>
|
|
|
+ ) : null}
|
|
|
+ </ProviderSelectorStyles.Wrapper>
|
|
|
+ </>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+const ProviderSelectorStyles = {
|
|
|
+ Wrapper: styled.div<{ isOpen?: boolean }>`
|
|
|
+ position: relative;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ height: 40px;
|
|
|
+ display: flex;
|
|
|
+ min-width: 50%;
|
|
|
+ cursor: pointer;
|
|
|
+ margin-right: 10px;
|
|
|
+ margin-left: 2px;
|
|
|
+ align-items: center;
|
|
|
+
|
|
|
+ > i {
|
|
|
+ margin-left: -26px;
|
|
|
+ margin-right: 10px;
|
|
|
+ z-index: 0;
|
|
|
+ transform: ${(props) => (props.isOpen ? "rotate(180deg)" : "")};
|
|
|
+ }
|
|
|
+ `,
|
|
|
+ Button: styled.div`
|
|
|
+ height: 100%;
|
|
|
+ font-weight: bold;
|
|
|
+ font-size: 14px;
|
|
|
+ border-bottom: 0;
|
|
|
+ z-index: 999;
|
|
|
+ white-space: nowrap;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ padding: 6px 15px;
|
|
|
+ padding-left: 40px;
|
|
|
+ padding-right: 28px;
|
|
|
+ border-bottom: 2px solid #ffffff;
|
|
|
+ padding-top: 11px;
|
|
|
+ `,
|
|
|
+ OptionWrapper: styled.div`
|
|
|
+ top: 40px;
|
|
|
+ position: absolute;
|
|
|
+ background: #37393f;
|
|
|
+ border-radius: 3px;
|
|
|
+ width: calc(100% - 4px);
|
|
|
+ box-shadow: 0 8px 20px 0px #00000088;
|
|
|
+ `,
|
|
|
+ Option: styled.div`
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+
|
|
|
+ :hover {
|
|
|
+ background-color: #ffffff22;
|
|
|
+ }
|
|
|
+ `,
|
|
|
+ Icon: styled.span`
|
|
|
+ font-size: 24px;
|
|
|
+ margin-left: 9px;
|
|
|
+ margin-right: -29px;
|
|
|
+ color: white;
|
|
|
+ `,
|
|
|
+ Text: styled.div`
|
|
|
+ font-weight: bold;
|
|
|
+ font-size: 14px;
|
|
|
+ margin-left: 40px;
|
|
|
+ height: 45px;
|
|
|
+ white-space: nowrap;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ padding: 8px 10px;
|
|
|
+ width: 100%;
|
|
|
+ padding-top: 14px;
|
|
|
+ padding-left: 0;
|
|
|
+ `,
|
|
|
+};
|
|
|
+
|
|
|
const RepoListWrapper = styled.div`
|
|
|
border: 1px solid #ffffff55;
|
|
|
border-radius: 3px;
|
|
|
@@ -314,9 +523,11 @@ const ExpandedWrapperAlt = styled(ExpandedWrapper)`
|
|
|
overflow-y: auto;
|
|
|
`;
|
|
|
|
|
|
-const A = styled.a`
|
|
|
+const A = styled(DynamicLink)`
|
|
|
color: #8590ff;
|
|
|
text-decoration: underline;
|
|
|
margin-left: 5px;
|
|
|
+ margin-right: 5px;
|
|
|
+
|
|
|
cursor: pointer;
|
|
|
`;
|