Skip to content

Commit

Permalink
feat(platform,platform-server): make project public to anyone
Browse files Browse the repository at this point in the history
  • Loading branch information
congjiujiu committed Sep 16, 2022
1 parent 1073a82 commit 8fc893b
Show file tree
Hide file tree
Showing 23 changed files with 512 additions and 95 deletions.
47 changes: 23 additions & 24 deletions packages/components/src/color-button/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,32 +15,31 @@ limitations under the License.
*/

import { PrimaryButton, IButtonProps } from '@fluentui/react'
import { merge } from 'lodash'

import { darken } from '@perfsee/dls'

export const ColorButton = (props: IButtonProps & { color: string }) => {
const { color } = props
return (
<PrimaryButton
{...props}
styles={{
root: {
backgroundColor: color,
borderColor: color,
},
rootHovered: {
backgroundColor: darken(color, 0.1),
borderColor: darken(color, 0.1),
},
rootFocused: {
backgroundColor: darken(color, 0.2),
borderColor: darken(color, 0.2),
},
rootPressed: {
backgroundColor: darken(color, 0.2),
borderColor: darken(color, 0.2),
},
}}
/>
)
const { color, styles } = props

const computedStyles = merge(styles, {
root: {
backgroundColor: color,
borderColor: color,
},
rootHovered: {
backgroundColor: darken(color, 0.1),
borderColor: darken(color, 0.1),
},
rootFocused: {
backgroundColor: darken(color, 0.2),
borderColor: darken(color, 0.2),
},
rootPressed: {
backgroundColor: darken(color, 0.2),
borderColor: darken(color, 0.2),
},
})

return <PrimaryButton {...props} styles={computedStyles} />
}
1 change: 1 addition & 0 deletions packages/components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export * from './content-card'
export * from './audit-item'
export * from './format-markdown-link'
export * from './donut-chart'
export * from './modal'

// if any of the components imported ever by non-async modules, then all components will be loaded in sync mode
// which would involve a lot of useless downloading traffic.
Expand Down
105 changes: 105 additions & 0 deletions packages/components/src/modal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
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 { CloseOutlined, ExclamationCircleOutlined, InfoCircleOutlined } from '@ant-design/icons'
import { DefaultButton, Modal as FluentModal, SharedColors, IModalStyles } from '@fluentui/react'
import { memo, useMemo, useCallback } from 'react'
import type { FC, ReactNode } from 'react'

import { ColorButton } from '../color-button'

import { CloseIconWrap, Container, Content, Footer, Header, Icon, Title } from './style'

export enum ModalType {
Confirm,
Warning,
}

export type ModalProps = {
isOpen: boolean
type?: ModalType
title?: string
children?: ReactNode
showCloseIcon?: boolean
confirmDisabled?: boolean
onClose: () => void
onConfirm?: () => void
}

const ModalColors: Record<ModalType, string> = {
[ModalType.Confirm]: SharedColors.cyanBlue10,
[ModalType.Warning]: SharedColors.red10,
}

const ModalIcons: Record<ModalType, ReactNode> = {
[ModalType.Confirm]: <InfoCircleOutlined />,
[ModalType.Warning]: <ExclamationCircleOutlined />,
}

export const Modal: FC<ModalProps> = memo((props) => {
const {
isOpen,
title,
children,
showCloseIcon = true,
type = ModalType.Confirm,
confirmDisabled = false,
onClose,
onConfirm,
} = props

const primaryColor = ModalColors[type]

const modalStyles = useMemo<Partial<IModalStyles>>(
() => ({
main: {
minWidth: '400px',
},
}),
[],
)

const onClickConfirm = useCallback(() => {
if (!confirmDisabled && onConfirm) {
onConfirm()
}
}, [confirmDisabled, onConfirm])

return (
<FluentModal isOpen={isOpen} styles={modalStyles}>
<Container>
<Header>
<Title>
<Icon color={primaryColor}>{ModalIcons[type]}</Icon>
<span>{title}</span>
</Title>
{showCloseIcon && (
<CloseIconWrap onClick={onClose}>
<CloseOutlined />
</CloseIconWrap>
)}
</Header>
<Content>{children}</Content>
<Footer>
<DefaultButton onClick={onClose}>Cancel</DefaultButton>
<ColorButton color={primaryColor} disabled={confirmDisabled} onClick={onClickConfirm}>
Confirm
</ColorButton>
</Footer>
</Container>
</FluentModal>
)
})
77 changes: 77 additions & 0 deletions packages/components/src/modal/style.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
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 styled from '@emotion/styled'

export const Container = styled.div({
display: 'flex',
flexDirection: 'column',
height: '100%',
})

export const Header = styled.div(({ theme }) => ({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '12px',
flexShrink: 0,

borderBottom: `1px solid ${theme.border.color}`,
}))

export const Content = styled.div({
minHeight: '60px',
flex: 1,
})

export const Footer = styled.div({
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
flexShrink: 0,

padding: '12px',

'> button + button': {
marginLeft: '12px',
},
})

