mirror of
https://github.com/labring/sealos.git
synced 2026-09-24 15:46:19 +08:00
feat(desktop):add inviting others into workspace by link (#4712)
* feat(desktop):link inviting * style(desktop): update style
This commit is contained in:
@@ -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) => {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user