Browse Source

feat(api): add left nav drawer and token page

Signed-off-by: Pranav C <pranavxc@gmail.com>
pull/4134/head
Pranav C 2 years ago
parent
commit
89080de589
  1. 2
      packages/nc-gui/components.d.ts
  2. 81
      packages/nc-gui/components/org/Token.vue
  3. 238
      packages/nc-gui/components/org/User.vue
  4. 13
      packages/nc-gui/components/org/UsersModal.vue
  5. 13
      packages/nc-gui/layouts/base.vue
  6. 49
      packages/nc-gui/pages/org/[page].vue
  7. 11
      packages/nc-gui/pages/users/index.vue

2
packages/nc-gui/components.d.ts vendored

@ -107,6 +107,7 @@ declare module '@vue/runtime-core' {
MdiAccountOutline: typeof import('~icons/mdi/account-outline')['default'] MdiAccountOutline: typeof import('~icons/mdi/account-outline')['default']
MdiAccountPlusOutline: typeof import('~icons/mdi/account-plus-outline')['default'] MdiAccountPlusOutline: typeof import('~icons/mdi/account-plus-outline')['default']
MdiAccountSupervisorOutline: typeof import('~icons/mdi/account-supervisor-outline')['default'] MdiAccountSupervisorOutline: typeof import('~icons/mdi/account-supervisor-outline')['default']
MdiAccountSupervisorOutline: typeof import('~icons/mdi/account-supervisor-outline')['default']
MdiAdd: typeof import('~icons/mdi/add')['default'] MdiAdd: typeof import('~icons/mdi/add')['default']
MdiAdd: typeof import('~icons/mdi/add')['default'] MdiAdd: typeof import('~icons/mdi/add')['default']
MdiAlpha: typeof import('~icons/mdi/alpha')['default'] MdiAlpha: typeof import('~icons/mdi/alpha')['default']
@ -203,6 +204,7 @@ declare module '@vue/runtime-core' {
MdiRocketLaunchOutline: typeof import('~icons/mdi/rocket-launch-outline')['default'] MdiRocketLaunchOutline: typeof import('~icons/mdi/rocket-launch-outline')['default']
MdiScriptTextKeyOutline: typeof import('~icons/mdi/script-text-key-outline')['default'] MdiScriptTextKeyOutline: typeof import('~icons/mdi/script-text-key-outline')['default']
MdiScriptTextOutline: typeof import('~icons/mdi/script-text-outline')['default'] MdiScriptTextOutline: typeof import('~icons/mdi/script-text-outline')['default']
MdiShieldAccountOutline: typeof import('~icons/mdi/shield-account-outline')['default']
MdiShieldKeyOutline: typeof import('~icons/mdi/shield-key-outline')['default'] MdiShieldKeyOutline: typeof import('~icons/mdi/shield-key-outline')['default']
MdiSlack: typeof import('~icons/mdi/slack')['default'] MdiSlack: typeof import('~icons/mdi/slack')['default']
MdiSort: typeof import('~icons/mdi/sort')['default'] MdiSort: typeof import('~icons/mdi/sort')['default']

81
packages/nc-gui/components/org-user/index.vue → packages/nc-gui/components/org/Token.vue

@ -1,11 +1,19 @@
<script lang="ts" setup> <script lang="ts" setup>
import { Modal, message } from 'ant-design-vue' import { Modal, message } from 'ant-design-vue'
import type { RequestParams, UserType } from 'nocodb-sdk' import type { RequestParams, UserType } from 'nocodb-sdk'
import { Role, extractSdkResponseErrorMsg } from '#imports' import { Role, extractSdkResponseErrorMsg, useApi, useDashboard, useNuxtApp , useCopy} from '#imports'
import { useApi } from '~/composables/useApi' import type { User } from '~/lib'
const { api, isLoading } = useApi() const { api, isLoading } = useApi()
const { $e } = useNuxtApp()
const { t } = useI18n()
const { dashboardUrl } = $(useDashboard())
const { copy } = useCopy()
let users = $ref<UserType[]>([]) let users = $ref<UserType[]>([])
let currentPage = $ref(1) let currentPage = $ref(1)
@ -70,6 +78,30 @@ const deleteUser = async (userId: string) => {
}, },
}) })
} }
const resendInvite = async (user: User) => {
try {
await api.orgUsers.resendInvite(user.id)
// Invite email sent successfully
message.success(t('msg.success.inviteEmailSent'))
await loadUsers()
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
$e('a:org-user:resend-invite')
}
const copyInviteUrl = (user: User) => {
if (!user.invite_token) return
copy(`${dashboardUrl}#/signup/${user.invite_token}`)
// Invite URL copied to clipboard
message.success(t('msg.success.inviteURLCopied'))
$e('c:user:copy-url')
}
</script> </script>
<template> <template>
@ -99,6 +131,7 @@ const deleteUser = async (userId: string) => {
:pagination="pagination" :pagination="pagination"
:loading="isLoading" :loading="isLoading"
@change="loadUsers($event.current)" @change="loadUsers($event.current)"
size="small"
> >
<template #emptyText> <template #emptyText>
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('labels.noData')" /> <a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('labels.noData')" />
@ -126,14 +159,14 @@ const deleteUser = async (userId: string) => {
@change="updateRole(record.id, record.roles)" @change="updateRole(record.id, record.roles)"
> >
<a-select-option :value="Role.OrgLevelCreator" :label="$t(`objects.roleType.orgLevelCreator`)"> <a-select-option :value="Role.OrgLevelCreator" :label="$t(`objects.roleType.orgLevelCreator`)">
<div >{{ $t(`objects.roleType.orgLevelCreator`) }}</div> <div>{{ $t(`objects.roleType.orgLevelCreator`) }}</div>
<span class="text-gray-500 text-xs whitespace-normal" <span class="text-gray-500 text-xs whitespace-normal"
>Creator can create new projects and access any invited project.</span >Creator can create new projects and access any invited project.</span
> >
</a-select-option> </a-select-option>
<a-select-option :value="Role.OrgLevelViewer" :label="$t(`objects.roleType.orgLevelViewer`)"> <a-select-option :value="Role.OrgLevelViewer" :label="$t(`objects.roleType.orgLevelViewer`)">
<div >{{ $t(`objects.roleType.orgLevelViewer`) }}</div> <div>{{ $t(`objects.roleType.orgLevelViewer`) }}</div>
<span class="text-gray-500 text-xs whitespace-normal" <span class="text-gray-500 text-xs whitespace-normal"
>Viewer is not allowed to create new projects but they can access any invited project.</span >Viewer is not allowed to create new projects but they can access any invited project.</span
> >
@ -143,21 +176,55 @@ const deleteUser = async (userId: string) => {
</template> </template>
</a-table-column> </a-table-column>
<!-- Projects --> <!-- &lt;!&ndash; Projects &ndash;&gt;
<a-table-column key="projectsCount" :title="$t('objects.projects')" data-index="projectsCount"> <a-table-column key="projectsCount" :title="$t('objects.projects')" data-index="projectsCount">
<template #default="{ text }"> <template #default="{ text }">
<div> <div>
{{ text }} {{ text }}
</div> </div>
</template> </template>
</a-table-column> </a-table-column>-->
<!-- Actions --> <!-- Actions -->
<a-table-column key="id" :title="$t('labels.actions')" data-index="id"> <a-table-column key="id" :title="$t('labels.actions')" data-index="id">
<template #default="{ text }"> <template #default="{ text, record }">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<MdiDeleteOutline class="nc-action-btn cursor-pointer" @click="deleteUser(text)" /> <MdiDeleteOutline class="nc-action-btn cursor-pointer" @click="deleteUser(text)" />
<a-dropdown :trigger="['click']" class="flex" placement="bottomRight" overlay-class-name="nc-dropdown-user-mgmt">
<div class="flex flex-row items-center">
<a-button type="text" class="!px-0">
<div class="flex flex-row items-center h-[1.2rem]">
<IcBaselineMoreVert />
</div>
</a-button>
</div>
<template #overlay>
<a-menu>
<a-menu-item>
<!-- Resend invite Email -->
<div class="flex flex-row items-center py-3" @click="resendInvite(record)">
<MdiEmailArrowRightOutline class="flex h-[1rem] text-gray-500" />
<div class="text-xs pl-2">{{ $t('activity.resendInvite') }}</div>
</div>
</a-menu-item>
<a-menu-item>
<div class="flex flex-row items-center py-3" @click="copyInviteUrl(record)">
<MdiContentCopy class="flex h-[1rem] text-gray-500" />
<div class="text-xs pl-2">{{ $t('activity.copyInviteURL') }}</div>
</div>
</a-menu-item>
<!-- <a-menu-item>
<div class="flex flex-row items-center py-3" @click="copyInviteUrl(user)">
<MdiContentCopy class="flex h-[1rem] text-gray-500" />
<div class="text-xs pl-2">{{ $t('activity.copyPasswordResetURL') }}</div>
</div>
</a-menu-item> -->
</a-menu>
</template>
</a-dropdown>
</div> </div>
</template> </template>
</a-table-column> </a-table-column>

238
packages/nc-gui/components/org/User.vue

@ -0,0 +1,238 @@
<script lang="ts" setup>
import { Modal, message } from 'ant-design-vue'
import type { RequestParams, UserType } from 'nocodb-sdk'
import { Role, extractSdkResponseErrorMsg, useApi, useDashboard, useNuxtApp , useCopy} from '#imports'
import type { User } from '~/lib'
const { api, isLoading } = useApi()
const { $e } = useNuxtApp()
const { t } = useI18n()
const { dashboardUrl } = $(useDashboard())
const { copy } = useCopy()
let users = $ref<UserType[]>([])
let currentPage = $ref(1)
const currentLimit = $ref(10)
const showUserModal = ref(false)
const searchText = ref<string>('')
const pagination = reactive({
total: 0,
pageSize: 10,
})
const loadUsers = async (page = currentPage, limit = currentLimit) => {
currentPage = page
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 UserType[]
} 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 UserType)
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?',
type: 'warn',
content:
'On deleting, user will remove from from organization and any sync source(Airtable) created by user will get removed',
onOk: async () => {
try {
await api.orgUsers.delete(userId)
message.success('User deleted successfully')
await loadUsers()
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
},
})
}
const resendInvite = async (user: User) => {
try {
await api.orgUsers.resendInvite(user.id)
// Invite email sent successfully
message.success(t('msg.success.inviteEmailSent'))
await loadUsers()
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
$e('a:org-user:resend-invite')
}
const copyInviteUrl = (user: User) => {
if (!user.invite_token) return
copy(`${dashboardUrl}#/signup/${user.invite_token}`)
// Invite URL copied to clipboard
message.success(t('msg.success.inviteURLCopied'))
$e('c:user:copy-url')
}
</script>
<template>
<div class="h-full overflow-y-scroll scrollbar-thin-dull">
<div class="max-w-[700px] mx-auto p-4">
<div class="py-2 flex">
<a-input-search
v-model:value="searchText"
size="small"
class="max-w-[300px]"
placeholder="Filter by email"
@blur="loadUsers"
@keydown.enter="loadUsers"
>
</a-input-search>
<div class="flex-grow"></div>
<a-button size="small" @click="showUserModal = true">
<div class="flex items-center gap-1">
<MdiAdd />
Invite new user
</div>
</a-button>
</div>
<a-table
:row-key="(record) => record.id"
:data-source="users"
:pagination="{ position: ['bottomCenter'] }"
:loading="isLoading"
@change="loadUsers($event.current)"
size="small"
>
<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>
<div v-if="record.roles.includes('super')" class="font-weight-bold">Super Admin</div>
<a-select
v-else
v-model:value="record.roles"
class="w-[220px]"
:dropdown-match-select-width="false"
@change="updateRole(record.id, record.roles)"
>
<a-select-option :value="Role.OrgLevelCreator" :label="$t(`objects.roleType.orgLevelCreator`)">
<div>{{ $t(`objects.roleType.orgLevelCreator`) }}</div>
<span class="text-gray-500 text-xs whitespace-normal"
>Creator can create new projects and access any invited project.</span
>
</a-select-option>
<a-select-option :value="Role.OrgLevelViewer" :label="$t(`objects.roleType.orgLevelViewer`)">
<div>{{ $t(`objects.roleType.orgLevelViewer`) }}</div>
<span class="text-gray-500 text-xs whitespace-normal"
>Viewer is not allowed to create new projects but they can access any invited project.</span
>
</a-select-option>
</a-select>
</div>
</template>
</a-table-column>
<!-- &lt;!&ndash; Projects &ndash;&gt;
<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, record }">
<div class="flex items-center gap-2">
<MdiDeleteOutline class="nc-action-btn cursor-pointer" @click="deleteUser(text)" />
<a-dropdown :trigger="['click']" class="flex" placement="bottomRight" overlay-class-name="nc-dropdown-user-mgmt">
<div class="flex flex-row items-center">
<a-button type="text" class="!px-0">
<div class="flex flex-row items-center h-[1.2rem]">
<IcBaselineMoreVert />
</div>
</a-button>
</div>
<template #overlay>
<a-menu>
<a-menu-item>
<!-- Resend invite Email -->
<div class="flex flex-row items-center py-3" @click="resendInvite(record)">
<MdiEmailArrowRightOutline class="flex h-[1rem] text-gray-500" />
<div class="text-xs pl-2">{{ $t('activity.resendInvite') }}</div>
</div>
</a-menu-item>
<a-menu-item>
<div class="flex flex-row items-center py-3" @click="copyInviteUrl(record)">
<MdiContentCopy class="flex h-[1rem] text-gray-500" />
<div class="text-xs pl-2">{{ $t('activity.copyInviteURL') }}</div>
</div>
</a-menu-item>
<!-- <a-menu-item>
<div class="flex flex-row items-center py-3" @click="copyInviteUrl(user)">
<MdiContentCopy class="flex h-[1rem] text-gray-500" />
<div class="text-xs pl-2">{{ $t('activity.copyPasswordResetURL') }}</div>
</div>
</a-menu-item> -->
</a-menu>
</template>
</a-dropdown>
</div>
</template>
</a-table-column>
</a-table>
<LazyOrgUserUsersModal :show="showUserModal" @closed="showUserModal = false" @reload="loadUsers" />
</div>
</div>
</template>
<style scoped></style>

13
packages/nc-gui/components/org-user/UsersModal.vue → packages/nc-gui/components/org/UsersModal.vue

@ -6,9 +6,6 @@ import {
extractSdkResponseErrorMsg, extractSdkResponseErrorMsg,
isEmail, isEmail,
message, message,
onMounted,
projectRoleTagColors,
projectRoles,
ref, ref,
useCopy, useCopy,
useDashboard, useDashboard,
@ -16,7 +13,7 @@ import {
useNuxtApp, useNuxtApp,
} from '#imports' } from '#imports'
import type { User } from '~/lib' import type { User } from '~/lib'
import { ProjectRole, Role } from '~/lib' import { Role } from '~/lib'
interface Props { interface Props {
show: boolean show: boolean
@ -210,12 +207,16 @@ const clickInviteMore = () => {
<a-select v-model:value="usersData.role" class="nc-user-roles" dropdown-class-name="nc-dropdown-user-role"> <a-select v-model:value="usersData.role" class="nc-user-roles" dropdown-class-name="nc-dropdown-user-role">
<a-select-option :value="Role.OrgLevelCreator" :label="$t(`objects.roleType.orgLevelCreator`)"> <a-select-option :value="Role.OrgLevelCreator" :label="$t(`objects.roleType.orgLevelCreator`)">
<div>{{ $t(`objects.roleType.orgLevelCreator`) }}</div> <div>{{ $t(`objects.roleType.orgLevelCreator`) }}</div>
<span class="text-gray-500 text-xs whitespace-normal ">Creator can create new projects and access any invited project.</span> <span class="text-gray-500 text-xs whitespace-normal"
>Creator can create new projects and access any invited project.</span
>
</a-select-option> </a-select-option>
<a-select-option :value="Role.OrgLevelViewer" :label="$t(`objects.roleType.orgLevelViewer`)"> <a-select-option :value="Role.OrgLevelViewer" :label="$t(`objects.roleType.orgLevelViewer`)">
<div>{{ $t(`objects.roleType.orgLevelViewer`) }}</div> <div>{{ $t(`objects.roleType.orgLevelViewer`) }}</div>
<span class="text-gray-500 text-xs whitespace-normal">Viewer is not allowed to create new projects but they can access any invited project.</span> <span class="text-gray-500 text-xs whitespace-normal"
>Viewer is not allowed to create new projects but they can access any invited project.</span
>
</a-select-option> </a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>

13
packages/nc-gui/layouts/base.vue

@ -106,6 +106,19 @@ hooks.hook('page:finish', () => {
<span class="prose group-hover:text-primary">{{ $t('title.appStore') }}</span> <span class="prose group-hover:text-primary">{{ $t('title.appStore') }}</span>
</nuxt-link> </nuxt-link>
</a-menu-item> </a-menu-item>
<a-menu-divider class="!m-0" />
<a-menu-item v-if="isUIAllowed('appStore')" key="0" class="!rounded-t">
<nuxt-link
v-e="['c:settings:appstore', { page: true }]"
class="nc-project-menu-item group !no-underline"
to="/org/users"
>
<MdiShieldAccountOutline class="mt-1 group-hover:text-accent" />&nbsp;
<!-- todo: i18n -->
<span class="prose group-hover:text-primary">Account management</span>
</nuxt-link>
</a-menu-item>
<a-menu-divider class="!m-0" /> <a-menu-divider class="!m-0" />

49
packages/nc-gui/pages/org/[page].vue

@ -0,0 +1,49 @@
<template>
<a-layout class="mt-3 h-[75vh] overflow-y-auto flex">
<!-- Side tabs -->
<a-layout-sider>
<a-menu :selected-keys="selectedTabKeys" class="tabs-menu h-full" :open-keys="[]">
<a-menu-item
key="users"
@click="navigateTo('/org/users')"
class="group active:(!ring-0) hover:(!bg-primary !bg-opacity-25)"
>
<div class="flex items-center space-x-2">
<MdiAccountSupervisorOutline/>
<div class="select-none">
Users
</div>
</div>
</a-menu-item>
<a-menu-item
key="tokens"
@click="navigateTo('/org/tokens')"
class="group active:(!ring-0) hover:(!bg-primary !bg-opacity-25)"
>
<div class="flex items-center space-x-2">
<MdiShieldKeyOutline/>
<div class="select-none">
Tokens
</div>
</div>
</a-menu-item>
</a-menu>
</a-layout-sider>
<!-- Sub Tabs -->
<a-layout-content class="h-auto px-4 scrollbar-thumb-gray-500">
<OrgUser v-if="$route.params.page === 'users'" />
<OrgToken v-else-if="$route.params.page === 'tokens'" />
</a-layout-content>
</a-layout>
</template>
<script lang="ts" setup>
import { navigateTo } from '#imports'
const $route = useRoute()
const selectedTabKeys = computed(() => [$route.params.page])
</script>

11
packages/nc-gui/pages/users/index.vue

@ -1,11 +0,0 @@
<script>
export default {
name: 'index.vue',
}
</script>
<template>
<OrgUser />
</template>
<style scoped></style>
Loading…
Cancel
Save