export const Title = styled.div({
display: 'flex',
alignItems: 'center',

span: {
fontSize: '18px',
fontWeight: 500,
},
})

export const Icon = styled.div<{ color: string }>(({ color }) => ({
display: 'flex',
alignItems: 'center',
color,
marginRight: '8px',
}))

export const CloseIconWrap = styled.div({
width: '18px',
height: '18px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
})
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,22 @@ test.serial('revoke users permission', async (t) => {
t.false(await permission.check(users[0], project.id, Permission.Read))
})

test.serial('get user permission', async (t) => {
const permission = t.context.module.get(SelfHostPermissionProvider)

const user = users[0]

await permission.grant(user, project.id, Permission.Read)

const permissions = await permission.get(user, project.id)
t.deepEqual(permissions, [Permission.Read])

await permission.grant(user, project.id, Permission.Admin)

const permissions2 = await permission.get(user, project.id)
t.deepEqual(permissions2, [Permission.Read, Permission.Admin])
})

test.serial('user allow list', async (t) => {
const permission = t.context.module.get(SelfHostPermissionProvider)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ export class PermissionProvider {
throw new Error('unimplemented')
}

get(_user: User, _id: number | string): Promise<Permission[]> {
throw new Error('unimplemented')
}

check(_user: User, _id: number | string, _permission: Permission): Promise<boolean> {
throw new Error('unimplemented')
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import { Permission } from '../../def'
import { PermissionProvider } from '../provider'

const PermissionsMap: { [key in Permission]: number } = {
[Permission.All]: 0b11,
[Permission.Admin]: 0b11,
[Permission.Read]: 0b01,
}
Expand All @@ -43,6 +42,17 @@ export class SelfHostPermissionProvider extends PermissionProvider {
)
}

async get(user: User, id: number | string) {
if (user.isAdmin) {
return [Permission.Admin]
}

const project = await Project.findOneByOrFail(typeof id === 'string' ? { slug: id } : { id })

const permissions = await UserPermission.findBy({ userId: user.id, projectId: project.id })
return permissions.map((permission) => permission.permission)
}

async check(user: User, id: number | string, permission: Permission) {
if (user.isAdmin) {
return true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,17 @@ test.serial('check permission', async (t) => {
t.false(accessible)
})

test.serial('get permission', async (t) => {
const service = t.context.module.get(ProjectService)
const permissionProvider: DeepMocked<PermissionProvider> = t.context.module.get(PermissionProvider)

permissionProvider.get.resolves([Permission.Admin])

const permissions = await service.getUserPermission(user, project)

t.deepEqual(permissions, [Permission.Admin])
})

test.serial('project owners', async (t) => {
const service = t.context.module.get(ProjectService)
const permissionProvider: DeepMocked<PermissionProvider> = t.context.module.get(PermissionProvider)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ test('update project', async (t) => {
projectInput: {
artifactBaselineBranch: newBranch,
owners: [user.email, newUser.email],
isPublic: true,
},
},
})
Expand All @@ -230,6 +231,7 @@ test('update project', async (t) => {
response.project.owners.map((o) => o.email),
[user.email, newUser.email],
)
t.is(response.project.isPublic, true)
})

test('unable to update project by no admin permission user', async (t) => {
Expand Down Expand Up @@ -329,6 +331,17 @@ test('unable to star no read permission project', async (t) => {

await client.loginAs(user)

// set project to private
await gqlClient.mutate({
mutation: updateProjectMutation,
variables: {
projectId: project.slug,
projectInput: {
isPublic: false,
},
},
})

await t.throwsAsync(
async () => {
await client.mutate({
Expand Down
5 changes: 5 additions & 0 deletions packages/platform-server/src/modules/project/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,11 @@ export class ProjectResolver {

return true
}

@ResolveField(() => [Permission], { description: 'current user permission to this project' })
async userPermission(@CurrentUser() user: User, @Parent() project: Project) {
return this.projectService.getUserPermission(user, project)
}
}

@Resolver(() => User)
Expand Down
8 changes: 6 additions & 2 deletions packages/platform-server/src/modules/project/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,9 +203,9 @@ export class ProjectService {
}

async update(projectId: number, input: UpdateProjectInput) {
const { artifactBaselineBranch, owners } = input
const { artifactBaselineBranch, owners, isPublic } = input

const projectPayload = omitBy({ artifactBaselineBranch }, isUndefined)
const projectPayload = omitBy({ artifactBaselineBranch, isPublic }, isUndefined)

if (!isEmpty(projectPayload)) {
await Project.update(projectId, projectPayload)
Expand Down Expand Up @@ -340,6 +340,10 @@ export class ProjectService {
}
}

async getUserPermission(user: User, project: Project) {
return this.permissionProvider.get(user, project.id)
}

@Cron(CronExpression.EVERY_HOUR, { exclusive: true, name: 'project-count' })
async recordActiveProject() {
const byDay = await this.getActiveProjectCountByPeriod(1, 'DAY')
Expand Down

0 comments on commit 8fc893b

Please sign in to comment.