Browse Source

feat(gui): update ui and integrate api

Signed-off-by: Pranav C <pranavxc@gmail.com>
pull/4134/head
Pranav C 2 years ago
parent
commit
13d1dbaac9
  1. 250
      packages/nc-gui/components/org-user/index.vue
  2. 2
      packages/nc-gui/plugins/tele.ts
  3. 10
      packages/nocodb/src/lib/meta/api/orgUserApis.ts
  4. 114
      packages/nocodb/src/lib/models/User.ts
  5. 3
      scripts/sdk/swagger.json

250
packages/nc-gui/components/org-user/index.vue

@ -1,70 +1,200 @@
<script lang="ts" setup>
import { Role, useNuxtApp } from '#imports'
import { message, Modal } from 'ant-design-vue'
import type { RequestParams } from 'nocodb-sdk'
import type { User } from '#imports'
import { Role, extractSdkResponseErrorMsg, useNuxtApp } from '#imports'
import { useApi } from '~/composables/useApi'
const { $api } = useNuxtApp()
const { api, isLoading } = useApi()
const { list: users, pageInfo } = await $api.orgUsers.orgUsersList()
let users = $ref<null | User[]>(null)
let totalRows = $ref(0)
const currentPage = $ref(1)
const currentLimit = $ref(10)
const searchText = ref<string>('')
const pagination = reactive({
total: 0,
pageSize: 10,
})
const loadUsers = async (page = currentPage, limit = currentLimit) => {
try {
const response: any = await api.orgUsers.list({
query: {
limit,
offset: searchText.value.length === 0 ? (page - 1) * limit : 0,
query: searchText.value,
},
} as RequestParams)
if (!response) return
pagination.total = response.pageInfo.totalRows ?? 0
pagination.pageSize = 10
users = response.list as User[]
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
loadUsers()
const updateRole = async (userId: string, roles: Role) => {
try {
await api.orgUsers.update(userId, {
roles,
} as unknown as User)
message.success('Role updated successfully')
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
const deleteUser = async (userId: string) => {
Modal.confirm({
title: 'Are you sure you want to delete this user?',
onOk: async () => {
try {
await api.orgUsers.delete(userId)
message.success('User deleted successfully')
await loadUsers()
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
},
})
}
</script>
<template>
<div class="max-w-[700px] mx-auto p-4">
<div class="px-5">
<div class="flex flex-row border-b-1 pb-2 px-2">
<div class="flex flex-row w-4/6 space-x-1 items-center pl-1">
<EvaEmailOutline class="flex text-gray-500 -mt-0.5" />
<div class="text-gray-600 text-xs space-x-1">{{ $t('labels.email') }}</div>
</div>
<div class="flex flex-row w-4/6 space-x-1 items-center pl-1">
<EvaEmailOutline class="flex text-gray-500 -mt-0.5" />
<div class="text-gray-600 text-xs space-x-1">{{ $t('object.projects') }}</div>
</div>
<div class="flex flex-row justify-center w-1/6 space-x-1 items-center pl-1">
<MdiDramaMasks class="flex text-gray-500 -mt-0.5" />
<div class="text-gray-600 text-xs">{{ $t('objects.role') }}</div>
</div>
<div class="flex flex-row w-1/6 justify-end items-center pl-1">
<div class="text-gray-600 text-xs">{{ $t('labels.actions') }}</div>
</div>
</div>
<div v-for="(user, index) of users" :key="index" class="flex flex-row items-center border-b-1 py-2 px-2 nc-user-row">
<div class="flex w-4/6 flex-wrap nc-user-email">
{{ user.email }}
</div>
<div class="flex w-1/6 justify-center flex-wrap ml-4">
{{ user.projectsCount }}
</div>
<div class="flex w-1/6 justify-center flex-wrap ml-4">
<!-- <div v-if="user.roles" class="rounded-full px-2 py-1 nc-user-role">
{{ $t(`objects.roleType.${user.roles.split(',')[0].replace(/-(\w)/g, (_, m1) => m1.toUpperCase())}`) }}
</div>-->
<a-select
class="min-w-[220px]"
:options="[
{ value: Role.OrgLevelCreator, label: $t(`objects.roleType.orgLevelCreator`) },
{ value: Role.OrgLevelViewer, label: $t(`objects.roleType.orgLevelViewer`) },
]"
>
</a-select>
</div>
<div class="flex w-1/6 flex-wrap justify-end">
<MdiDeleteOutline />
</div>
</div>
<a-pagination
v-model:current="currentPage"
hide-on-single-page
class="mt-4"
:page-size="currentLimit"
:total="totalRows"
show-less-items
<div class=" h-full overflow-y-scroll scrollbar-thin-dull">
<div class="max-w-[700px] mx-auto p-4">
<a-input-search size="small" class="my-4 max-w-[300px]" placeholder="Filter by email" v-model:value="searchText"
@change="loadUsers" @keydown.enter="loadUsers"></a-input-search>
<a-table
:row-key="(record) => record.id"
:data-source="users"
:pagination="pagination"
:loading="isLoading"
@change="loadUsers"
/>
>
<template #emptyText>
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('labels.noData')" />
</template>
<!-- Email -->
<a-table-column key="email" :title="$t('labels.email')" data-index="email">
<template #default="{ text }">
<div>
{{ text }}
</div>
</template>
</a-table-column>
<!-- Role -->
<a-table-column key="roles" :title="$t('objects.role')" data-index="roles">
<template #default="{ record }">
<div>
<a-select
v-model:value="record.roles"
class="min-w-[220px]"
:options="[
{ value: Role.OrgLevelCreator, label: $t(`objects.roleType.orgLevelCreator`) },
{ value: Role.OrgLevelViewer, label: $t(`objects.roleType.orgLevelViewer`) },
]"
@change="updateRole(record.id, record.roles)"
>
</a-select>
</div>
</template>
</a-table-column>
<!-- Projects -->
<a-table-column key="projectsCount" :title="$t('objects.projects')" data-index="projectsCount">
<template #default="{ text }">
<div>
{{ text }}
</div>
</template>
</a-table-column>
<!-- Actions -->
<a-table-column key="id" :title="$t('labels.actions')" data-index="id">
<template #default="{ text }">
<div class="flex items-center gap-2">
<MdiDeleteOutline class="nc-action-btn cursor-pointer" @click="deleteUser(text)" />
</div>
</template>
</a-table-column>
</a-table>
<!-- <div class="py-4"> -->
<!-- <a-input-search class="mx-w-[300px]" v-model="searchText" @search="loadUsers" /> -->
<!-- </div> -->
<!-- <div class="px-5">
<div class="flex flex-row border-b-1 pb-2 px-2">
<div class="flex flex-row w-4/6 space-x-1 items-center pl-1">
<EvaEmailOutline class="flex text-gray-500 -mt-0.5" />
<div class="text-gray-600 text-xs space-x-1">{{ $t('labels.email') }}</div>
</div>
<div class="flex flex-row w-4/6 space-x-1 items-center pl-1">
<EvaEmailOutline class="flex text-gray-500 -mt-0.5" />
<div class="text-gray-600 text-xs space-x-1">{{ $t('object.projects') }}</div>
</div>
<div class="flex flex-row justify-center w-1/6 space-x-1 items-center pl-1">
<MdiDramaMasks class="flex text-gray-500 -mt-0.5" />
<div class="text-gray-600 text-xs">{{ $t('objects.role') }}</div>
</div>
<div class="flex flex-row w-1/6 justify-end items-center pl-1">
<div class="text-gray-600 text-xs">{{ $t('labels.actions') }}</div>
</div>
</div>
<div v-for="(user, index) of users" :key="index" class="flex flex-row items-center border-b-1 py-2 px-2 nc-user-row">
<div class="flex w-4/6 flex-wrap nc-user-email">
{{ user.email }}
</div>
<div class="flex w-1/6 justify-center flex-wrap ml-4">
{{ user.projectsCount }}
</div>
<div class="flex w-1/6 justify-center flex-wrap ml-4">
&lt;!&ndash; <div v-if="user.roles" class="rounded-full px-2 py-1 nc-user-role">
{{ $t(`objects.roleType.${user.roles.split(',')[0].replace(/-(\w)/g, (_, m1) => m1.toUpperCase())}`) }}
</div>&ndash;&gt;
<a-select
class="min-w-[220px]"
:options="[
{ value: Role.OrgLevelCreator, label: $t(`objects.roleType.orgLevelCreator`) },
{ value: Role.OrgLevelViewer, label: $t(`objects.roleType.orgLevelViewer`) },
]"
>
</a-select>
</div>
<div class="flex w-1/6 flex-wrap justify-end">
<MdiDeleteOutline />
</div>
</div>
<a-pagination
v-model:current="currentPage"
hide-on-single-page
class="mt-4"
:page-size="currentLimit"
:total="totalRows"
show-less-items
@change="loadUsers"
/>
</div> -->
</div>
</div>
</template>

2
packages/nc-gui/plugins/tele.ts

@ -37,6 +37,7 @@ export default defineNuxtPlugin(async (nuxtApp) => {
socket.emit('page', {
path: to.matched[0].path + (to.query && to.query.type ? `?type=${to.query.type}` : ''),
pid: route?.params?.projectId,
})
})
@ -48,6 +49,7 @@ export default defineNuxtPlugin(async (nuxtApp) => {
event: evt,
...(data || {}),
path: route?.matched?.[0]?.path,
pid: route?.params?.projectId,
})
}
},

