feat(desktop):add inviting others into workspace by link (#4712)

* feat(desktop):link inviting

* style(desktop): update style
This commit is contained in:
xudaotutou
2024-04-29 17:44:34 +08:00
committed by GitHub
parent 950e6cb44e
commit 18464891fc
47 changed files with 1124 additions and 448 deletions
@@ -84,12 +84,19 @@
"Added": "Added",
"Invaild Context": "You need switch to other workspace for handling",
"Remove Member Tips": "Determine that you want to remove the member?",
"Quit Workspace Tips":"Confirm leaving workspace?",
"Invalid User ID": "Invalid User ID",
"The invited user must be others": "The invited user must be others",
"Dissovle Tips": "Deleting the workspace will clear all resources. Are you sure you want to disband",
"Enter Confirm.": "Please enter {{value}} to confirm",
"Accept Invitation": "Accept Invitation",
"Recive Tips": "{{managerName}} invite you join in {{teamName}} as {{role}}",
"Receive Tips": "{{managerName}} invite you join in {{teamName}} as {{role}}",
"Invite members to workspace ": "Invite members to workspace {{workspace}}",
"Generate invitation link": "Generate invitation link",
"Failed to generate invitation link": "Failed to generate invitation link",
"Redirecting to homepage in 3 seconds": "Redirecting to homepage in 3 seconds",
"Invalid invitation link": "Invalid invitation link",
"Invitation reminder": "Invitation reminder",
"pay with stripe": "Pay With Stripe",
"pay with wechat": "Pay With Wechat",
"License Buy": "License Buy",
@@ -24,8 +24,6 @@
"confirmNewPassword": "确认新密码",
"passwordMismatch": "密码不一致",
"passwordChangeSuccess": "密码修改成功",
"passwordMismatch": "密码不一致",
"newPassword": "新密码",
"Verify password": "确认密码",
"change": "修改",
"Invalid username or password": "用户名或密码错误",
@@ -80,12 +78,19 @@
"Added": "已加入",
"Invaild Context": "你需要切换到其他工作空间操作",
"Remove Member Tips": "确认要移除该成员?",
"Quit Workspace Tips":"确认要退出工作空间吗?",
"Invalid User ID": "用户的 ID 不合法",
"The invited user must be others": "只能邀请其他人",
"Dissovle Tips": "删除工作空间会清空所有资源,确定要删除吗?",
"Enter Confirm.": "请输入 {{value}} 确认",
"Accept Invitation": "接受邀请",
"Recive Tips": "{{managerName}} 邀请你到 {{teamName}} 当 {{role}}",
"Receive Tips": "{{managerName}} 邀请你到 {{teamName}} 成为 {{role}}",
"Invite members to workspace": "邀请成员至工作空间 {{workspace}}",
"Generate invitation link": "生成邀请链接",
"Failed to generate invitation link": "生成邀请链接失败",
"Redirecting to homepage in 3 seconds": "3秒后跳回主页",
"Invalid invitation link": "邀请链接非法",
"Invitation reminder": "受邀提醒",
"pay with wechat": "微信支付",
"pay with stripe": "Stripe 支付",
"License Buy": "License 购买",
@@ -0,0 +1,127 @@
import { _passwordLoginRequest } from '@/api/auth';
import {
_createRequest,
_getInviteCodeInfoRequest,
_getInviteCodeRequest,
_nsListRequest,
_verifyInviteCodeRequest,
reciveAction
} from '@/api/namespace';
import { NamespaceDto, UserRole } from '@/types/team';
import * as k8s from '@kubernetes/client-node';
import request from '@/__tests__/api/request';
import { _setAuth, cleanDb, cleanK8s } from '@/__tests__/api/tools';
import { AccessTokenPayload } from '@/types/token';
import { prisma } from '@/services/backend/db/init';
import { jwtDecode } from 'jwt-decode';
import { getTeamInviteLimit } from '@/services/enable';
const createRequest = _createRequest(request);
const inviteMemberRequest = _getInviteCodeRequest(request);
const getInviteInfoRequest = _getInviteCodeInfoRequest(request);
const verifyInviteRequest = _verifyInviteCodeRequest(request);
const listNamespaceRequest = _nsListRequest(request);
const TEAM_INVITE_LIMIT = getTeamInviteLimit();
describe('invite member', () => {
let token1: string;
let payload1: AccessTokenPayload;
let token2: string;
let payload2: AccessTokenPayload;
const setAuth = _setAuth(request);
let ns: NamespaceDto;
const passwordLoginRequest = _passwordLoginRequest(request, setAuth);
beforeAll(async () => {
//@ts-ignore
// console.log('MONGODB_URI', uri)
const kc = new k8s.KubeConfig();
await cleanK8s(kc, prisma);
await cleanDb(prisma);
setAuth();
const res = await passwordLoginRequest({ user: 'abdicatetesttest', password: 'testtest' });
// make sure valid session
expect(res!.data).toBeDefined();
token1 = res!.data!.token;
payload1 = jwtDecode<AccessTokenPayload>(token1);
console.log('payload1', payload1);
setAuth();
const res2 = await passwordLoginRequest({ user: 'inviteTesttest2', password: 'testtest2' });
// make sure valid session
expect(res2!.data).toBeDefined();
token2 = res2!.data!.token;
payload2 = jwtDecode<AccessTokenPayload>(token2);
setAuth(token1);
const nsRes = await createRequest({ teamName: 'teamZero' });
expect(nsRes.data?.namespace).toBeDefined();
ns = nsRes.data?.namespace!;
}, 100000);
describe('invite Owner', () => {
it('invite Owner', async () => {
const res = await inviteMemberRequest({
ns_uid: ns.uid,
role: UserRole.Owner
});
expect(res.code).toBe(403);
});
});
describe.each([[UserRole.Developer], [UserRole.Manager]])('owner request', (role) => {
it('null param', async () => {
setAuth(token1);
const res = await inviteMemberRequest({ ns_uid: '', role });
// invalid param
expect(res.code).toBe(400);
const res3 = await inviteMemberRequest({
ns_uid: 'xxx',
role: '' as any
});
expect(res3.code).toBe(400);
});
it('invite prviate team', async () => {
setAuth(token1);
const res = await inviteMemberRequest({
ns_uid: payload1.workspaceUid,
role: role
});
// bug
expect(res.code).toBe(403);
});
it('invite self', async () => {
setAuth(token2);
const ns_uid = ns.uid;
const { data } = await inviteMemberRequest({
ns_uid,
role
});
expect(data).toBeDefined();
const res = await verifyInviteRequest({ code: data!.code, action: reciveAction.Accepte });
expect(res.code).toBe(403);
});
it('invite exist member', async () => {
setAuth(token1);
const nsRes = await createRequest({ teamName: 'teamLimit2' + role });
expect(nsRes.data?.namespace).toBeDefined();
const ns = nsRes.data?.namespace!;
const ns_uid = ns.uid;
const data1 = await inviteMemberRequest({
ns_uid,
role
});
console.log(ns);
const res = await verifyInviteRequest({
code: data1.data!.code,
action: reciveAction.Accepte
});
console.log(payload1);
expect(res.code).toBe(200);
setAuth(token2);
const res2 = await verifyInviteRequest({
code: data1.data!.code,
action: reciveAction.Accepte
});
expect(res2.code).toBe(403);
}, 10000);
});
});
@@ -5,7 +5,6 @@ import { PrismaClient as GlobalPrismaClient } from 'prisma/global/generated/clie
export const cleanK8s = async (kc: k8s.KubeConfig, prisma: PrismaClient) => {
kc.loadFromDefault();
console.log('cleanK8s');
const userCrs = await prisma.userCr.findMany();
await Promise.all(
userCrs.map(async (user) => {
+30 -8
View File
@@ -4,6 +4,7 @@ import { NamespaceDto, UserRole, teamMessageDto } from '@/types/team';
import { TeamUserDto } from '@/types/user';
import { AxiosInstance } from 'axios';
import { Session } from 'sealos-desktop-sdk/*';
export const _abdicateRequest =
(request: AxiosInstance) => (data: { ns_uid: string; targetUserCrUid: string }) =>
request.post<any, ApiResp<null>>('/api/auth/namespace/abdicate', data);
@@ -37,10 +38,31 @@ export enum reciveAction {
Accepte = 'accept',
Reject = 'reject'
}
type verifyParam = { ns_uid: string; action: reciveAction };
type verifyParam = { ns_uid: string; action: reciveAction };
type verifyCodeParam = { code: string; action: reciveAction };
export const _verifyInviteRequest = (request: AxiosInstance) => (data: verifyParam) =>
request.post<verifyParam, ApiResp<{ result: unknown }>>('/api/auth/namespace/verifyInvite', data);
export const _verifyInviteCodeRequest = (request: AxiosInstance) => (data: verifyCodeParam) =>
request.post<verifyCodeParam, ApiResp<{ result: unknown }>>(
'/api/auth/namespace/verifyInviteCode',
data
);
export const _getInviteCodeRequest =
(request: AxiosInstance) => (data: { ns_uid: string; role: UserRole }) =>
request.post<verifyCodeParam, ApiResp<{ code: string }>>(
'/api/auth/namespace/getInviteCode',
data
);
export const _getInviteCodeInfoRequest = (request: AxiosInstance) => (data: { code: string }) =>
request.post<
typeof data,
ApiResp<{
workspace: string;
role: UserRole;
inviterNickname: string;
}>
>('/api/auth/namespace/getInviteCodeInfo', data);
export const _removeMemberRequest =
(request: AxiosInstance) => (data: { ns_uid: string; targetUserCrUid: string }) =>
request.post<typeof data, ApiResp<null>>('/api/auth/namespace/removeUser', data);
@@ -53,13 +75,10 @@ export const _teamDetailsRequest = (request: AxiosInstance) => (ns_uid: string)
export const _reciveMessageRequest = (request: AxiosInstance) => () =>
request.post<any, ApiResp<{ messages: teamMessageDto[] }>>('/api/auth/namespace/recive');
export const _switchRequest = (request: AxiosInstance) => (ns_uid: string) =>
request.post<any, ApiResp<{ token: string; kubeconfig: string; appToken: string }>>(
'/api/auth/namespace/switch',
{
ns_uid
}
);
// 提供给prod/dev环境使用
request.post<any, ApiResp<{ token: string; appToken: string }>>('/api/auth/namespace/switch', {
ns_uid
});
// for prod/dev
export const abdicateRequest = _abdicateRequest(request);
export const createRequest = _createRequest(request);
export const deleteTeamRequest = _deleteTeamRequest(request);
@@ -71,3 +90,6 @@ export const removeMemberRequest = _removeMemberRequest(request);
export const teamDetailsRequest = _teamDetailsRequest(request);
export const reciveMessageRequest = _reciveMessageRequest(request);
export const switchRequest = _switchRequest(request);
export const getInviteCodeRequest = _getInviteCodeRequest(request);
export const getInviteCodeInfoRequest = _getInviteCodeInfoRequest(request);
export const verifyInviteCodeRequest = _verifyInviteCodeRequest(request);
@@ -9,114 +9,22 @@ import {
Stack,
Text,
type UseDisclosureReturn,
PopoverTrigger,
Popover,
PopoverContent,
PopoverBody,
IconButton,
HStack,
VStack
} from '@chakra-ui/react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'next-i18next';
import { useRouter } from 'next/router';
import { useMemo, useState } from 'react';
import useAppStore from '@/stores/app';
import { ApiResp, Region } from '@/types';
import { formatMoney } from '@/utils/format';
import TeamCenter from '@/components/team/TeamCenter';
import NsList from '@/components/team/NsList';
import { nsListRequest, switchRequest } from '@/api/namespace';
import { NSType } from '@/types/team';
import PasswordModify from './PasswordModify';
import { useGlobalStore } from '@/stores/global';
import { CopyIcon, DownloadIcon, LogoutIcon, RightArrowIcon } from '@sealos/ui';
import { ImageFallBackUrl } from '@/stores/config';
import { jwtDecode } from 'jwt-decode';
import { AccessTokenPayload } from '@/types/token';
import { sessionConfig } from '@/utils/sessionConfig';
const NsMenu = () => {
const { t } = useTranslation();
const { session } = useSessionStore();
const ns_uid = session?.user?.ns_uid || '';
const router = useRouter();
const mutation = useMutation({
mutationFn: switchRequest,
async onSuccess(data) {
if (data.code === 200 && !!data.data) {
await sessionConfig(data.data);
} else {
throw Error('session in invalid');
}
}
});
const switchTeam = async ({ uid }: { uid: string }) => {
if (ns_uid !== uid) mutation.mutateAsync(uid).then(router.reload);
};
const { data } = useQuery({
queryKey: ['teamList', 'teamGroup'],
queryFn: nsListRequest
});
const namespaces = data?.data?.namespaces || [];
const namespace = namespaces.find((x) => x.uid === ns_uid);
const defaultNamespace = namespaces.find((x) => x.nstype === NSType.Private);
if (!namespace && defaultNamespace && namespaces.length > 0) {
// will be deleted
switchTeam({ uid: defaultNamespace.uid });
}
return (
<Popover placement="left">
<PopoverTrigger>
{
<Box
px="16px"
cursor={'pointer'}
py="10px"
bgColor={'rgba(255, 255, 255, 0.6)'}
borderRadius={'8px'}
width={'full'}
>
<Text color="#7B838B" fontSize={'11px'} p="4px">
{t('Team')}
</Text>
<Flex
justify={'space-between'}
align={'center'}
_hover={{
bgColor: 'rgba(0, 0, 0, 0.03)'
}}
transition={'0.3s'}
p="4px"
>
<Text fontSize="13px" fontWeight={'500'} color={'#363C42'}>
{namespace?.nstype === NSType.Private ? t('Default Team') : namespace?.teamName}
</Text>
<RightArrowIcon w="16px" h="16px" />
</Flex>
</Box>
}
</PopoverTrigger>
<PopoverContent
shadow={'0px 1.1666667461395264px 2.3333334922790527px 0px rgba(0, 0, 0, 0.2) !important'}
borderRadius={'8px'}
p="6px"
w="250px"
background="linear-gradient(270deg, #F1F1F1 0%, #EEE 43.75%, #ECECEC 100%)"
border={'none'}
>
<PopoverBody px="0" pb="0" pt="4px" maxH={'320px'} overflowY={'auto'}>
<NsList
displayPoint={true}
selected_ns_uid={ns_uid}
click={switchTeam}
namespaces={namespaces}
/>
</PopoverBody>
</PopoverContent>
</Popover>
);
};
export default function Account({ disclosure }: { disclosure: UseDisclosureReturn }) {
const [showId, setShowId] = useState(true);
const { needPassword, rechargeEnabled } = useGlobalStore((state) => state.systemEnv);
@@ -143,12 +51,7 @@ export default function Account({ disclosure }: { disclosure: UseDisclosureRetur
return real_balance;
}, [data]);
const queryclient = useQueryClient();
const kubeconfigQuery = useQuery({
queryKey: [user, 'kubeconfig'],
queryFn: () => request.get<any, ApiResp<{ kubeconfig: string }>>('/api/auth/getKubeconfig'),
enabled: !!user
});
const kubeconfig = kubeconfigQuery.data?.data?.kubeconfig;
const kubeconfig = session?.kubeconfig || '';
const logout = (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault();
delSession();
@@ -217,8 +120,6 @@ export default function Account({ disclosure }: { disclosure: UseDisclosureRetur
/>
</HStack>
<VStack w={'full'} gap={'12px'}>
{/*<RegionMenu />*/}
<NsMenu />
<Stack
direction={'column'}
width={'100%'}
@@ -227,10 +128,6 @@ export default function Account({ disclosure }: { disclosure: UseDisclosureRetur
fontSize={'13px'}
gap={'0px'}
>
<Flex alignItems={'center'} borderBottom={'1px solid #0000001A'} px="16px" py="11px">
<Text>{t('Manage Team')}</Text>
<TeamCenter mr="0" />
</Flex>
{needPassword && (
<Flex
justify={'space-between'}
@@ -42,16 +42,20 @@ export default function RegionToggle() {
return (
<>
{regionList?.length > 1 && (
<HStack h="36px" borderRadius={'12px'} position={'relative'}>
<HStack
// fix for blur
position={'relative'}
>
<HStack
borderRadius={'12px'}
borderRadius={'10px'}
p={'8px 12px'}
gap={'20px'}
background={'rgba(244, 246, 248, 0.7)'}
background={'rgba(244, 246, 248, 0.6)'}
boxShadow={'0px 1px 2px rgba(0, 0, 0, 0.2)'}
fontSize={'14px'}
color={'#152539'}
fontWeight={'500'}
backdropFilter={'blur(8px)'}
onClick={() => disclosure.onOpen()}
>
<Text>
@@ -59,13 +63,17 @@ export default function RegionToggle() {
</Text>
<ExchangeIcon />
</HStack>
{disclosure.isOpen ? (
<>
<Box
position={'fixed'}
inset={0}
zIndex={'998'}
onClick={() => disclosure.onClose()}
onClick={(e) => {
e.stopPropagation();
disclosure.onClose();
}}
></Box>
<Box position={'absolute'} inset={0} zIndex={'999'}>
<Box
@@ -73,7 +81,7 @@ export default function RegionToggle() {
bg="rgba(255, 255, 255, 0.8)"
boxShadow={'0px 1px 2px rgba(0, 0, 0, 0.2)'}
position={'absolute'}
top="48px"
top="43px"
right={0}
cursor={'initial'}
borderRadius={'8px'}
@@ -14,27 +14,30 @@ import {
MenuItem,
Text,
Flex,
useToast,
Spinner,
FlexProps,
ButtonProps
ButtonProps,
HStack,
useToast
} from '@chakra-ui/react';
import CustomInput from './Input';
import { useState } from 'react';
import { MouseEventHandler, useState } from 'react';
import { ROLE_LIST, UserRole } from '@/types/team';
import useSessionStore from '@/stores/session';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { inviteMemberRequest } from '@/api/namespace';
import { getInviteCodeRequest, inviteMemberRequest } from '@/api/namespace';
import { vaildManage } from '@/utils/tools';
import { ApiResp } from '@/types';
import { useTranslation } from 'react-i18next';
import { GroupAddIcon } from '@sealos/ui';
import { useCopyData } from '@/hooks/useCopyData';
export default function InviteMember({
ns_uid,
workspaceName,
ownRole,
...props
}: ButtonProps & {
ns_uid: string;
workspaceName: string;
ownRole: UserRole;
}) {
const { onOpen, isOpen, onClose } = useDisclosure();
@@ -59,34 +62,34 @@ export default function InviteMember({
});
}
});
const canManage = vaildManage(ownRole, 'x');
const canManage = vaildManage(ownRole);
const { t } = useTranslation();
const submit = () => {
let trim_to = userId.trim();
if (!trim_to || trim_to.length !== 10) {
const { copyData } = useCopyData();
const getLinkCode = useMutation({
mutationFn: getInviteCodeRequest,
mutationKey: [session?.user.ns_uid],
onError() {
toast({
status: 'error',
title: t('Invalid User ID'),
title: t('Failed to generate invitation link'),
isClosable: true,
position: 'top'
});
return;
}
const tk8s_username = trim_to.startsWith('ns-') ? trim_to.replace('ns-', '') : trim_to;
if (tk8s_username === k8s_username) {
toast({
status: 'error',
title: t('The invited user must be others'),
isClosable: true,
position: 'top'
});
return;
}
mutation.mutate({
});
const generateLink = (code: string) => {
return window.location.origin + encodeURI(`/WorkspaceInvite/?code=${code}`);
};
const handleGenLink: MouseEventHandler<HTMLButtonElement> = async (e) => {
e.preventDefault();
const data = await getLinkCode.mutateAsync({
ns_uid,
targetUserId: trim_to,
role
});
const code = data.data?.code!;
const link = generateLink(code);
await copyData(link);
};
return (
<>
@@ -121,69 +124,66 @@ export default function InviteMember({
<Spinner mx="auto" />
) : (
<ModalBody h="100%" w="100%" p="0" mt="22px">
<CustomInput
onChange={(e) => {
e.preventDefault();
setUserId(e.target.value);
}}
placeholder={t('private team ID of user') || ''}
value={userId}
/>
<Menu>
<MenuButton
as={Button}
variant={'unstyled'}
borderRadius="2px"
border="1px solid #DEE0E2"
bgColor="#FBFBFC"
w="100%"
mt="24px"
<Text>
{t('Invite members to workspace', {
workspace: workspaceName
})}
</Text>
<HStack gap={'8px'} justifyContent={'stretch'}>
<Menu>
<MenuButton
as={Button}
variant={'unstyled'}
borderRadius="2px"
border="1px solid #DEE0E2"
bgColor="#FBFBFC"
w="140px"
mt="24px"
px="12px"
>
<Flex alignItems={'center'} justifyContent={'space-between'}>
<Text>{ROLE_LIST[role]}</Text>
<Image
src="/images/material-symbols_expand-more-rounded.svg"
w="16px"
h="16px"
transform={'rotate(90deg)'}
/>
</Flex>
</MenuButton>
<MenuList borderRadius={'2px'} minW={'unset'}>
{ROLE_LIST.map((role, idx) => (
<MenuItem
w="140px"
onClick={(e) => {
e.preventDefault();
setRole(idx);
}}
key={idx}
>
{role}
</MenuItem>
)).filter((_, i) => canManage(i, false) && i !== UserRole.Owner)}
</MenuList>
</Menu>
<Button
flex={'1'}
variant={''}
bg="#24282C"
borderRadius={'4px'}
color="#fff"
py="6px"
px="12px"
mt="24px"
_hover={{
opacity: '0.8'
}}
isDisabled={getLinkCode.isLoading}
onClick={handleGenLink}
>
<Flex alignItems={'center'} justifyContent={'space-between'}>
<Text>{ROLE_LIST[role]}</Text>
<Image
src="/images/material-symbols_expand-more-rounded.svg"
w="16px"
h="16px"
transform={'rotate(90deg)'}
/>
</Flex>
</MenuButton>
<MenuList borderRadius={'2px'}>
{ROLE_LIST.map((role, idx) => (
<MenuItem
w="330px"
onClick={(e) => {
e.preventDefault();
setRole(idx);
}}
key={idx}
>
{role}
</MenuItem>
)).filter((_, i) => canManage(i, 'y') && i !== UserRole.Owner)}
</MenuList>
</Menu>
<Button
w="100%"
variant={'unstyled'}
bg="#24282C"
borderRadius={'4px'}
color="#fff"
py="6px"
px="12px"
mt="24px"
_hover={{
opacity: '0.8'
}}
onClick={(e) => {
e.preventDefault();
submit();
}}
>
{t('Confirm')}
</Button>
{t('Generate invitation link')}
</Button>
</HStack>
</ModalBody>
)}
</ModalContent>
@@ -1,78 +0,0 @@
import { useCopyData } from '@/hooks/useCopyData';
import { Box, Flex, Text } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import { NSType, NamespaceDto } from '@/types/team';
import { useTranslation } from 'react-i18next';
const NsList = ({
click,
selected_ns_uid,
namespaces,
displayPoint = false,
...boxprop
}: {
displayPoint: boolean;
click?: (ns: NamespaceDto) => void;
selected_ns_uid: string;
namespaces: NamespaceDto[];
} & Parameters<typeof Box>[0]) => {
const queryClient = useQueryClient();
const { t } = useTranslation();
return (
<Box {...boxprop}>
{namespaces.length > 0 &&
namespaces.map((ns) => (
<Flex
key={ns.id}
align={'center'}
py="10px"
mb="2px"
position={'relative'}
borderRadius="2px"
onClick={(e) => {
e.preventDefault();
queryClient.invalidateQueries({ queryKey: ['teamList'] });
click && click(ns);
}}
cursor={'pointer'}
{...(selected_ns_uid === ns.uid
? {
background: 'rgba(0, 0, 0, 0.05)'
}
: {
bgColor: 'unset'
})}
px={'4px'}
_hover={{
'> .namespace-option': {
display: 'flex'
},
bgColor: 'rgba(0, 0, 0, 0.03)'
}}
>
<Flex align={'center'} width={'full'}>
<Box
h="8px"
w={displayPoint ? '8px' : '0'}
mr="8px"
borderRadius="50%"
bgColor={selected_ns_uid === ns.uid ? '#47C8BF' : '#9699B4'}
/>
<Text
fontSize={'14px'}
{...(selected_ns_uid === ns.uid
? {
color: '#0884DD'
}
: {})}
textTransform={'capitalize'}
>
{ns.nstype === NSType.Private ? t('Default Team') : ns.teamName}
</Text>
</Flex>
</Flex>
))}
</Box>
);
};
export default NsList;
@@ -0,0 +1,69 @@
import { Box, Flex, FlexProps, HStack, Text, VStack } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
const NsListItem = ({
isSelected,
isPrivate,
displayPoint = false,
teamName,
...flexprop
}: {
displayPoint: boolean;
teamName: string;
isPrivate: boolean;
isSelected: boolean;
} & FlexProps) => {
const queryClient = useQueryClient();
const { t } = useTranslation();
return (
<Flex
align={'center'}
p="6px 4px"
mb="2px"
position={'relative'}
borderRadius="4px"
onClick={(e) => {
e.preventDefault();
queryClient.invalidateQueries({ queryKey: ['teamList'] });
}}
cursor={'pointer'}
{...flexprop}
{...(isSelected
? {
background: 'rgba(0, 0, 0, 0.05)'
}
: {
bgColor: 'unset'
})}
_hover={{
'> .namespace-option': {
display: 'flex'
},
bgColor: 'rgba(0, 0, 0, 0.03)'
}}
>
<HStack gap={'8px'} align={'center'} width={'full'}>
<Box
h="8px"
w={displayPoint ? '8px' : '0'}
m="4px"
borderRadius="50%"
bgColor={isSelected ? '#47C8BF' : '#9699B4'}
/>
<Text
{...(isSelected
? {
color: '#0884DD'
}
: {})}
textTransform={'capitalize'}
>
{isPrivate ? t('Default Team') : teamName}
</Text>
</HStack>
</Flex>
);
};
export default NsListItem;
@@ -39,7 +39,7 @@ export default function ReciveMessage({
});
const { t, i18n } = useTranslation();
const inviteTips = ({ managerName, teamName, role }: Record<string, string>) =>
t('Recive Tips', {
t('Receive Tips', {
managerName,
teamName,
role
@@ -20,6 +20,7 @@ import { removeMemberRequest } from '@/api/namespace';
import { useCustomToast } from '@/hooks/useCustomToast';
import { ApiResp } from '@/types';
import { useTranslation } from 'react-i18next';
export default function RemoveMember({
ns_uid,
status,
@@ -41,10 +42,7 @@ export default function RemoveMember({
const mutation = useMutation({
mutationFn: removeMemberRequest,
onSuccess() {
queryClient.invalidateQueries({
queryKey: ['ns-detail'],
exact: false
});
queryClient.invalidateQueries();
onClose();
},
onError(error) {
@@ -61,6 +59,8 @@ export default function RemoveMember({
: selfUserCrUid === targetUserCrUid
? t('Quit')
: t('Remove');
const removeTips =
selfUserCrUid === targetUserCrUid ? t('Quit Workspace Tips') : t('Remove Member Tips');
return (
<>
<Button
@@ -98,7 +98,7 @@ export default function RemoveMember({
<Spinner mx="auto" />
) : (
<ModalBody h="100%" w="100%" p="0" mt="22px">
<Text>{t('Remove Member Tips')}</Text>
<Text>{removeTips}</Text>
<Flex mt="37px" justify={'flex-end'} gap={'12px'}>
<Button
variant={'unstyled'}
@@ -2,7 +2,6 @@ import {
Box,
Flex,
Text,
Image,
Modal,
ModalBody,
ModalCloseButton,
@@ -15,9 +14,10 @@ import {
ButtonProps,
Center,
VStack,
Circle
Circle,
HStack,
StackProps
} from '@chakra-ui/react';
import NsList from './NsList';
import { useEffect, useState } from 'react';
import CreateTeam from './CreateTeam';
import DissolveTeam from './DissolveTeam';
@@ -34,8 +34,9 @@ import { nsListRequest, reciveMessageRequest, teamDetailsRequest } from '@/api/n
import { useTranslation } from 'react-i18next';
import { CopyIcon, ListIcon, SettingIcon, StorageIcon } from '@sealos/ui';
import { GetUserDefaultNameSpace } from '@/services/backend/kubernetes/user';
import NsListItem from '@/components/team/NsListItem';
export default function TeamCenter(props: ButtonProps) {
export default function TeamCenter(props: StackProps) {
const session = useSessionStore((s) => s.session);
const { t } = useTranslation();
const user = session?.user;
@@ -102,19 +103,26 @@ export default function TeamCenter(props: ButtonProps) {
}, [_namespaces, ns_uid]);
return (
<>
<IconButton
<HStack
gap={'8px'}
alignItems={'center'}
// borderBottom={'1px solid #0000001A'}
p={'6px 4px'}
cursor={'pointer'}
borderRadius={'4px'}
onClick={() => {
// 清理消息过滤
setMessageFilter([]);
onOpen();
}}
variant={'white-bg-icon'}
p="4px"
ml="auto"
icon={<SettingIcon boxSize={'20px'} color={'#219BF4'} />}
aria-label={'open team center'}
{...props}
/>
_hover={{
bgColor: 'rgba(0, 0, 0, 0.03)'
}}
>
<SettingIcon boxSize={'16px'} color={'grayModern.600'} />
<Text>{t('Manage Team')}</Text>
</HStack>
<Modal isOpen={isOpen} onClose={onClose} isCentered closeOnOverlayClick={false}>
<ModalOverlay />
<ModalContent
@@ -134,7 +142,6 @@ export default function TeamCenter(props: ButtonProps) {
message={message}
key={message.ns_uid}
CloseTipHandler={(ns_uid) => {
// 纳入被过滤的消息
setMessageFilter([...messageFilter, ns_uid]);
}}
/>
@@ -157,15 +164,25 @@ export default function TeamCenter(props: ButtonProps) {
</Flex>
<Box overflow={'scroll'} h="0" flex="1" px="16px">
{namespaces && namespaces.length > 0 ? (
<NsList
displayPoint={false}
selected_ns_uid={ns_uid}
click={(ns) => {
setNs_uid(ns.uid);
setNsid(ns.id);
}}
namespaces={namespaces || []}
/>
namespaces.map((ns) => {
return (
<NsListItem
key={ns.uid}
width={'full'}
onClick={() => {
setNs_uid(ns.uid);
setNsid(ns.id);
}}
p={'7.5px 9px'}
fontSize={'14px'}
displayPoint={false}
id={ns.uid}
isPrivate={ns.nstype === NSType.Private}
isSelected={ns.uid === ns_uid}
teamName={ns.teamName}
/>
);
})
) : (
<Center w="full" h="full">
<Text color={'grayModern.600'} fontSize={'12px'}>
@@ -258,6 +275,7 @@ export default function TeamCenter(props: ButtonProps) {
<InviteMember
ownRole={curTeamUser?.role ?? UserRole.Developer}
ns_uid={ns_uid}
workspaceName={namespace.teamName}
ml="auto"
/>
)}
@@ -267,7 +285,7 @@ export default function TeamCenter(props: ButtonProps) {
</Box>
</Stack>
</>
) : (
) : namespaces.length === 0 ? (
<Center w="full" flex={'1'}>
<VStack gap={'20px'}>
<Circle size={'48px'} border={'0.5px dashed'} borderColor={'grayModern.400'}>
@@ -279,7 +297,7 @@ export default function TeamCenter(props: ButtonProps) {
<CreateTeam textButton />
</VStack>
</Center>
)}
) : null}
</VStack>
</ModalBody>
</ModalContent>
@@ -0,0 +1,129 @@
import { Box, Divider, HStack, Text, useDisclosure, VStack } from '@chakra-ui/react';
import { ExchangeIcon } from '@sealos/ui';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'next-i18next';
import useSessionStore from '@/stores/session';
import { useRouter } from 'next/router';
import { nsListRequest, switchRequest } from '@/api/namespace';
import { sessionConfig } from '@/utils/sessionConfig';
import { NSType } from '@/types/team';
import TeamCenter from '@/components/team/TeamCenter';
import NsListItem from '@/components/team/NsListItem';
import useAppStore from '@/stores/app';
import { jwtDecode } from 'jwt-decode';
import { AccessTokenPayload } from '@/types/token';
import { switchKubeconfigNamespace } from '@/utils/switchKubeconfigNamespace';
export default function WorkspaceToggle() {
const disclosure = useDisclosure();
const { setWorkSpaceId, session } = useSessionStore();
const { t } = useTranslation();
const ns_uid = session?.user?.ns_uid || '';
const router = useRouter();
const queryClient = useQueryClient();
const { init } = useAppStore();
const mutation = useMutation({
mutationFn: switchRequest,
async onSuccess(data) {
if (data.code === 200 && !!data.data && session) {
const payload = jwtDecode<AccessTokenPayload>(data.data.token);
console.log(payload, session);
await sessionConfig({
...data.data,
kubeconfig: switchKubeconfigNamespace(session.kubeconfig, payload.workspaceId)
});
queryClient.clear();
router.reload();
} else {
throw Error('session in invalid');
}
}
});
const switchTeam = async ({ uid }: { uid: string }) => {
if (ns_uid !== uid && !mutation.isLoading) return mutation.mutateAsync(uid);
};
const { data } = useQuery({
queryKey: ['teamList', 'teamGroup'],
queryFn: nsListRequest
});
const namespaces = data?.data?.namespaces || [];
const namespace = namespaces.find((x) => x.uid === ns_uid);
const defaultNamespace = namespaces.find((x) => x.nstype === NSType.Private);
if (!namespace && defaultNamespace && namespaces.length > 0) {
// will be deleted
switchTeam({ uid: defaultNamespace.uid });
}
return (
<HStack position={'relative'}>
<HStack
borderRadius={'10px'}
p={'8px 12px'}
justifyContent={'space-between'}
background={'rgba(244, 246, 248, 0.6)'}
boxShadow={'0px 1px 2px rgba(0, 0, 0, 0.2)'}
fontSize={'13px'}
color={'#152539'}
fontWeight={'500'}
minW={'166px'}
backdropFilter={'blur(8px)'}
onClick={() => disclosure.onOpen()}
>
<Text>
{namespace?.nstype === NSType.Private ? t('Default Team') : namespace?.teamName}
</Text>
<ExchangeIcon />
</HStack>
{disclosure.isOpen ? (
<>
<Box
position={'fixed'}
inset={0}
zIndex={'998'}
onClick={(e) => {
e.stopPropagation();
disclosure.onClose();
}}
/>
<Box position={'absolute'} inset={0} zIndex={'999'} fontSize={'13px'}>
<Box
bgColor={'red'}
bg="rgba(255, 255, 255, 0.8)"
boxShadow={'0px 1px 2px rgba(0, 0, 0, 0.2)'}
position={'absolute'}
top="43px"
right={0}
left={0}
cursor={'initial'}
borderRadius={'8px'}
p="6px"
backdropFilter={'blur(150px)'}
>
<VStack gap={0} alignItems={'stretch'}>
<TeamCenter />
<Divider bgColor={'rgba(0, 0, 0, 0.1)'} my={'4px'} h={'1px'} />
{namespaces.map((ns) => {
return (
<NsListItem
key={ns.uid}
width={'full'}
onClick={() => {
switchTeam({ uid: ns.uid });
}}
displayPoint={true}
id={ns.uid}
isPrivate={ns.nstype === NSType.Private}
isSelected={ns.uid === ns_uid}
teamName={ns.teamName}
/>
);
})}
</VStack>
</Box>
</Box>
</>
) : null}
</HStack>
);
}
@@ -19,6 +19,7 @@ import RemoveMember from './RemoveMember';
import Abdication from './Abdication';
import ModifyRole from './ModifyRole';
import { useTranslation } from 'react-i18next';
export default function UserTable({
users = [],
isTeam,
@@ -39,11 +40,10 @@ export default function UserTable({
const userSelf = users.find(
(user) => user.crUid === userCrUid && user.k8s_username === k8s_username
);
const canManage = vaildManage(userSelf?.role ?? UserRole.Developer, '');
const abdicationUser = users.filter(
const canManage = vaildManage(userSelf?.role ?? UserRole.Developer);
const otherWorkspaceUsers = users.filter(
(user) => user.uid !== userSelf?.uid && user.status === InvitedStatus.Accepted
);
const vaildateRoles: UserRole[] = [];
if (userSelf?.role === UserRole.Owner) vaildateRoles.push(UserRole.Manager, UserRole.Developer);
else if (userSelf?.role === UserRole.Manager) vaildateRoles.push(UserRole.Developer);
@@ -108,10 +108,12 @@ export default function UserTable({
{status[user.status]}
</Td>
<Td py="5px">
{isTeam ? (
userCrUid && canManage(user.role, userCrUid) ? (
user.role === UserRole.Owner && abdicationUser.length !== 0 ? (
<Abdication ns_uid={ns_uid} users={abdicationUser} />
{isTeam &&
(userCrUid &&
canManage(user.role, user.crUid === userCrUid) &&
otherWorkspaceUsers.length !== 0 ? (
user.role === UserRole.Owner ? (
<Abdication ns_uid={ns_uid} users={otherWorkspaceUsers} />
) : userCrUid !== user.uid ? (
<RemoveMember
nsid={nsid}
@@ -120,15 +122,8 @@ export default function UserTable({
k8s_username={user.k8s_username}
targetUserCrUid={user.crUid}
/>
) : (
<></>
)
) : (
<></>
)
) : (
<></>
)}
) : null
) : null)}
</Td>
</Tr>
))}
@@ -6,8 +6,9 @@ import LangSelectSimple from '../LangSelect/simple';
import Iconfont from '../iconfont';
import GithubComponent from './github';
import { ImageFallBackUrl, useSystemConfigStore } from '@/stores/config';
import { ReactElement, useState } from 'react';
import { ReactElement, useCallback, useState } from 'react';
import RegionToggle from '@/components/region/RegionToggle';
import WorkspaceToggle from '@/components/team/WorkspaceToggle';
enum UserMenuKeys {
LangSelect,
@@ -23,6 +24,7 @@ export default function Index(props: { userMenuStyleProps?: FlexProps }) {
const { systemConfig } = useSystemConfigStore();
const userInfo = useSessionStore((state) => state.session);
const onAmount = useCallback((amount: number) => setNotificationAmount(amount), []);
const {
userMenuStyleProps = {
alignItems: 'center',
@@ -37,7 +39,8 @@ export default function Index(props: { userMenuStyleProps?: FlexProps }) {
const baseItemStyle = {
w: '36px',
h: '36px',
background: 'rgba(244, 246, 248, 0.7)',
background: 'rgba(244, 246, 248, 0.6)',
boxShadow: '0px 1.2px 2.3px rgba(0, 0, 0, 0.2)'
};
@@ -49,17 +52,9 @@ export default function Index(props: { userMenuStyleProps?: FlexProps }) {
}[] = [
{
key: UserMenuKeys.Notification,
button: (
<Iconfont iconName="icon-notifications" width={20} height={20} color="#24282C"></Iconfont>
),
button: <Iconfont iconName="icon-notifications" width={20} height={20}></Iconfont>,
click: () => showDisclosure.onOpen(),
content: (
<Notification
key={'notification'}
disclosure={showDisclosure}
onAmount={(amount) => setNotificationAmount(amount)}
/>
)
content: <Notification key={'notification'} disclosure={showDisclosure} onAmount={onAmount} />
},
{
key: UserMenuKeys.Account,
@@ -80,6 +75,7 @@ export default function Index(props: { userMenuStyleProps?: FlexProps }) {
return (
<Flex {...userMenuStyleProps}>
<RegionToggle />
<WorkspaceToggle />
<LangSelectSimple {...baseItemStyle} />
{systemConfig?.showGithubStar && <GithubComponent {...baseItemStyle} />}
{buttonList.map((item) => (
@@ -91,7 +87,7 @@ export default function Index(props: { userMenuStyleProps?: FlexProps }) {
position={'relative'}
{...baseItemStyle}
>
<Center w="100%" h="100%" onClick={item.click} cursor={'pointer'}>
<Center w="100%" h="100%" onClick={item.click} cursor={'pointer'} borderRadius={'50%'}>
{item.button}
</Center>
{item.content}
-1
View File
@@ -49,7 +49,6 @@ export default function useDriver({ openDesktopApp }: { openDesktopApp: any }) {
const { systemEnv } = useGlobalStore();
const handleSkipGuide = () => {
console.log('handleSkipGuide');
setShowGuide(false);
updateDesktopGuide().catch((err) => {
console.log(err);
@@ -0,0 +1,174 @@
import type { NextPage } from 'next';
import { useRouter } from 'next/router';
import useSessionStore from '@/stores/session';
import { Button, Flex, HStack, Image, Spinner, Text, VStack } from '@chakra-ui/react';
import { isString } from 'lodash';
import { dehydrate, QueryClient, useMutation, useQuery } from '@tanstack/react-query';
import {
getInviteCodeInfoRequest,
nsListRequest,
reciveAction,
verifyInviteCodeRequest
} from '@/api/namespace';
import useCallbackStore from '@/stores/callback';
import { useEffect } from 'react';
import { useTranslation } from 'next-i18next';
import { ImageFallBackUrl } from '@/stores/config';
import { ROLE_LIST } from '@/types/team';
import { compareFirstLanguages } from '@/utils/tools';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
const Callback: NextPage = () => {
const router = useRouter();
const { token: curToken, session } = useSessionStore((s) => s);
const { lastWorkSpaceId } = useSessionStore();
const { setWorkspaceInviteCode } = useCallbackStore();
const { t } = useTranslation();
const verifyMutation = useMutation({
mutationFn: verifyInviteCodeRequest
});
const inviteTips = ({ managerName, teamName, role }: Record<string, string>) =>
t('Receive Tips', {
managerName,
teamName,
role
});
const inviteCode = router.query.code;
useEffect(() => {
if (!isString(inviteCode)) return;
setWorkspaceInviteCode(inviteCode);
}, [inviteCode]);
const infoResp = useQuery({
queryKey: [inviteCode],
queryFn: () =>
getInviteCodeInfoRequest({
code: inviteCode as string
}),
enabled: isString(inviteCode)
});
const reset = () => {
setWorkspaceInviteCode();
router.replace('/');
};
const isValid = !!(infoResp.isSuccess && infoResp.data?.data);
const statusCode = infoResp.data?.code;
const needLogin = statusCode === 401;
useEffect(() => {
if (needLogin) {
router.replace('/');
return;
}
if (statusCode === 204) {
// already exist
reset();
return;
}
if (!isValid) {
const tag = setTimeout(reset, 3000);
return () => clearTimeout(tag);
}
}, [statusCode, isValid, router]);
const acceptHandle = async () => {
verifyMutation
.mutateAsync({
code: inviteCode as string,
action: reciveAction.Accepte
})
.then(() => {
reset();
return;
});
};
const cancelHandle = () => {
if (!isValid) return;
reset();
return;
};
return (
<Flex
w={'full'}
h={'full'}
justify={'center'}
align={'center'}
onClick={cancelHandle}
bgColor={'#F3F4F5'}
>
<VStack
onClick={(e) => e.stopPropagation()}
borderRadius={'16px'}
w={'590px'}
overflow={'hidden'}
bgColor={'white'}
boxShadow="0px 8px 29px 0px #BBC4CE40"
h={'430px'}
>
<Flex
alignSelf={'stretch'}
py={'10px'}
justifyContent={'center'}
alignItems={'center'}
bgColor={'grayModern.900'}
color={'white'}
gap={'8px'}
>
{' '}
<Image boxSize={'34px'} borderRadius="full" src={ImageFallBackUrl} alt="logo" />
<Text fontWeight={700} fontSize={'24px'}>
Sealos
</Text>
</Flex>
{isValid ? (
<VStack gap={'40px'} alignItems={'center'} flex={1}>
<Text fontSize={'30px'} fontWeight={'600'} mt={'60px'}>
{t('Invitation reminder')}
</Text>
<Text fontSize={'16px'} fontWeight={'400'} mb={'40px'} w={'330px'}>
{inviteTips({
managerName: infoResp.data.data!.inviterNickname,
teamName: infoResp.data.data!.workspace,
role: ROLE_LIST[infoResp.data.data!.role]
})}
</Text>
<Button
p={'8px 165px'}
borderRadius={'64px'}
disabled={infoResp.isSuccess || verifyMutation.isLoading}
color={'#FEFEFE'}
onClick={async (e) => {
e.stopPropagation();
await acceptHandle();
}}
>
{t('Accept Invitation')}
</Button>
</VStack>
) : infoResp.isError ? (
<VStack alignItems={'center'} gap={'40px'} flex={1} justifyContent={'center'}>
<Text fontSize={'30px'} fontWeight={'600'}>
{t('Invalid invitation link')}
</Text>
<Text fontSize={'16px'} fontWeight={'400'} maxW={'330px'}>
{t('Redirecting to homepage in 3 seconds')}
</Text>
</VStack>
) : null}
</VStack>
</Flex>
);
};
export async function getServerSideProps({ req, res, locales }: any) {
const local =
req?.cookies?.NEXT_LOCALE || compareFirstLanguages(req?.headers?.['accept-language'] || 'zh');
res.setHeader('Set-Cookie', `NEXT_LOCALE=${local}; Max-Age=2592000; Secure; SameSite=None`);
const queryClient = new QueryClient();
const props = {
...(await serverSideTranslations(local, undefined, null, locales || [])),
dehydratedState: dehydrate(queryClient)
};
return {
props
};
}
export default Callback;
@@ -2,7 +2,8 @@ import { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/services/backend/response';
import { getUserKubeconfigNotPatch } from '@/services/backend/kubernetes/admin';
import { verifyAccessToken } from '@/services/backend/auth';
import { switchKubeconfigNamespace } from '@/services/backend/kubernetes/user';
import { switchKubeconfigNamespace } from '@/utils/switchKubeconfigNamespace';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
@@ -15,7 +16,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const defaultKc = await getUserKubeconfigNotPatch(regionUser.userCrName);
if (!defaultKc) throw new Error('get kubeconfig error, regionUserUid: ' + regionUser.userCrUid);
const kubeconfig = switchKubeconfigNamespace(defaultKc, regionUser.workspaceId);
return jsonRes(res, {
code: 200,
message: 'Successfully',
@@ -0,0 +1,69 @@
import { jsonRes } from '@/services/backend/response';
import { bindingRole } from '@/services/backend/team';
import { UserRole } from '@/types/team';
import { isUserRole, roleToUserRole, vaildManage } from '@/utils/tools';
import { NextApiRequest, NextApiResponse } from 'next';
import { globalPrisma, prisma } from '@/services/backend/db/init';
import { v4, validate } from 'uuid';
import { JoinStatus } from 'prisma/region/generated/client';
import { verifyAccessToken } from '@/services/backend/auth';
import { getTeamInviteLimit } from '@/services/enable';
const TEAM_INVITE_LIMIT = getTeamInviteLimit();
import { Role } from 'prisma/region/generated/client';
import { addOrUpdateInviteCode } from '@/services/backend/db/workspaceInviteCode';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const payload = await verifyAccessToken(req.headers);
if (!payload) return jsonRes(res, { code: 401, message: 'token verify error' });
const { ns_uid, role } = req.body as {
ns_uid?: string;
role?: UserRole;
};
if (!ns_uid || !validate(ns_uid))
return jsonRes(res, { code: 400, message: 'ns_uid is invaild' });
if (!isUserRole(role)) return jsonRes(res, { code: 400, message: 'role is required' });
if (role === UserRole.Owner) return jsonRes(res, { code: 403, message: 'role must be others' });
const queryResults = await prisma.userWorkspace.findMany({
where: {
workspaceUid: ns_uid,
status: {
in: [JoinStatus.IN_WORKSPACE, JoinStatus.INVITED]
}
},
include: {
workspace: true,
userCr: true
}
});
const own = queryResults.find((x) => x.userCrUid === payload.userCrUid);
if (!own) return jsonRes(res, { code: 403, message: 'you are not in the namespace' });
if (own.isPrivate) return jsonRes(res, { code: 403, message: 'the namespace is invalid' });
const vaild = ([Role.OWNER, Role.MANAGER] as Role[]).includes(own.role);
if (!vaild) return jsonRes(res, { code: 403, message: 'you are not manager' });
if (queryResults.length >= TEAM_INVITE_LIMIT)
return jsonRes(res, { code: 403, message: 'these invitees are too many' });
const spec = {
inviterUid: payload.userUid,
workspaceUid: ns_uid,
inviterCrUid: payload.userCrUid,
role
};
const code = v4();
const result = await addOrUpdateInviteCode({
...spec,
code
});
if (!result) throw Error('invite member error');
return jsonRes(res, {
code: 200,
data: {
code
},
message: 'Successfully'
});
} catch (e) {
console.log(e);
return jsonRes(res, { code: 500, message: 'invite member error' });
}
}
@@ -0,0 +1,55 @@
import { jsonRes } from '@/services/backend/response';
import { roleToUserRole, vaildManage } from '@/utils/tools';
import { NextApiRequest, NextApiResponse } from 'next';
import { globalPrisma, prisma } from '@/services/backend/db/init';
import { verifyAccessToken } from '@/services/backend/auth';
import { findInviteCode } from '@/services/backend/db/workspaceInviteCode';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const payload = await verifyAccessToken(req.headers);
if (!payload) return jsonRes(res, { code: 401, message: 'token verify error' });
const { code } = req.body as {
code?: string;
};
if (!code) return jsonRes(res, { code: 400, message: 'code is required' });
const linkResults = await findInviteCode(code);
if (!linkResults) return jsonRes(res, { code: 404, message: 'the link is not found' });
const queryResults = await prisma.userWorkspace.findMany({
where: {
workspaceUid: linkResults.workspaceUid,
userCrUid: {
in: [linkResults.inviterCrUid, payload.userCrUid]
}
},
include: {
workspace: true,
userCr: true
}
});
const inviteeStatus = queryResults.find((item) => item.userCrUid === payload.userCrUid);
if (inviteeStatus) return jsonRes(res, { code: 204, message: 'you are already in namespace' });
const inviterStatus = queryResults.find((r) => r.userCrUid === linkResults.inviterCrUid);
if (!inviterStatus || !vaildManage(roleToUserRole(inviterStatus.role))(linkResults.role, false))
return jsonRes(res, { code: 404, message: 'the inviter or the namespace is not found' });
const user = await globalPrisma.user.findUnique({
where: {
uid: inviterStatus.userCr.userUid
}
});
if (!user) throw Error('invalid user');
return jsonRes(res, {
code: 200,
data: {
workspace: inviterStatus.workspace.displayName,
role: linkResults.role,
inviterNickname: user.nickname
},
message: 'Successfully'
});
} catch (e) {
console.log(e);
jsonRes(res, { code: 500, message: 'invite member error' });
}
}
@@ -54,7 +54,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const own = queryResults.find((x) => x.userCrUid === payload.userCrUid);
if (!own) return jsonRes(res, { code: 403, message: 'you are not in the namespace' });
if (own.isPrivate) return jsonRes(res, { code: 403, message: 'the namespace is invalid' });
const vaild = vaildManage(roleToUserRole(own.role), own.userCrUid)(role, targetRegionUser.uid);
const vaild = vaildManage(roleToUserRole(own.role))(role, false);
if (!vaild) return jsonRes(res, { code: 403, message: 'you are not manager' });
if (queryResults.length === 0)
return jsonRes(res, { code: 404, message: 'there are not user in the namespace ' });
@@ -42,10 +42,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
const own = queryResults.find((x) => x.userCrUid === payload.userCrUid);
if (!own) return jsonRes(res, { code: 403, message: 'you are not in namespace' });
const vaildFn = vaildManage(roleToUserRole(own.role), own.userCrUid);
const vaildFn = vaildManage(roleToUserRole(own.role));
const targetUser = queryResults.find((x) => x.userCrUid === targetUserCrUid);
if (!targetUser) return jsonRes(res, { code: 404, message: 'target is not in namespace' });
if (!vaildFn(roleToUserRole(targetUser.role), targetUser.userCrUid))
if (!vaildFn(roleToUserRole(targetUser.role), targetUser.userCrUid === own.userCrUid))
return jsonRes(res, { code: 403, message: 'you are not manager' });
// 权限一致,不用管
@@ -6,12 +6,11 @@ import { validate } from 'uuid';
import { prisma } from '@/services/backend/db/init';
import { JoinStatus } from 'prisma/region/generated/client';
import { verifyAccessToken } from '@/services/backend/auth';
import { Role } from 'prisma/region/generated/client';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const payload = await verifyAccessToken(req.headers);
if (!payload) return jsonRes(res, { code: 401, message: 'token verify error' });
//
const { ns_uid, targetUserCrUid } = req.body as {
ns_uid?: string;
targetUserCrUid?: string;
@@ -22,9 +21,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
if (!targetUserCrUid || !validate(targetUserCrUid))
return jsonRes(res, { code: 400, message: 'tUserId is invalid' });
if (targetUserCrUid === payload.userCrUid) {
return jsonRes(res, { code: 403, message: 'target user must be others' });
}
const queryResults = await prisma.userWorkspace.findMany({
where: {
workspaceUid: ns_uid,
@@ -39,26 +35,29 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
});
const own = queryResults.find((x) => x.userCrUid === payload.userCrUid);
if (!own || own.status !== JoinStatus.IN_WORKSPACE)
return jsonRes(res, { code: 403, message: 'you are not in the namespace' });
if (targetUserCrUid === payload.userCrUid && own.role === Role.OWNER) {
return jsonRes(res, { code: 403, message: 'target user must be others' });
}
const tItem = queryResults.find((item) => item.userCr.uid === targetUserCrUid);
// 之前没绑上,
// for inviting state,
if (!tItem) return jsonRes(res, { code: 404, message: 'target user is not in namespace' });
const vaild = vaildManage(roleToUserRole(own.role), own.userCrUid)(
const vaild = vaildManage(roleToUserRole(own.role))(
roleToUserRole(tItem.role),
targetUserCrUid
tItem.userCrUid === payload.userCrUid
);
if (!vaild) return jsonRes(res, { code: 403, message: 'you are not manager' });
let unbinding_result = null;
if (JoinStatus.INVITED === tItem.status) {
// 等于取消邀请
// equal to cancel inviting
unbinding_result = await unbindingRole({
userCrUid: tItem.userCrUid,
workspaceUid: ns_uid
});
} else if (JoinStatus.IN_WORKSPACE === tItem.status) {
// 删除权限
// modify role
await modifyTeamRole({
k8s_username: tItem.userCr.crName,
role: roleToUserRole(tItem.role),
@@ -1,8 +1,6 @@
import { jsonRes } from '@/services/backend/response';
import { NextApiRequest, NextApiResponse } from 'next';
import { prisma } from '@/services/backend/db/init';
import { getUserKubeconfig } from '@/services/backend/kubernetes/admin';
import { switchKubeconfigNamespace } from '@/services/backend/kubernetes/user';
import { validate } from 'uuid';
import { JoinStatus } from 'prisma/region/generated/client';
import { generateAccessToken, generateAppToken, verifyAccessToken } from '@/services/backend/auth';
@@ -29,9 +27,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const newWorkspaceItem = queryResults.find((item) => item.workspace.uid === ns_uid);
if (!newWorkspaceItem)
return jsonRes(res, { code: 403, message: 'You are not in this workspace' });
const oldKcRaw = await getUserKubeconfig(payload.userCrUid, payload.userCrName);
if (!oldKcRaw) return jsonRes(res, { code: 404, message: 'The kubeconfig is not found' });
const kubeconfig = switchKubeconfigNamespace(oldKcRaw, newWorkspaceItem.workspace.id);
const jwtPayload = {
workspaceUid: newWorkspaceItem.workspaceUid,
workspaceId: newWorkspaceItem.workspace.id,
@@ -45,7 +40,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const appToken = generateAppToken(jwtPayload);
const data = {
token,
kubeconfig,
appToken
};
return jsonRes(res, {
@@ -56,7 +56,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
if (!unbindingResult) throw new Error('fail to unbinding');
}
jsonRes(res, {
code: 200,
message: 'Successfully'
@@ -0,0 +1,76 @@
import { reciveAction } from '@/api/namespace';
import { jsonRes } from '@/services/backend/response';
import { modifyTeamRole } from '@/services/backend/team';
import { NextApiRequest, NextApiResponse } from 'next';
import { prisma } from '@/services/backend/db/init';
import { UserRoleToRole } from '@/utils/tools';
import { JoinStatus } from 'prisma/region/generated/client';
import { verifyAccessToken } from '@/services/backend/auth';
import { findInviteCode } from '@/services/backend/db/workspaceInviteCode';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const payload = await verifyAccessToken(req.headers);
if (!payload) return jsonRes(res, { code: 401, message: 'token verify error' });
const { code, action } = req.body as { code?: string; action?: reciveAction };
if (!code) return jsonRes(res, { code: 400, message: 'code is invalid' });
if (action === undefined || ![reciveAction.Accepte, reciveAction.Reject].includes(action))
return jsonRes(res, {
code: 400,
message: `action must be ${reciveAction.Accepte}, ${reciveAction.Reject}`
});
if (action === reciveAction.Accepte) {
const linkResults = await findInviteCode(code);
if (!linkResults) return jsonRes(res, { code: 404, message: 'the link is not found' });
const queryResults = await prisma.userWorkspace.findMany({
where: {
workspaceUid: linkResults.workspaceUid,
userCrUid: {
in: [linkResults.inviterCrUid, payload.userCrUid]
},
status: JoinStatus.IN_WORKSPACE
},
include: {
workspace: true,
userCr: true
}
});
const inviteeStatus = queryResults.find((item) => item.userCrUid === payload.userCrUid);
if (inviteeStatus)
return jsonRes(res, { code: 409, message: 'you are already in namespace' });
const inviterStatus = queryResults.find((r) => r.userCrUid === linkResults.inviterCrUid);
if (!inviterStatus)
return jsonRes(res, { code: 404, message: 'the inviter or the namespace is not found' });
await modifyTeamRole({
k8s_username: payload.userCrName,
role: linkResults.role,
workspaceId: inviterStatus.workspace.id,
action: 'Grant'
});
const result = await prisma.userWorkspace.create({
data: {
status: JoinStatus.IN_WORKSPACE,
role: UserRoleToRole(linkResults.role),
isPrivate: false,
workspaceUid: linkResults.workspaceUid,
userCrUid: payload.userCrUid,
joinAt: new Date(),
handlerUid: linkResults.inviterUid
}
});
if (!result) throw new Error('failed to change Status');
}
return jsonRes(res, {
code: 200,
message: 'Successfully'
});
} catch (e) {
console.log(e);
return jsonRes(res, { code: 500, message: 'get price error' });
}
}
@@ -1,10 +1,12 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { K8sApi, ListCRD, switchKubeconfigNamespace } from '@/services/backend/kubernetes/user';
import { K8sApi, ListCRD } from '@/services/backend/kubernetes/user';
import { jsonRes } from '@/services/backend/response';
import { CRDMeta, TAppCRList, TAppConfig } from '@/types';
import { getUserKubeconfigNotPatch } from '@/services/backend/kubernetes/admin';
import { verifyAccessToken } from '@/services/backend/auth';
import { switchKubeconfigNamespace } from '@/utils/switchKubeconfigNamespace';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const payload = await verifyAccessToken(req.headers);
@@ -1,13 +1,10 @@
import {
CRDMeta,
K8sApi,
ListCRD,
switchKubeconfigNamespace
} from '@/services/backend/kubernetes/user';
import { CRDMeta, K8sApi, ListCRD } from '@/services/backend/kubernetes/user';
import { jsonRes } from '@/services/backend/response';
import type { NextApiRequest, NextApiResponse } from 'next';
import { verifyAccessToken } from '@/services/backend/auth';
import { getUserKubeconfigNotPatch } from '@/services/backend/kubernetes/admin';
import { switchKubeconfigNamespace } from '@/utils/switchKubeconfigNamespace';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const payload = await verifyAccessToken(req.headers);
@@ -1,14 +1,11 @@
import { verifyAccessToken } from '@/services/backend/auth';
import {
CRDMeta,
K8sApi,
switchKubeconfigNamespace,
UpdateCRD
} from '@/services/backend/kubernetes/user';
import { CRDMeta, K8sApi, UpdateCRD } from '@/services/backend/kubernetes/user';
import { jsonRes } from '@/services/backend/response';
import type { NextApiRequest, NextApiResponse } from 'next';
import * as k8s from '@kubernetes/client-node';
import { getUserKubeconfigNotPatch } from '@/services/backend/kubernetes/admin';
import { switchKubeconfigNamespace } from '@/utils/switchKubeconfigNamespace';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { name } = req.body;
+10 -1
View File
@@ -13,6 +13,7 @@ import Head from 'next/head';
import { useRouter } from 'next/router';
import Script from 'next/script';
import { createContext, useEffect, useState } from 'react';
import useCallbackStore from '@/stores/callback';
const destination = '/signin';
interface IMoreAppsContext {
@@ -27,12 +28,13 @@ export default function Home({ sealos_cloud_domain }: { sealos_cloud_domain: str
const init = useAppStore((state) => state.init);
const setAutoLaunch = useAppStore((state) => state.setAutoLaunch);
const { systemConfig } = useSystemConfigStore();
const { workspaceInviteCode, setWorkspaceInviteCode } = useCallbackStore();
useEffect(() => {
colorMode === 'dark' ? toggleColorMode() : null;
}, [colorMode, toggleColorMode]);
const [showMoreApps, setShowMoreApps] = useState(false);
// openApp by query
useEffect(() => {
const { query } = router;
const is_login = isUserLogin();
@@ -87,6 +89,13 @@ export default function Home({ sealos_cloud_domain }: { sealos_cloud_domain: str
}
}, []);
// handle workspaceInvite
useEffect(() => {
if (workspaceInviteCode) {
router.replace('/WorkspaceInvite?code=' + workspaceInviteCode);
return;
}
}, [workspaceInviteCode]);
return (
<Box position={'relative'} overflow={'hidden'} w="100vw" h="100vh">
<Head>
@@ -29,8 +29,6 @@ export default function Callback() {
const oauthProvider = router.query.oauthProxyProvider;
// nextjs auto decode
let oauthClientId = router.query.oauthProxyClientID;
// const abortController = new AbortController()
console.log(oauthProvider, oauthProxyState, oauthClientId);
if (!isString(oauthProxyState) || !isString(oauthProvider) || !isString(oauthClientId)) return;
console.log(oauthProvider, oauthProxyState, oauthClientId, callback_url);
@@ -42,9 +40,7 @@ export default function Callback() {
).json()) as ApiResp<{ containDomain: boolean }>;
console.log(result);
if (!result.data?.containDomain) return;
console.log('generate');
const state = generateState(oauthProxyState);
console.log(callback_url, 'callback');
if (oauthProvider === 'github') {
await oauthLogin({
+9 -5
View File
@@ -3,28 +3,32 @@ import { useRouter } from 'next/router';
import { useEffect } from 'react';
import useSessionStore from '@/stores/session';
import { Flex, Spinner } from '@chakra-ui/react';
import { uploadConvertData } from '@/api/platform';
import { getRegionToken, UserInfo } from '@/api/auth';
import { getRegionToken } from '@/api/auth';
import { isString } from 'lodash';
import { jwtDecode } from 'jwt-decode';
import { AccessTokenPayload } from '@/types/token';
import { sessionConfig } from '@/utils/sessionConfig';
import { useMutation } from '@tanstack/react-query';
import { nsListRequest, switchRequest } from '@/api/namespace';
import { switchKubeconfigNamespace } from '@/utils/switchKubeconfigNamespace';
const Callback: NextPage = () => {
const router = useRouter();
const setSession = useSessionStore((s) => s.setSession);
const setToken = useSessionStore((s) => s.setToken);
const delSession = useSessionStore((s) => s.delSession);
const curToken = useSessionStore((s) => s.token);
const { token: curToken, session } = useSessionStore((s) => s);
const { lastWorkSpaceId } = useSessionStore();
const mutation = useMutation({
mutationFn: switchRequest,
async onSuccess(data) {
if (data.code === 200 && !!data.data) {
await sessionConfig(data.data);
if (data.code === 200 && !!data.data && session) {
const payload = jwtDecode<AccessTokenPayload>(data.data.token);
await sessionConfig({
...data.data,
kubeconfig: switchKubeconfigNamespace(session.kubeconfig, payload.workspaceId)
});
} else {
throw Error('session in invalid');
}
@@ -0,0 +1,71 @@
import { connectToDatabase } from './mongodb';
import { UserRole } from '@/types/team';
type TWorkspace_invite_link = {
role: UserRole;
code: string; // unique
workspaceUid: string;
createdAt: Date;
inviterUid: string;
inviterCrUid: string;
};
async function connectToUserCollection() {
const client = await connectToDatabase();
const collection = client.db().collection<TWorkspace_invite_link>('workspace_invite_links');
await collection.createIndex({ createdAt: 1 }, { expireAfterSeconds: 60 * 30 });
await collection.createIndex({ code: 1 }, { unique: true });
return collection;
}
export async function addOrUpdateInviteCode({
code,
inviterUid,
role,
workspaceUid,
inviterCrUid
}: Omit<TWorkspace_invite_link, 'createdAt'>) {
const codes = await connectToUserCollection();
const result = await codes.updateOne(
{
inviterCrUid,
inviterUid,
role,
workspaceUid
},
{
$set: {
code,
createdAt: new Date()
}
},
{
upsert: true
}
);
return result;
}
export async function getInviteCode({
inviterUid,
role,
workspaceUid,
inviterCrUid
}: Omit<TWorkspace_invite_link, 'createdAt' | 'code'>) {
const codes = await connectToUserCollection();
const result = await codes.findOne({
inviterUid,
inviterCrUid,
workspaceUid,
role
});
return result;
}
export async function findInviteCode(code: string) {
const codes = await connectToUserCollection();
const result = await codes.findOne({
code
});
return result;
}
@@ -1,12 +1,7 @@
import * as k8s from '@kubernetes/client-node';
import http from 'http';
import * as yaml from 'js-yaml';
export function switchKubeconfigNamespace(kc: string, namespace: string) {
const oldKc = yaml.load(kc);
// @ts-ignore
oldKc.contexts[0].context.namespace = namespace;
return yaml.dump(oldKc);
}
export function K8sApi(config: string): k8s.KubeConfig {
const kc = new k8s.KubeConfig();
kc.loadFromString(config);
@@ -11,8 +11,8 @@ import { K8sApiDefault } from './kubernetes/admin';
import { ApplyYaml } from './kubernetes/user';
import { prisma } from '@/services/backend/db/init';
import { JoinStatus, Role } from 'prisma/region/generated/client';
import { UserRoleToRole } from '@/utils/tools';
const UserRoleToRole = (role: UserRole): Role => [Role.OWNER, Role.MANAGER, Role.DEVELOPER][role];
const _applyRoleRequest =
(kc: KubeConfig, nsid: string) =>
(action: 'Grant' | 'Deprive' | 'Update', types: watchEventType[]) =>
@@ -192,7 +192,7 @@ export const modifyBinding = async ({
});
};
// 拒绝邀请 === 解绑
// reject Invitation
export const unbindingRole = async ({
userCrUid,
workspaceUid
@@ -209,7 +209,7 @@ export const unbindingRole = async ({
}
});
};
// 接受邀请
// accept Invitation
export const acceptInvite = async ({
userCrUid,
workspaceUid
+9 -2
View File
@@ -6,6 +6,7 @@ import { immer } from 'zustand/middleware/immer';
import AppStateManager from '../utils/ProcessManager';
import { formatUrl } from '@/utils/format';
import { minBy, cloneDeep } from 'lodash';
export class AppInfo {
pid: number;
isShow: boolean;
@@ -33,6 +34,7 @@ export class AppInfo {
};
displayType: displayType;
i18n?: any;
constructor(app: TApp, pid: number) {
this.isShow = false;
this.zIndex = 1;
@@ -66,7 +68,7 @@ const useAppStore = create<TOSState>()(
launchQuery: {},
autolaunch: '',
runner: new AppStateManager([]),
init: async () => {
async init() {
const res = await request('/api/desktop/getInstalledApps');
set((state) => {
state.installedApps = res?.data?.map((app: TApp) => new AppInfo(app, -1));
@@ -83,7 +85,12 @@ const useAppStore = create<TOSState>()(
state.runningInfo = state.runningInfo.filter((item) => item.pid !== pid);
});
},
closeAppAll: () => {
set((state) => {
state.runner.closeAppAll();
state.runningInfo = [];
});
},
installApp: (app: TApp) => {
set((state) => {
state.installedApps.push(new AppInfo(app, -1));
+24
View File
@@ -0,0 +1,24 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
type CallbackState = {
workspaceInviteCode?: string;
setWorkspaceInviteCode: (id?: string) => void;
};
const useCallbackStore = create<CallbackState>()(
persist(
immer((set, get) => ({
workspaceInviteCode: undefined,
setWorkspaceInviteCode: (id?: string) => {
set((state) => ({ workspaceInviteCode: id }));
}
})),
{
name: 'callbackParam'
}
)
);
export default useCallbackStore;
+11 -2
View File
@@ -1,5 +1,6 @@
import { defineStyleConfig, extendTheme } from '@chakra-ui/react';
import { theme as originTheme } from '@sealos/ui';
const Button = defineStyleConfig({
baseStyle: {
borderRadius: '4px'
@@ -41,12 +42,20 @@ const Select = defineStyleConfig({
variant: 'filled'
}
});
export const Modal = defineStyleConfig({
baseStyle: {
header: {
fontWeight: 600,
fontSize: '20px'
}
}
});
export const theme = extendTheme(originTheme, {
initialColorMode: 'light', // 'dark | 'light'
components: {
Button,
Input,
Select
Select,
Modal
}
});
+1
View File
@@ -77,6 +77,7 @@ export type TOSState = {
): Promise<void>;
// close app
closeAppById: (pid: number) => void;
closeAppAll: () => void;
// get current runningApp
currentApp: () => AppInfo | undefined;
switchAppById: (pid: number) => void;
@@ -31,6 +31,9 @@ export default class AppStateManager {
const idx = this.openedApps.findIndex((app) => app.pid === pid);
this.openedApps.splice(idx, 1);
}
closeAppAll() {
this.openedApps = [];
}
loadApps(appKeys: string[]) {
this.allApps = new Set(appKeys);
}
@@ -0,0 +1,8 @@
import * as yaml from 'js-yaml';
export function switchKubeconfigNamespace(kc: string, namespace: string) {
const oldKc = yaml.load(kc);
// @ts-ignore
oldKc.contexts[0].context.namespace = namespace;
return yaml.dump(oldKc);
}
+15 -16
View File
@@ -66,24 +66,23 @@ export const retrySerially = <T>(fn: () => Promise<T>, times: number) =>
};
attempt();
});
// 自己的权限能管理什么权限
export const vaildManage =
(ownRole: UserRole, userId: string) => (targetRole: UserRole, tUserId: string) => {
if (targetRole === UserRole.Owner)
return [UserRole.Owner].includes(ownRole); // 不论目标是不是自己,都必须要最高权限
else if (targetRole === UserRole.Manager)
return [UserRole.Owner, ...(tUserId === userId ? [UserRole.Manager] : [])].includes(ownRole);
//对自己操作定义的role可以是平级
else if (targetRole === UserRole.Developer)
return [
UserRole.Owner,
UserRole.Manager,
...(tUserId === userId ? [UserRole.Developer] : [])
].includes(ownRole);
else return false;
};
// search manager relation
export const vaildManage = (ownRole: UserRole) => (targetRole: UserRole, isSelf: boolean) => {
if (targetRole === UserRole.Owner) return [UserRole.Owner].includes(ownRole); // only owner
else if (targetRole === UserRole.Manager)
//
return [UserRole.Owner, ...(isSelf ? [UserRole.Manager] : [])].includes(ownRole);
// manager via self
else if (targetRole === UserRole.Developer)
return [UserRole.Owner, UserRole.Manager, ...(isSelf ? [UserRole.Developer] : [])].includes(
ownRole
);
else return false;
};
export const isUserRole = (role: any): role is UserRole =>
[UserRole.Developer, UserRole.Manager, UserRole.Owner].includes(role);
export const UserRoleToRole = (role: UserRole): Role =>
[Role.OWNER, Role.MANAGER, Role.DEVELOPER][role];
export const roleToUserRole = (role: Role): UserRole => {
const map: Record<Role, UserRole> = {
[Role.OWNER]: UserRole.Owner,
+1 -6
View File
@@ -1,11 +1,6 @@
import { v4 } from 'uuid';
import { API_NAME } from './constants';
import {
AppMessageType,
AppSendMessageType,
MasterReplyMessageType,
SessionV1
} from './types';
import { AppMessageType, AppSendMessageType, MasterReplyMessageType, SessionV1 } from './types';
import { isBrowser } from './utils';
class ClientSDK {
@@ -1,6 +1,6 @@
/** @type {import('next').NextConfig} */
const { i18n } = require('./next-i18next.config')
const path = require('path')
const { i18n } = require('./next-i18next.config');
const path = require('path');
const nextConfig = {
i18n,
output: 'standalone',
@@ -13,14 +13,14 @@ const nextConfig = {
issuer: /\.[jt]sx?$/,
use: ['@svgr/webpack']
}
])
config.plugins = [...config.plugins]
return config
]);
config.plugins = [...config.plugins];
return config;
},
transpilePackages: ['@sealos/driver', '@sealos/ui'],
experimental: {
outputFileTracingRoot: path.join(__dirname, '../../')
}
}
};
module.exports = nextConfig
module.exports = nextConfig;
+4 -4
View File
@@ -1,6 +1,6 @@
/** @type {import('next').NextConfig} */
const { i18n } = require('./next-i18next.config')
const path = require('path')
const { i18n } = require('./next-i18next.config');
const path = require('path');
const nextConfig = {
i18n,
@@ -12,6 +12,6 @@ const nextConfig = {
// this includes files from the monorepo base two directories up
outputFileTracingRoot: path.join(__dirname, '../../')
}
}
};
module.exports = nextConfig
module.exports = nextConfig;
+10 -10
View File
@@ -1,10 +1,10 @@
/** @type {import('next').NextConfig} */
const { i18n } = require('./next-i18next.config')
const path = require('path')
const { i18n } = require('./next-i18next.config');
const path = require('path');
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
})
enabled: process.env.ANALYZE === 'true'
});
const nextConfig = {
i18n,
@@ -18,9 +18,9 @@ const nextConfig = {
issuer: /\.[jt]sx?$/,
use: ['@svgr/webpack']
}
])
config.plugins = [...config.plugins]
return config
]);
config.plugins = [...config.plugins];
return config;
},
experimental: {
// this includes files from the monorepo base two directories up
@@ -32,9 +32,9 @@ const nextConfig = {
protocol: 'https',
hostname: '**'
}
],
]
},
transpilePackages: ['@sealos/ui', 'sealos-desktop-sdk']
}
};
module.exports = withBundleAnalyzer(nextConfig)
module.exports = withBundleAnalyzer(nextConfig);