Skip to content

Import GitLab projects #5120

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
204 changes: 125 additions & 79 deletions components/dashboard/src/projects/NewProject.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ export default function NewProject() {
const location = useLocation();
const history = useHistory();
const { teams } = useContext(TeamsContext);
const { user } = useContext(UserContext);
const { user, setUser } = useContext(UserContext);

const [provider, setProvider] = useState<string>("github.com");
const [provider, setProvider] = useState<string | undefined>();
const [reposInAccounts, setReposInAccounts] = useState<ProviderRepository[]>([]);
const [repoSearchFilter, setRepoSearchFilter] = useState<string>("");
const [selectedAccount, setSelectedAccount] = useState<string | undefined>(undefined);
Expand All @@ -37,6 +37,16 @@ export default function NewProject() {
const [showNewTeam, setShowNewTeam] = useState<boolean>(false);
const [loaded, setLoaded] = useState<boolean>(false);

useEffect(() => {
if (user && provider === undefined) {
if (user.identities.find(i => i.authProviderId === "Public-GitLab")) {
setProvider("gitlab.com");
} else if (user.identities.find(i => i.authProviderId === "Public-GitHub")) {
setProvider("github.com");
}
}
}, [user]);

useEffect(() => {
const params = new URLSearchParams(location.search);
const teamParam = params.get("team");
Expand All @@ -45,16 +55,6 @@ export default function NewProject() {
const team = teams?.find(t => t.slug === teamParam);
setSelectedTeamOrUser(team);
}

(async () => {
updateOrgsState();
const repos = await updateReposInAccounts();
const first = repos[0];
if (first) {
setSelectedAccount(first.account);
}
setLoaded(true);
})();
}, []);

useEffect(() => {
Expand All @@ -77,9 +77,27 @@ export default function NewProject() {
setRepoSearchFilter("");
}, [selectedAccount]);

useEffect(() => {
if (!provider) {
return;
}
(async () => {
updateOrgsState();
const repos = await updateReposInAccounts();
const first = repos[0];
if (first) {
setSelectedAccount(first.account);
}
setLoaded(true);
})();
}, [provider]);

const isGitHub = () => provider === "github.com";

const updateReposInAccounts = async (installationId?: string) => {
if (!provider) {
return [];
}
try {
const repos = await getGitpodService().server.getProviderRepositoriesForUser({ provider, hints: { installationId } });
setReposInAccounts(repos);
Expand All @@ -96,7 +114,7 @@ export default function NewProject() {
}

const updateOrgsState = async () => {
if (isGitHub()) {
if (provider && isGitHub()) {
try {
const ghToken = await getToken(provider);
setNoOrgs(ghToken?.scopes.includes("read:org") !== true);
Expand Down Expand Up @@ -130,6 +148,9 @@ export default function NewProject() {
}

const createProject = async (teamOrUser: Team | User, selectedRepo: string) => {
if (!provider) {
return;
}
const repo = reposInAccounts.find(r => r.account === selectedAccount && r.name === selectedRepo);
if (!repo) {
console.error("No repo selected!")
Expand All @@ -156,7 +177,6 @@ export default function NewProject() {
return splitted.shift() && splitted.join("/");
}

const reposToRender = Array.from(reposInAccounts).filter(r => r.account === selectedAccount && r.name.includes(repoSearchFilter));
const accounts = new Map<string, { avatarUrl: string }>();
reposInAccounts.forEach(r => { if (!accounts.has(r.account)) accounts.set(r.account, { avatarUrl: r.accountAvatarUrl }) });

Expand Down Expand Up @@ -193,33 +213,40 @@ export default function NewProject() {

const renderSelectRepository = () => {

const noReposAvailable = reposInAccounts.length === 0;
const filteredRepos = Array.from(reposInAccounts).filter(r => r.account === selectedAccount && r.name.includes(repoSearchFilter));
const icon = selectedAccount && accounts.get(selectedAccount)?.avatarUrl;

const renderRepos = () => (<div className="mt-10 border rounded-t-xl border-gray-100 flex-col">
<div className="px-8 pt-8 flex flex-col space-y-2">
<ContextMenu classes="w-full left-0 cursor-pointer" menuEntries={getDropDownEntries(accounts)}>
<div className="w-full">
<img src={icon} className="rounded-full w-6 h-6 absolute top-1/4 left-4" />
<input className="w-full px-12 cursor-pointer font-semibold" readOnly type="text" value={selectedAccount || ""}></input>
<img src={CaretDown} title="Select Account" className="filter-grayscale absolute top-1/2 right-3" />
</div>
</ContextMenu>
<div className="w-full relative ">
<img src={search} title="Search" className="filter-grayscale absolute top-1/3 left-3" />
<input className="w-96 pl-10 border-0" type="text" placeholder="Search Repositories" value={repoSearchFilter}
onChange={(e) => setRepoSearchFilter(e.target.value)}></input>
const renderRepos = () => (<>
<div className={`mt-10 border rounded-xl border-gray-100 flex-col`}>
<div className="px-8 pt-8 flex flex-col space-y-2">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: For some reason, the pre-selected account is a specific but maybe random group. Does it make sense to default to pre-selecting your own account?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

defaulting to own account sounds good.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DEAL. Let's do this here or open a follow up issue to fine tune this later! Your call. 📞

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fyi: Opened follow up issue in #5331.

<ContextMenu classes="w-full left-0 cursor-pointer" menuEntries={getDropDownEntries(accounts)}>
<div className="w-full">
{icon && (
<img src={icon} className="rounded-full w-6 h-6 absolute top-1/4 left-4" />
)}
<input className="w-full px-12 cursor-pointer font-semibold" readOnly type="text" value={selectedAccount || ""}></input>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: Is it expected that group avatars fail to load for groups? It seems that shared projects with write access on personal accounts successfully fetch the avatars.

Missing avatars on groups (a) Missing avatars on personal accounts (b)
Screenshot 2021-08-17 at 5 44 31 PM Frame 34

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indeed :-(

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fyi: Opened follow up issue in #5330.

<img src={CaretDown} title="Select Account" className="filter-grayscale absolute top-1/2 right-3" />
</div>
</ContextMenu>
{filteredRepos.length > 0 && (
<div className="w-full relative ">
<img src={search} title="Search" className="filter-grayscale absolute top-1/3 left-3" />
<input className="w-96 pl-10 border-0" type="text" placeholder="Search Repositories" value={repoSearchFilter}
onChange={(e) => setRepoSearchFilter(e.target.value)}></input>
</div>
)}
</div>
</div>
<div className="p-6 flex-col">
{reposToRender.length > 0 && (
<div className="overscroll-contain max-h-80 overflow-y-auto pr-2">
{reposToRender.map(r => (
<div key={`repo-${r.name}`} className="flex p-3 rounded-xl hover:bg-gray-100 dark:hover:bg-gray-800 focus:bg-gitpod-kumquat-light transition ease-in-out group" >

<div className="flex-grow">
<div className="text-base text-gray-900 dark:text-gray-50 font-medium rounded-xl whitespace-nowrap">{toSimpleName(r.name)}</div>
<p>Updated {moment(r.updatedAt).fromNow()}</p>
</div>
<div className="p-6 flex-col">
{filteredRepos.length > 0 && (
<div className="overscroll-contain max-h-80 overflow-y-auto pr-2">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: What do you think of using a smaller max height like max-h-52 here? Suggesting this mostly because the scolling area is overflowing below the viewport height in some cases. Your call! We can also pick up such details in #4948 and #4951.

BEFORE AFTER
Screenshot 2021-08-19 at 5 31 45 PM Screenshot 2021-08-19 at 5 31 23 PM

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fyi: Opened follow up issue in #5334.

{filteredRepos.map((r, index) => (
<div key={`repo-${index}-${r.account}-${r.name}`} className="flex p-3 rounded-xl hover:bg-gray-100 dark:hover:bg-gray-800 focus:bg-gitpod-kumquat-light transition ease-in-out group" >

<div className="flex-grow">
<div className="text-base text-gray-900 dark:text-gray-50 font-medium rounded-xl whitespace-nowrap">{toSimpleName(r.name)}</div>
<p>Updated {moment(r.updatedAt).fromNow()}</p>
</div>
<div className="flex justify-end">
<div className="h-full my-auto flex self-center opacity-0 group-hover:opacity-100">
{!r.inUse ? (
Expand All @@ -229,54 +256,73 @@ export default function NewProject() {
)}
</div>
</div>
</div>
))}
</div>
)}
{!noReposAvailable && filteredRepos.length === 0 && (
<p className="text-center">No Results</p>
)}
{loaded && noReposAvailable && isGitHub() && (<div>
<div className="px-12 py-16 text-center text-gray-500 bg-gray-50 dark:bg-gray-800 rounded-xl">
<img src={NoAccess} title="No Access" className="m-auto mb-4" />
<h3 className="mb-2 text-gray-600 dark:text-gray-400">
No Access
</h3>
<span className="dark:text-gray-500">
Authorize GitHub (github.com) or select a different account.
</span>
<br />
<button className="mt-6" onClick={() => reconfigure()}>Authorize</button>
</div>
</div>)}
</div>

</div>
{isGitHub() && (
<div className="pt-3">
<div className="text-gray-500 text-center w-96 mx-8">
Repository not found? <a href="javascript:void(0)" onClick={e => reconfigure()} className="text-gray-400 underline underline-thickness-thin underline-offset-small hover:text-gray-600">Reconfigure</a>
</div>
{isGitHub() && noOrgs && (
<div className="text-gray-500 mx-auto text-center">
Missing organizations? <a href="javascript:void(0)" onClick={e => grantReadOrgPermissions()} className="text-gray-400 underline underline-thickness-thin underline-offset-small hover:text-gray-600">Grant permissions</a>
</div>
))}
)}
</div>
)}
{reposToRender.length === 0 && (
<p className="text-center ">not found</p>
)}
</div>

<div className="px-3 pt-3 bg-gray-100">
<div className="text-gray-500 text-center">
Repository not found? <a href="javascript:void(0)" onClick={e => reconfigure()} className="text-gray-400 underline underline-thickness-thin underline-offset-small hover:text-gray-600">Reconfigure</a>
</div>
{isGitHub() && noOrgs && (
<div className="text-gray-500 mx-auto text-center">
Missing organizations? <a href="javascript:void(0)" onClick={e => grantReadOrgPermissions()} className="text-gray-400 underline underline-thickness-thin underline-offset-small hover:text-gray-600">Grant permissions</a>
)}
</>
);

const renderEmptyState = () => (<div>
<div className="mt-8 border rounded-xl border-gray-100 dark:border-gray-700 flex-col">
<div>
<div className="px-12 py-16 text-center text-gray-500 bg-gray-50 dark:bg-gray-800 rounded-xl w-96 h-h96 flex items-center justify-center">
<h3 className="mb-2 text-gray-400 dark:text-gray-600 animate-pulse">
Loading ...
</h3>
</div>
)}
</div>
<div className="h-3 border rounded-b-xl border-gray-100 bg-gray-100"></div>
</div>);

const renderEmptyState = () => (<div className="mt-8 border rounded-xl border-gray-100 dark:border-gray-700 flex-col">
<div>
<div className="px-12 py-16 text-center text-gray-500 bg-gray-50 dark:bg-gray-800 rounded-xl">
<img src={NoAccess} title="No Access" className="m-auto mb-4" />
<h3 className="mb-2 text-gray-600 dark:text-gray-400">
No Access
</h3>
<span className="dark:text-gray-500">
Authorize GitHub (github.com) or select a different account.
</span>
<br/>
<button className="mt-6" onClick={() => reconfigure()}>Authorize Provider</button>
</div>
</div>
</div>)

const empty = reposInAccounts.length === 0;

const onGitProviderSeleted = (host: string) => {
const onGitProviderSeleted = async (host: string, updateUser?: boolean) => {
if (updateUser) {
setUser(await getGitpodService().server.getLoggedInUser());
}
setShowGitProviders(false);
setProvider(host);
}

return (<>
{(loaded && empty) ? renderEmptyState() : (showGitProviders ? (<GitProviders onHostSelected={onGitProviderSeleted} />) : renderRepos())}
</>)
if (!loaded) {
return renderEmptyState();
}

if (showGitProviders) {
return (<GitProviders onHostSelected={onGitProviderSeleted} />);
}

return renderRepos();
};

const renderSelectTeam = () => {
Expand Down Expand Up @@ -322,7 +368,7 @@ export default function NewProject() {

return (<div className="flex flex-col w-96 mt-24 mx-auto items-center">
<h1>New Project</h1>
<p className="text-gray-500 text-center text-base">Select a git repository.</p>
<p className="text-gray-500 text-center text-base">Select a git repository on <strong>{provider}</strong>.</p>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Worth linking to the provider itself? Feel free to ignore if it does not seem helpful.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fyi: Opened follow up issue in #5333


{!selectedRepo && renderSelectRepository()}

Expand All @@ -335,7 +381,7 @@ export default function NewProject() {
}

function GitProviders(props: {
onHostSelected: (host: string) => void
onHostSelected: (host: string, updateUser?: boolean) => void
}) {
const [authProviders, setAuthProviders] = useState<AuthProviderInfo[]>([]);

Expand All @@ -355,8 +401,8 @@ function GitProviders(props: {
await openAuthorizeWindow({
host: ap.host,
scopes: ap.requirements?.default,
onSuccess: () => {
props.onHostSelected(ap.host);
onSuccess: async () => {
props.onHostSelected(ap.host, true);
},
onError: (error) => {
console.log(error);
Expand Down
8 changes: 5 additions & 3 deletions components/dashboard/src/projects/Prebuild.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ import PrebuildLogs from "../components/PrebuildLogs";
import { shortCommitMessage } from "./render-utils";

export default function () {
const { teams } = useContext(TeamsContext);
const location = useLocation();

const { teams } = useContext(TeamsContext);
const team = getCurrentTeam(location, teams);

const match = useRouteMatch<{ team: string, project: string, prebuildId: string }>("/:team/:project/:prebuildId");
const projectName = match?.params?.project;
const prebuildId = match?.params?.prebuildId;
const team = getCurrentTeam(location, teams);

const [ prebuild, setPrebuild ] = useState<PrebuildInfo | undefined>();

Expand All @@ -44,7 +46,7 @@ export default function () {
});
setPrebuild(prebuilds[0]);
})();
}, [ teams, team ]);
}, [ teams ]);

const renderTitle = () => {
if (!prebuild) {
Expand Down
8 changes: 5 additions & 3 deletions components/dashboard/src/projects/Prebuilds.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ import { shortCommitMessage } from "./render-utils";

export default function () {
const history = useHistory();
const { teams } = useContext(TeamsContext);
const location = useLocation();

const { teams } = useContext(TeamsContext);
const team = getCurrentTeam(location, teams);

const match = useRouteMatch<{ team: string, resource: string }>("/:team/:resource");
const projectName = match?.params?.resource;
const team = getCurrentTeam(location, teams);

// @ts-ignore
const [project, setProject] = useState<Project | undefined>();
Expand Down Expand Up @@ -55,7 +57,7 @@ export default function () {
}
}
})();
}, [ teams, team ]);
}, [ teams ]);

const prebuildContextMenu = (p: PrebuildInfo) => {
const running = p.status === "building";
Expand Down
10 changes: 6 additions & 4 deletions components/dashboard/src/projects/Project.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ import { shortCommitMessage } from "./render-utils";

export default function () {
const history = useHistory();
const { teams } = useContext(TeamsContext);
const location = useLocation();

const { teams } = useContext(TeamsContext);
const team = getCurrentTeam(location, teams);

const match = useRouteMatch<{ team: string, resource: string }>("/:team/:resource");
const projectName = match?.params?.resource;
const team = getCurrentTeam(location, teams);

const [project, setProject] = useState<Project | undefined>();

Expand All @@ -34,7 +36,7 @@ export default function () {

useEffect(() => {
updateProject();
}, [ teams, team ]);
}, [ teams ]);

const updateProject = async () => {
if (!teams || !projectName) {
Expand Down Expand Up @@ -87,7 +89,7 @@ export default function () {
// TODO(at): this need to be revised once prebuild events are integrated
return;
}
if (!team || !project) {
if (!project) {
return;
}
prebuildLoaders.add(branch.name);
Expand Down
2 changes: 1 addition & 1 deletion components/dashboard/src/projects/Projects.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export default function () {

useEffect(() => {
updateProjects();
}, [ teams, team ]);
}, [ teams ]);

const updateProjects = async () => {
if (!teams) {
Expand Down
Loading