Skip to content
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

feat(gitlab): connect with GitLab #991

Merged
merged 12 commits into from
Sep 2, 2023
5 changes: 4 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,7 @@ TEST_GITHUB_USER_ACCESS_TOKEN=xx
VERCEL_CLIENT_SECRET=
VERCEL_CLIENT_ID=
# Stripe webhook secret
STRIPE_WEBHOOK_SECRET=
STRIPE_WEBHOOK_SECRET=
# GitLab dev app credentials
GITLAB_APP_ID=
GITLAB_APP_SECRET=
14 changes: 9 additions & 5 deletions apps/app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
import { Layout } from "@/containers/Layout";

import { ApolloInitializer } from "./containers/Apollo";
import { AuthProvider } from "./containers/Auth";
import { AuthContextProvider, AuthProvider } from "./containers/Auth";
import { ColorModeProvider } from "./containers/ColorMode";
import { Account } from "./pages/Account";
import { AccountNewProject } from "./pages/Account/NewProject";
Expand All @@ -33,8 +33,12 @@ import { TooltipProvider } from "./ui/Tooltip";

const router = createBrowserRouter([
{
path: "/auth/github/callback",
element: <AuthCallback />,
path: `/auth/${AuthProvider.GitHub}/callback`,
element: <AuthCallback provider={AuthProvider.GitHub} />,
},
{
path: `/auth/${AuthProvider.GitLab}/callback`,
element: <AuthCallback provider={AuthProvider.GitLab} />,
},
{
path: "/vercel/callback",
Expand Down Expand Up @@ -135,13 +139,13 @@ export const App = () => {
<>
<Helmet defaultTitle="Argos" />
<ColorModeProvider>
<AuthProvider>
<AuthContextProvider>
<ApolloInitializer>
<TooltipProvider>
<RouterProvider router={router} />
</TooltipProvider>
</ApolloInitializer>
</AuthProvider>
</AuthContextProvider>
</ColorModeProvider>
</>
);
Expand Down
88 changes: 88 additions & 0 deletions apps/app/src/containers/Account/GitLab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { useApolloClient } from "@apollo/client";
import { FormProvider, SubmitHandler, useForm } from "react-hook-form";

import { FragmentType, graphql, useFragment } from "@/gql";
import { Card, CardBody, CardParagraph, CardTitle } from "@/ui/Card";
import { Form } from "@/ui/Form";
import { FormCardFooter } from "@/ui/FormCardFooter";
import { FormTextInput } from "@/ui/FormTextInput";
import { Permission } from "@/gql/graphql";

const AccountFragment = graphql(`
fragment AccountGitLab_Account on Account {
id
permissions
gitlabAccessToken
}
`);

const UpdateAccountMutation = graphql(`
mutation AccountGitLab_updateAccount($id: ID!, $gitlabAccessToken: String) {
updateAccount(input: { id: $id, gitlabAccessToken: $gitlabAccessToken }) {
id
gitlabAccessToken
}
}
`);

type Inputs = {
gitlabAccessToken: string;
};

export type AccountGitLabProps = {
account: FragmentType<typeof AccountFragment>;
};

export const AccountGitLab = (props: AccountGitLabProps) => {
const account = useFragment(AccountFragment, props.account);
const client = useApolloClient();
const form = useForm<Inputs>({
defaultValues: {
gitlabAccessToken: account.gitlabAccessToken ?? "",
},
});
const onSubmit: SubmitHandler<Inputs> = async (data) => {
await client.mutate({
mutation: UpdateAccountMutation,
variables: {
id: account.id,
gitlabAccessToken: data.gitlabAccessToken || null,
},
});
};
const writable = account.permissions.includes(Permission.Write);
return (
<Card>
<FormProvider {...form}>
<Form onSubmit={onSubmit}>
<CardBody>
<CardTitle id="gitlab">GitLab</CardTitle>
<CardParagraph>
Setup GitLab to get Argos updates in your merge requests.
</CardParagraph>
<FormTextInput
{...form.register("gitlabAccessToken")}
label="Personal access token"
disabled={!writable}
/>
<div className="text-sm text-low mt-2">
The access token is used to update commit status in GitLab. These
updates are made on behalf of the user who created the access
token. It must have access to the repository you want to integrate
with.
</div>
{!writable && (
<div className="mt-4">
If you want to setup GitLab integration, please ask your team
owner to setup it.
</div>
)}
</CardBody>
<FormCardFooter>
<div>Learn more about setting up GitLab + Argos integration.</div>
</FormCardFooter>
</Form>
</FormProvider>
</Card>
);
};
31 changes: 7 additions & 24 deletions apps/app/src/containers/AccountAvatar.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { clsx } from "clsx";
import { forwardRef } from "react";

import { FragmentType, graphql, useFragment } from "@/gql";

import { ImageAvatar } from "./ImageAvatar";
import { InitialAvatar } from "./InitialAvatar";

const AvatarFragment = graphql(`
fragment AccountAvatarFragment on AccountAvatar {
Expand All @@ -25,30 +25,13 @@ export const AccountAvatar = forwardRef<any, AccountAvatarProps>(
const size = props.size ?? 32;
if (!avatar.url) {
return (
<div
<InitialAvatar
ref={ref}
className={clsx(
props.className,
"flex select-none items-center justify-center rounded-full",
)}
style={{
backgroundColor: avatar.color,
width: size,
height: size,
}}
>
<svg width="100%" height="100%" viewBox="-50 -66 100 100">
<text
fill="white"
fontWeight="600"
textAnchor="middle"
fontSize="50"
fontFamily="Inter, sans-serif"
>
{avatar.initial}
</text>
</svg>
</div>
initial={avatar.initial}
color={avatar.color}
size={size}
className={props.className}
/>
);
}
return (
Expand Down
11 changes: 10 additions & 1 deletion apps/app/src/containers/Auth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ import {
useState,
} from "react";

export enum AuthProvider {
GitHub = "github",
GitLab = "gitlab",
}

type Token = null | string;

interface AuthContextValue {
Expand All @@ -21,7 +26,11 @@ const COOKIE_NAME = "argos_jwt";
const COOKIE_DOMAIN =
process.env["NODE_ENV"] === "production" ? ".argos-ci.com" : "";

export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
export const AuthContextProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
const [token, setStateToken] = useState<string | null>(() => {
return Cookie.get(COOKIE_NAME) ?? null;
});
Expand Down
2 changes: 1 addition & 1 deletion apps/app/src/containers/GitHub.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const GitHubLoginButton = memo<GitHubLoginButtonProps>(
({ children, redirect, ...props }) => {
const loginUrl = useLoginUrl(redirect);
return (
<Button color="neutral" {...props}>
<Button color="github" {...props}>
{(buttonProps) => (
<a href={loginUrl} {...buttonProps}>
<ButtonIcon>
Expand Down
65 changes: 65 additions & 0 deletions apps/app/src/containers/GitLab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import * as React from "react";
import { useLocation } from "react-router-dom";

import config from "@/config";
import { Button, ButtonIcon, ButtonProps } from "@/ui/Button";

const useLoginUrl = (redirect: string | null | undefined) => {
const { origin } = window.location;
const { pathname } = useLocation();
const callbackUrl = `${origin}/auth/gitlab/callback?r=${encodeURIComponent(
redirect ?? pathname,
)}`;
return `${config.get("gitlab.loginUrl")}&redirect_uri=${encodeURIComponent(
callbackUrl,
)}`;
};

export type GitLabLoginButtonProps = Omit<ButtonProps, "children"> & {
children?: React.ReactNode;
redirect?: string | null;
};

export const GitLabLogo = (props: React.SVGProps<SVGSVGElement>) => {
return (
<svg width="1em" height="1em" viewBox="0 0 24 22" {...props}>
<path
fill="currentColor"
d="M1.279 8.29.044 12.294c-.117.367 0 .78.325 1.014l11.323 8.23-.009-.012-.03-.039L1.279 8.29zm21.713 5.018a.905.905 0 0 0 .325-1.014L22.085 8.29 11.693 21.52l11.299-8.212z"
/>
<path
fill="currentColor"
d="m1.279 8.29 10.374 13.197.03.039.01-.006L22.085 8.29H1.28z"
opacity={0.4}
/>
<path
fill="currentColor"
d="m15.982 8.29-4.299 13.236-.004.011.014-.017L22.085 8.29h-6.103zm-8.606 0H1.279l10.374 13.197L7.376 8.29z"
opacity={0.6}
/>
<path
fill="currentColor"
d="m18.582.308-2.6 7.982h6.103L19.48.308c-.133-.41-.764-.41-.897 0zM1.279 8.29 3.88.308c.133-.41.764-.41.897 0l2.6 7.982H1.279z"
opacity={0.4}
/>
</svg>
);
};

export const GitLabLoginButton = React.memo<GitLabLoginButtonProps>(
({ children, redirect, ...props }) => {
const loginUrl = useLoginUrl(redirect);
return (
<Button color="gitlab" {...props}>
{(buttonProps) => (
<a href={loginUrl} {...buttonProps}>
<ButtonIcon>
<GitLabLogo />
</ButtonIcon>
{children ?? "Login with GitLab"}
</a>
)}
</Button>
);
},
);
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@ import { MarkGithubIcon } from "@primer/octicons-react";

import config from "@/config";
import { FragmentType, graphql, useFragment } from "@/gql";
import { Anchor } from "@/ui/Link";
import {
Select,
SelectArrow,
SelectItem,
SelectPopover,
SelectText,
SelectSeparator,
useSelectState,
} from "@/ui/Select";
import { ListBulletIcon, PlusIcon } from "@heroicons/react/24/outline";

const InstallationFragment = graphql(`
fragment InstallationsSelect_GhApiInstallation on GhApiInstallation {
fragment GithubInstallationsSelect_GhApiInstallation on GhApiInstallation {
id
account {
id
Expand All @@ -23,11 +23,12 @@ const InstallationFragment = graphql(`
}
`);

export const InstallationsSelect = (props: {
export const GithubInstallationsSelect = (props: {
installations: FragmentType<typeof InstallationFragment>[];
value: string;
setValue: (value: string) => void;
disabled?: boolean;
onSwitch: () => void;
}) => {
const installations = useFragment(InstallationFragment, props.installations);
const select = useSelectState({
Expand Down Expand Up @@ -72,19 +73,42 @@ export const InstallationsSelect = (props: {
</SelectItem>
);
})}
<SelectText>
Don't see your org?{" "}
<Anchor
href={`${config.get(
"github.appUrl",
)}/installations/new?state=${encodeURIComponent(
window.location.pathname,
)}`}
external
>
<SelectSeparator />
<SelectItem
state={select}
button
onClick={(event) => {
event.preventDefault();
window.open(
`${config.get(
"github.appUrl",
)}/installations/new?state=${encodeURIComponent(
window.location.pathname,
)}`,
"_blank",
"noopener noreferrer",
);
}}
className="cursor-pointer"
>
<div className="flex items-center gap-2">
<PlusIcon className="w-[1em] h-[1em]" />
Add GitHub Account
</Anchor>
</SelectText>
</div>
</SelectItem>
<SelectItem
state={select}
button
onClick={(event) => {
event.preventDefault();
props.onSwitch();
}}
>
<div className="flex items-center gap-2">
<ListBulletIcon className="w-[1em] h-[1em]" />
Switch Git Provider
</div>
</SelectItem>
</SelectPopover>
</>
);
Expand Down
Loading
Loading