10
packages/nocodb/src/lib/meta/api/orgUserApis.ts

@ -22,7 +22,7 @@ async function userUpdate(req, res) {
const user = await User.get(req.params.userId);
if (user.roles.includes(OrgUserRoles.SUPER)) {
throw new Error('Cannot update super admin roles');
NcError.badRequest('Cannot update super admin roles');
}
res.json(await User.update(req.params.userId, updteBody));
@ -32,7 +32,7 @@ async function userDelete(req, res) {
const user = await User.get(req.params.userId);
if (user.roles.includes(OrgUserRoles.SUPER)) {
throw new Error('Cannot delete super admin');
NcError.badRequest('Cannot delete super admin');
}
res.json(await User.delete(req.params.userId));
@ -49,17 +49,17 @@ router.get(
ncMetaAclMw(userList, 'userList', [OrgUserRoles.SUPER])
);
router.patch(
'/api/v1/db/meta/users/:userId',
'/api/v1/users/:userId',
metaApiMetrics,
ncMetaAclMw(userUpdate, 'userUpdate', [OrgUserRoles.SUPER])
);
router.delete(
'/api/v1/db/meta/users/:userId',
'/api/v1/users/:userId',
metaApiMetrics,
ncMetaAclMw(userAdd, 'userAdd', [OrgUserRoles.SUPER])
);
router.post(
'/api/v1/db/meta/users/:userId',
'/api/v1/users/:userId',
metaApiMetrics,
ncMetaAclMw(userDelete, 'userDelete', [OrgUserRoles.SUPER])
);

114
packages/nocodb/src/lib/models/User.ts

@ -6,28 +6,28 @@ import { extractProps } from '../meta/helpers/extractProps';
import NocoCache from '../cache/NocoCache';
import { NcError } from '../meta/helpers/catchError';
export default class User implements UserType {
id: string;
id: string
/** @format email */
email: string;
password?: string;
salt?: string;
firstname: string;
lastname: string;
username?: string;
refresh_token?: string;
invite_token?: string;
invite_token_expires?: number | Date;
reset_password_expires?: number | Date;
reset_password_token?: string;
email_verification_token?: string;
email_verified: boolean;
roles?: string;
token_version?: string;
email: string
password?: string
salt?: string
firstname: string
lastname: string
username?: string
refresh_token?: string
invite_token?: string
invite_token_expires?: number | Date
reset_password_expires?: number | Date
reset_password_token?: string
email_verification_token?: string
email_verified: boolean
roles?: string
token_version?: string
constructor(data: User) {
Object.assign(this, data);
Object.assign(this, data)
}
public static async insert(user: Partial<User>, ncMeta = Noco.ncMeta) {
@ -48,22 +48,22 @@ export default class User implements UserType {
'email_verified',
'roles',
'token_version',
]);
])
if (insertObj.email) {
insertObj.email = insertObj.email.toLowerCase();
insertObj.email = insertObj.email.toLowerCase()
}
const { id } = await ncMeta.metaInsert2(
null,
null,
MetaTable.USERS,
insertObj
);
insertObj,
)
await NocoCache.del(CacheScope.INSTANCE_META);
await NocoCache.del(CacheScope.INSTANCE_META)
return this.get(id, ncMeta);
return this.get(id, ncMeta)
}
public static async update(id, user: Partial<User>, ncMeta = Noco.ncMeta) {
const updateObj = extractProps(user, [
@ -82,13 +82,13 @@ export default class User implements UserType {
'email_verified',
'roles',
'token_version',
]);
])
if (updateObj.email) {
updateObj.email = updateObj.email.toLowerCase();
updateObj.email = updateObj.email.toLowerCase()
} else {
// set email prop to avoid generation of invalid cache key
updateObj.email = (await this.get(id, ncMeta))?.email?.toLowerCase();
updateObj.email = (await this.get(id, ncMeta))?.email?.toLowerCase()
}
// get existing cache
const keys = [
@ -96,43 +96,43 @@ export default class User implements UserType {
`${CacheScope.USER}:${id}`,
// update user:<email>
`${CacheScope.USER}:${user.email}`,
];
]
for (const key of keys) {
let o = await NocoCache.get(key, CacheGetType.TYPE_OBJECT);
let o = await NocoCache.get(key, CacheGetType.TYPE_OBJECT)
if (o) {
o = { ...o, ...updateObj };
o = { ...o, ...updateObj }
// set cache
await NocoCache.set(key, o);
await NocoCache.set(key, o)
}
}
// as <projectId> is unknown, delete user:<email>___<projectId> in cache
await NocoCache.delAll(CacheScope.USER, `${user.email}___*`);
await NocoCache.delAll(CacheScope.USER, `${user.email}___*`)
// set meta
return await ncMeta.metaUpdate(null, null, MetaTable.USERS, updateObj, id);
return await ncMeta.metaUpdate(null, null, MetaTable.USERS, updateObj, id)
}
public static async getByEmail(_email: string, ncMeta = Noco.ncMeta) {
const email = _email?.toLowerCase();
const email = _email?.toLowerCase()
let user =
email &&
(await NocoCache.get(
`${CacheScope.USER}:${email}`,
CacheGetType.TYPE_OBJECT
));
CacheGetType.TYPE_OBJECT,
))
if (!user) {
user = await ncMeta.metaGet2(null, null, MetaTable.USERS, {
email,
});
await NocoCache.set(`${CacheScope.USER}:${email}`, user);
})
await NocoCache.set(`${CacheScope.USER}:${email}`, user)
}
return user;
return user
}
static async isFirst(ncMeta = Noco.ncMeta) {
const isFirst = !(await NocoCache.getAll(`${CacheScope.USER}:*`))?.length;
const isFirst = !(await NocoCache.getAll(`${CacheScope.USER}:*`))?.length
if (isFirst)
return !(await ncMeta.metaGet2(null, null, MetaTable.USERS, {}));
return false;
return !(await ncMeta.metaGet2(null, null, MetaTable.USERS, {}))
return false
}
public static async count(
@ -141,15 +141,15 @@ export default class User implements UserType {
}: {
query?: string;
} = {},
ncMeta = Noco.ncMeta
ncMeta = Noco.ncMeta,
): Promise<number> {
const qb = ncMeta.knex(MetaTable.USERS);
const qb = ncMeta.knex(MetaTable.USERS)
if (query) {
qb.where('email', 'like', `%${query.toLowerCase?.()}%`);
qb.where('email', 'like', `%${query.toLowerCase?.()}%`)
}
return (await qb.count('id', { as: 'count' }).first()).count;
return (await qb.count('id', { as: 'count' }).first()).count
}
static async get(userId, ncMeta = Noco.ncMeta): Promise<UserType> {
@ -157,20 +157,20 @@ export default class User implements UserType {
userId &&
(await NocoCache.get(
`${CacheScope.USER}:${userId}`,
CacheGetType.TYPE_OBJECT
));
CacheGetType.TYPE_OBJECT,
))
if (!user) {
user = await ncMeta.metaGet2(null, null, MetaTable.USERS, userId);
await NocoCache.set(`${CacheScope.USER}:${userId}`, user);
user = await ncMeta.metaGet2(null, null, MetaTable.USERS, userId)
await NocoCache.set(`${CacheScope.USER}:${userId}`, user)
}
return user;
return user
}
static async getByRefreshToken(refresh_token, ncMeta = Noco.ncMeta) {
const user = await ncMeta.metaGet2(null, null, MetaTable.USERS, {
refresh_token,
});
return user;
})
return user
}
public static async list(
@ -183,7 +183,7 @@ export default class User implements UserType {
offset?: number | undefined;
query?: string;
} = {},
ncMeta = Noco.ncMeta
ncMeta = Noco.ncMeta,
) {
let queryBuilder = ncMeta.knex(MetaTable.USERS);
@ -214,7 +214,7 @@ export default class User implements UserType {
.as('projectsCount')
);
if (query) {
queryBuilder.where('email', 'like', `%${query.toLowerCase?.()}%`);
queryBuilder.where('email', 'like', `%${query.toLowerCase?.()}%`)
}
return queryBuilder;
@ -226,8 +226,4 @@ export default class User implements UserType {
await NocoCache.del(`${CacheScope.USER}:${userId}`);
await ncMeta.metaDelete(null, null, MetaTable.USERS, userId);
}
static async delete(_userId: string) {
NcError.notImplemented();
}
}

3
scripts/sdk/swagger.json

@ -6407,8 +6407,7 @@
"format": "email"
},
"roles": {
"type": "string",
"format": "email"
"type": "string"
},
"date_of_birth": {
"type": "string",

Loading…
Cancel
Save