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> <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> </script>
<template> <template>
<div class="max-w-[700px] mx-auto p-4"> <div class=" h-full overflow-y-scroll scrollbar-thin-dull">
<div class="px-5"> <div class="max-w-[700px] mx-auto p-4">
<div class="flex flex-row border-b-1 pb-2 px-2"> <a-input-search size="small" class="my-4 max-w-[300px]" placeholder="Filter by email" v-model:value="searchText"
<div class="flex flex-row w-4/6 space-x-1 items-center pl-1"> @change="loadUsers" @keydown.enter="loadUsers"></a-input-search>
<EvaEmailOutline class="flex text-gray-500 -mt-0.5" /> <a-table
:row-key="(record) => record.id"
<div class="text-gray-600 text-xs space-x-1">{{ $t('labels.email') }}</div> :data-source="users"
</div> :pagination="pagination"
<div class="flex flex-row w-4/6 space-x-1 items-center pl-1"> :loading="isLoading"
<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
@change="loadUsers" @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>
</div> </div>
</template> </template>

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

@ -37,6 +37,7 @@ export default defineNuxtPlugin(async (nuxtApp) => {
socket.emit('page', { socket.emit('page', {
path: to.matched[0].path + (to.query && to.query.type ? `?type=${to.query.type}` : ''), 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, event: evt,
...(data || {}), ...(data || {}),
path: route?.matched?.[0]?.path, 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); const user = await User.get(req.params.userId);
if (user.roles.includes(OrgUserRoles.SUPER)) { 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)); 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); const user = await User.get(req.params.userId);
if (user.roles.includes(OrgUserRoles.SUPER)) { 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)); res.json(await User.delete(req.params.userId));
@ -49,17 +49,17 @@ router.get(
ncMetaAclMw(userList, 'userList', [OrgUserRoles.SUPER]) ncMetaAclMw(userList, 'userList', [OrgUserRoles.SUPER])
); );
router.patch( router.patch(
'/api/v1/db/meta/users/:userId', '/api/v1/users/:userId',
metaApiMetrics, metaApiMetrics,
ncMetaAclMw(userUpdate, 'userUpdate', [OrgUserRoles.SUPER]) ncMetaAclMw(userUpdate, 'userUpdate', [OrgUserRoles.SUPER])
); );
router.delete( router.delete(
'/api/v1/db/meta/users/:userId', '/api/v1/users/:userId',
metaApiMetrics, metaApiMetrics,
ncMetaAclMw(userAdd, 'userAdd', [OrgUserRoles.SUPER]) ncMetaAclMw(userAdd, 'userAdd', [OrgUserRoles.SUPER])
); );
router.post( router.post(
'/api/v1/db/meta/users/:userId', '/api/v1/users/:userId',
metaApiMetrics, metaApiMetrics,
ncMetaAclMw(userDelete, 'userDelete', [OrgUserRoles.SUPER]) 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 NocoCache from '../cache/NocoCache';
import { NcError } from '../meta/helpers/catchError'; import { NcError } from '../meta/helpers/catchError';
export default class User implements UserType { export default class User implements UserType {
id: string; id: string
/** @format email */ /** @format email */
email: string; email: string
password?: string; password?: string
salt?: string; salt?: string
firstname: string; firstname: string
lastname: string; lastname: string
username?: string; username?: string
refresh_token?: string; refresh_token?: string
invite_token?: string; invite_token?: string
invite_token_expires?: number | Date; invite_token_expires?: number | Date
reset_password_expires?: number | Date; reset_password_expires?: number | Date
reset_password_token?: string; reset_password_token?: string
email_verification_token?: string; email_verification_token?: string
email_verified: boolean; email_verified: boolean
roles?: string; roles?: string
token_version?: string; token_version?: string
constructor(data: User) { constructor(data: User) {
Object.assign(this, data); Object.assign(this, data)
} }
public static async insert(user: Partial<User>, ncMeta = Noco.ncMeta) { public static async insert(user: Partial<User>, ncMeta = Noco.ncMeta) {
@ -48,22 +48,22 @@ export default class User implements UserType {
'email_verified', 'email_verified',
'roles', 'roles',
'token_version', 'token_version',
]); ])
if (insertObj.email) { if (insertObj.email) {
insertObj.email = insertObj.email.toLowerCase(); insertObj.email = insertObj.email.toLowerCase()
} }
const { id } = await ncMeta.metaInsert2( const { id } = await ncMeta.metaInsert2(
null, null,
null, null,
MetaTable.USERS, 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) { public static async update(id, user: Partial<User>, ncMeta = Noco.ncMeta) {
const updateObj = extractProps(user, [ const updateObj = extractProps(user, [
@ -82,13 +82,13 @@ export default class User implements UserType {
'email_verified', 'email_verified',
'roles', 'roles',
'token_version', 'token_version',
]); ])
if (updateObj.email) { if (updateObj.email) {
updateObj.email = updateObj.email.toLowerCase(); updateObj.email = updateObj.email.toLowerCase()
} else { } else {
// set email prop to avoid generation of invalid cache key // 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 // get existing cache
const keys = [ const keys = [
@ -96,43 +96,43 @@ export default class User implements UserType {
`${CacheScope.USER}:${id}`, `${CacheScope.USER}:${id}`,
// update user:<email> // update user:<email>
`${CacheScope.USER}:${user.email}`, `${CacheScope.USER}:${user.email}`,
]; ]
for (const key of keys) { 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) { if (o) {
o = { ...o, ...updateObj }; o = { ...o, ...updateObj }
// set cache // set cache
await NocoCache.set(key, o); await NocoCache.set(key, o)
} }
} }
// as <projectId> is unknown, delete user:<email>___<projectId> in cache // 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 // 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) { public static async getByEmail(_email: string, ncMeta = Noco.ncMeta) {
const email = _email?.toLowerCase(); const email = _email?.toLowerCase()
let user = let user =
email && email &&
(await NocoCache.get( (await NocoCache.get(
`${CacheScope.USER}:${email}`, `${CacheScope.USER}:${email}`,
CacheGetType.TYPE_OBJECT CacheGetType.TYPE_OBJECT,
)); ))
if (!user) { if (!user) {
user = await ncMeta.metaGet2(null, null, MetaTable.USERS, { user = await ncMeta.metaGet2(null, null, MetaTable.USERS, {
email, 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) { static async isFirst(ncMeta = Noco.ncMeta) {
const isFirst = !(await NocoCache.getAll(`${CacheScope.USER}:*`))?.length; const isFirst = !(await NocoCache.getAll(`${CacheScope.USER}:*`))?.length
if (isFirst) if (isFirst)
return !(await ncMeta.metaGet2(null, null, MetaTable.USERS, {})); return !(await ncMeta.metaGet2(null, null, MetaTable.USERS, {}))
return false; return false
} }
public static async count( public static async count(
@ -141,15 +141,15 @@ export default class User implements UserType {
}: { }: {
query?: string; query?: string;
} = {}, } = {},
ncMeta = Noco.ncMeta ncMeta = Noco.ncMeta,
): Promise<number> { ): Promise<number> {
const qb = ncMeta.knex(MetaTable.USERS); const qb = ncMeta.knex(MetaTable.USERS)
if (query) { 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> { static async get(userId, ncMeta = Noco.ncMeta): Promise<UserType> {
@ -157,20 +157,20 @@ export default class User implements UserType {
userId && userId &&
(await NocoCache.get( (await NocoCache.get(
`${CacheScope.USER}:${userId}`, `${CacheScope.USER}:${userId}`,
CacheGetType.TYPE_OBJECT CacheGetType.TYPE_OBJECT,
)); ))
if (!user) { if (!user) {
user = await ncMeta.metaGet2(null, null, MetaTable.USERS, userId); user = await ncMeta.metaGet2(null, null, MetaTable.USERS, userId)
await NocoCache.set(`${CacheScope.USER}:${userId}`, user); await NocoCache.set(`${CacheScope.USER}:${userId}`, user)
} }
return user; return user
} }
static async getByRefreshToken(refresh_token, ncMeta = Noco.ncMeta) { static async getByRefreshToken(refresh_token, ncMeta = Noco.ncMeta) {
const user = await ncMeta.metaGet2(null, null, MetaTable.USERS, { const user = await ncMeta.metaGet2(null, null, MetaTable.USERS, {
refresh_token, refresh_token,
}); })
return user; return user
} }
public static async list( public static async list(
@ -183,7 +183,7 @@ export default class User implements UserType {
offset?: number | undefined; offset?: number | undefined;
query?: string; query?: string;
} = {}, } = {},
ncMeta = Noco.ncMeta ncMeta = Noco.ncMeta,
) { ) {
let queryBuilder = ncMeta.knex(MetaTable.USERS); let queryBuilder = ncMeta.knex(MetaTable.USERS);
@ -214,7 +214,7 @@ export default class User implements UserType {
.as('projectsCount') .as('projectsCount')
); );
if (query) { if (query) {
queryBuilder.where('email', 'like', `%${query.toLowerCase?.()}%`); queryBuilder.where('email', 'like', `%${query.toLowerCase?.()}%`)
} }
return queryBuilder; return queryBuilder;
@ -226,8 +226,4 @@ export default class User implements UserType {
await NocoCache.del(`${CacheScope.USER}:${userId}`); await NocoCache.del(`${CacheScope.USER}:${userId}`);
await ncMeta.metaDelete(null, null, MetaTable.USERS, 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" "format": "email"
}, },
"roles": { "roles": {
"type": "string", "type": "string"
"format": "email"
}, },
"date_of_birth": { "date_of_birth": {
"type": "string", "type": "string",

Loading…
Cancel
Save