Skip to content

Commit

Permalink
feat(platform): clearer github account selection
Browse files Browse the repository at this point in the history
  • Loading branch information
EYHN committed Sep 2, 2022
1 parent 4060dec commit 5058812
Show file tree
Hide file tree
Showing 9 changed files with 205 additions and 42 deletions.
26 changes: 22 additions & 4 deletions packages/platform-server/src/modules/github/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ class GithubAccount {

@Field(() => String)
avatar_url!: string

@Field(() => String)
type!: string
}

@ObjectType()
Expand Down Expand Up @@ -81,13 +84,28 @@ export class PaginatedGithubInstallations extends Paginated(GithubInstallation)
export class GithubIntegrationResolver {
constructor(private readonly userService: UserService, private readonly service: GithubService) {}

@Query(() => GithubInstallation, {
name: 'githubInstallation',
nullable: true,
description:
'Get installation by the github account connected with the current user. Throws if user is not connected to github account.',
})
async getGithubInstallation(@CurrentUser() user: User) {
const githubAccount = await this.userService.getUserConnectedAccount(user, ExternalAccount.github)
if (!githubAccount || !githubAccount.accessToken) {
throw new UserError('Please connect your github account first.')
}

return this.service.getInstallationByUser(githubAccount.externUsername)
}

@Query(() => PaginatedGithubInstallations, {
name: 'githubInstallations',
name: 'associatedGithubInstallations',
description:
'List all installations of the github account connected by the current user. Throws if user is not connected to github account. \n' +
'List all installations associated with the github account connected by the current user, include joined organizations. Throws if user is not connected to github account. \n' +
'NOTE: Limited by github endpoint, pagination.skip must be a multiple of pagination.first for this function. pagination.after is not supported.',
})
async getGithubInstallations(
async getAssociatedGithubInstallations(
@CurrentUser() user: User,
@Args({ name: 'pagination', nullable: true, defaultValue: { first: 10, skip: 0 } })
paginationInput: PaginationInput,
Expand All @@ -97,7 +115,7 @@ export class GithubIntegrationResolver {
throw new UserError('Please connect your github account first.')
}

return this.service.getInstallationsByUser(paginationInput, githubAccount.accessToken)
return this.service.getAssociatedInstallationsByUser(paginationInput, githubAccount.accessToken)
}

@Query(() => GithubRepoVerificationResult, {
Expand Down
13 changes: 12 additions & 1 deletion packages/platform-server/src/modules/github/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,18 @@ export class GithubService {
return newToken.token
}

async getInstallationsByUser(pagination: PaginationInput, userToken: string) {
async getInstallationByUser(username: string) {
try {
return await this.fetchApi<GithubInstallation>('GET', `https://api.github.com/users/${username}/installation`)
} catch (err) {
if (err instanceof GithubApiError && err.status === 404) {
return null
}
throw err
}
}

async getAssociatedInstallationsByUser(pagination: PaginationInput, userToken: string) {
if (pagination.after) {
throw new UserError('pagination.after is not supported for this function.')
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,18 @@ import { Draft } from 'immer'
import { endWith, exhaustMap, filter, map, Observable, startWith, withLatestFrom } from 'rxjs'

import { createErrorCatcher, GraphQLClient } from '@perfsee/platform/common'
import { githubInstallationsQuery, GithubInstallationsQuery } from '@perfsee/schema'
import { associatedGithubInstallationsQuery } from '@perfsee/schema'

export type Installation = GithubInstallationsQuery['githubInstallations']['edges'][number]['node']
import { Installation } from './github-installation.module'

interface State {
installations: Installation[]
installationsTotalCount: number
loading: boolean
}

@Module('GithubInstallationModel')
export class GithubInstallationModel extends EffectModule<State> {
@Module('AssociatedGithubInstallationsModel')
export class AssociatedGithubInstallationsModel extends EffectModule<State> {
defaultState = {
loading: true,
installationsTotalCount: 0,
Expand All @@ -51,14 +51,14 @@ export class GithubInstallationModel extends EffectModule<State> {
exhaustMap(([_, state]) =>
this.client
.query({
query: githubInstallationsQuery,
query: associatedGithubInstallationsQuery,
variables: { pagination: { first: 30, skip: state.installations.length } },
})
.pipe(
map((data) => {
return this.getActions().append({
installations: data.githubInstallations.edges.map((edge) => edge.node),
totalCount: data.githubInstallations.pageInfo.totalCount,
installations: data.associatedGithubInstallations.edges.map((edge) => edge.node),
totalCount: data.associatedGithubInstallations.pageInfo.totalCount,
})
}),
startWith(this.getActions().setLoading(true)),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
Copyright 2022 ByteDance and/or its affiliates.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import { Effect, EffectModule, ImmerReducer, Module } from '@sigi/core'
import { Draft } from 'immer'
import { endWith, switchMap, map, Observable, startWith } from 'rxjs'

import { createErrorCatcher, GraphQLClient } from '@perfsee/platform/common'
import { githubInstallationQuery, GithubInstallationQuery } from '@perfsee/schema'

export type Installation = NonNullable<GithubInstallationQuery['githubInstallation']>

interface State {
installation: Installation | null
loading: boolean
}

@Module('GithubInstallationModel')
export class GithubInstallationModel extends EffectModule<State> {
defaultState = {
loading: true,
installation: null,
}

constructor(private readonly client: GraphQLClient) {
super()
}

@Effect()
getInstallation(payload$: Observable<void>) {
return payload$.pipe(
switchMap(() =>
this.client
.query({
query: githubInstallationQuery,
})
.pipe(
map((data) => {
return this.getActions().setInstallation(data.githubInstallation)
}),
startWith(this.getActions().setLoading(true)),
endWith(this.getActions().setLoading(false)),
createErrorCatcher(),
),
),
)
}

@ImmerReducer()
setLoading(state: Draft<State>, loading: boolean) {
state.loading = loading
}

@ImmerReducer()
setInstallation(state: Draft<State>, payload: Installation | null) {
state.installation = payload
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,16 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import { MessageBarType, Persona, Text } from '@fluentui/react'
import { MessageBarType, Persona, Spinner, SpinnerSize, Text } from '@fluentui/react'
import { SelectionMode } from '@fluentui/utilities'
import { useModule } from '@sigi/react'
import React, { useCallback, useEffect, useMemo } from 'react'

import { ForeignLink, MessageBar, Table, TableColumnProps } from '@perfsee/components'

import { AssociatedGithubInstallationsModel } from './associated-github-installations.module'
import { GithubInstallationModel, Installation } from './github-installation.module'
import { SelectorContainer } from './style'
import { CenterText, PersonaContainer, SelectorContainer } from './style'

interface Props {
onSelect: (installation: Installation) => void
Expand All @@ -43,24 +44,41 @@ const columns: TableColumnProps<Installation | null>[] = [
]

export const InstallationSelector: React.VFC<Props> = ({ onSelect }) => {
const [{ installations, installationsTotalCount, loading }, dispatch] = useModule(GithubInstallationModel)
const [
{
installations: associatedInstallations,
installationsTotalCount: associatedInstallationsTotalCount,
loading: associatedInstallationsLoading,
},
associatedGithubInstallationsDispatch,
] = useModule(AssociatedGithubInstallationsModel)
const [{ installation, loading: installationLoading }, githubInstallationDispatch] =
useModule(GithubInstallationModel)

useEffect(() => {
dispatch.loadMore()
return dispatch.reset
}, [dispatch])
associatedGithubInstallationsDispatch.loadMore()
return associatedGithubInstallationsDispatch.reset
}, [associatedGithubInstallationsDispatch])

const loadMore = useCallback(() => {
dispatch.loadMore()
}, [dispatch])
associatedGithubInstallationsDispatch.loadMore()
}, [associatedGithubInstallationsDispatch])

useEffect(() => {
githubInstallationDispatch.getInstallation()
return githubInstallationDispatch.reset
}, [githubInstallationDispatch])

const tableItems = useMemo(() => {
if (installations.length === installationsTotalCount) {
return installations
let items
if (associatedInstallations.length === associatedInstallationsTotalCount) {
items = associatedInstallations
} else {
return (installations as (Installation | null)[]).concat([null])
items = (associatedInstallations as (Installation | null)[]).concat([null])
}
}, [installations, installationsTotalCount])

return items.filter((t) => t == null || t.account.type === 'Organization')
}, [associatedInstallations, associatedInstallationsTotalCount])

const handleScroll = useCallback(
(e: React.UIEvent<HTMLDivElement>) => {
Expand All @@ -81,29 +99,47 @@ export const InstallationSelector: React.VFC<Props> = ({ onSelect }) => {
[onSelect],
)

const handleClickYour = useCallback(() => {
if (installation) {
onSelect(installation)
}
}, [onSelect, installation])

return (
<>
{!loading && installationsTotalCount === 0 ? (
<MessageBar messageBarType={MessageBarType.blocked}>
You do not have our Github app installed, please
<ForeignLink href="/github/new">install our Github app</ForeignLink>.
</MessageBar>
<CenterText variant="smallPlus">
<b>Your:</b>
</CenterText>
{!installationLoading ? (
installation ? (
<PersonaContainer horizontal onClick={handleClickYour}>
<Persona imageUrl={installation.account.avatar_url} /> <Text>{installation.account.login}</Text>
</PersonaContainer>
) : (
<MessageBar messageBarType={MessageBarType.blocked}>
You do not have our Github app installed, please
<ForeignLink href="/github/new">install our Github app</ForeignLink>.
</MessageBar>
)
) : (
<MessageBar messageBarType={MessageBarType.info}>
If you can't find the desired Github user or organization below, please
<ForeignLink href="/github/new">install our Github app</ForeignLink> to the user or organization.
</MessageBar>
<Spinner size={SpinnerSize.large} />
)}

<SelectorContainer onScroll={handleScroll}>
<CenterText variant="smallPlus">
<b>Organizations:</b>
</CenterText>
<MessageBar messageBarType={MessageBarType.info}>
If you can't find the desired Github organization below, please
<ForeignLink href="/github/new">install our Github app</ForeignLink> to the user or organization.
</MessageBar>
<SelectorContainer onScroll={handleScroll} size={300}>
<Table
selectionMode={SelectionMode.none}
item
items={tableItems}
columns={columns}
isHeaderVisible={false}
enableShimmer={loading && installations.length === 0}
shimmerLines={8}
enableShimmer={associatedInstallationsLoading && associatedInstallations.length === 0}
shimmerLines={6}
onRowClick={handleRowClick}
/>
</SelectorContainer>
Expand Down
12 changes: 11 additions & 1 deletion packages/platform/src/modules/import-github/style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ limitations under the License.
*/

import styled from '@emotion/styled'
import { Text } from '@fluentui/react'
import { Stack, Text } from '@fluentui/react'

import { Card } from '@perfsee/components'

Expand Down Expand Up @@ -45,3 +45,13 @@ export const DisplayInformation = styled(Text)(({ theme }) => ({
textAlign: 'right',
color: theme.text.colorSecondary,
}))

export const PersonaContainer = styled(Stack)(({ theme }) => ({
border: `1px solid ${theme.border.color}`,
padding: '11px 11px 11px 12px',
alignItems: 'center',
cursor: 'pointer',
'&:hover': {
background: theme.colors.primaryBackground,
},
}))
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
query GithubInstallations($pagination: PaginationInput!) {
githubInstallations(pagination: $pagination) {
query AssociatedGithubInstallations($pagination: PaginationInput!) {
associatedGithubInstallations(pagination: $pagination) {
edges {
node {
id
account {
login
avatar_url
type
}
}
}
Expand Down
10 changes: 10 additions & 0 deletions packages/schema/src/graphql/github/github-installation.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
query GithubInstallation {
githubInstallation {
id
account {
login
avatar_url
type
}
}
}
10 changes: 8 additions & 2 deletions packages/schema/src/server-schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,7 @@ type PaginatedGithubRepositories {
type GithubAccount {
login: String!
avatar_url: String!
type: String!
}

type GithubInstallation {
Expand Down Expand Up @@ -999,10 +1000,15 @@ type Query {
searchUsers(query: String!): [SearchUserResult!]!

"""
List all installations of the github account connected by the current user. Throws if user is not connected to github account.
Get installation by the github account connected with the current user. Throws if user is not connected to github account.
"""
githubInstallation: GithubInstallation

"""
List all installations associated with the github account connected by the current user, include joined organizations. Throws if user is not connected to github account.
NOTE: Limited by github endpoint, pagination.skip must be a multiple of pagination.first for this function. pagination.after is not supported.
"""
githubInstallations(pagination: PaginationInput = {first: 10, skip: 0}): PaginatedGithubInstallations!
associatedGithubInstallations(pagination: PaginationInput = {first: 10, skip: 0}): PaginatedGithubInstallations!

"""
Verify that the github project exists and the current user has permissions to the project. Throws if user is not connected to github account.
Expand Down

0 comments on commit 5058812

Please sign in to comment.