Skip to content

Commit

Permalink
Users admin tab
Browse files Browse the repository at this point in the history
  • Loading branch information
gaspergrom committed Nov 4, 2024
1 parent 66ef96a commit cbe0250
Show file tree
Hide file tree
Showing 6 changed files with 227 additions and 0 deletions.
9 changes: 9 additions & 0 deletions backend/src/database/repositories/userRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,15 @@ export default class UserRepository {
whereAnd.push(SequelizeFilterUtils.ilikeIncludes('user', 'email', filter.email))
}

if (filter.query) {
whereAnd.push({
[Op.or]: [
SequelizeFilterUtils.ilikeIncludes('user', 'fullName', filter.query),
SequelizeFilterUtils.ilikeIncludes('user', 'email', filter.query),
],
})
}

if (filter.role) {
const innerWhereAnd: Array<any> = []

Expand Down
6 changes: 6 additions & 0 deletions frontend/src/modules/admin/models/User.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface UserModel {
id: string;
email: string;
fullName: string;
roles: string[];
}
145 changes: 145 additions & 0 deletions frontend/src/modules/admin/pages/users.page.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<template>
<div>
<div class="pt-6 pb-6">
<lf-search v-model="search" class="h-9" :lazy="true" placeholder="Search users..." @update:model-value="searchUsers()" />
</div>
<div v-if="users.length > 0">
<lf-table>
<thead>
<tr>
<th>User</th>
<th>Email</th>
<th>Role</th>
</tr>
</thead>
<tbody>
<tr v-for="user of users" :key="user.id">
<td>
<div class="flex items-center gap-3">
<lf-avatar :name="user.fullName" :size="32" />
<p class="text-medium font-semibold">
{{ user.fullName }}
</p>
</div>
</td>
<td>
<p class="text-medium">
{{ user.email }}
</p>
</td>
<td>
<p class="text-medium">
{{ roleDisplay(user.roles) }}
</p>
</td>
</tr>
</tbody>
</lf-table>
<div v-if="users.length < total" class="pt-4">
<lf-button
type="primary-ghost"
loading-text="Loading users..."
:loading="loading"
@click="loadMore()"
>
Load more
</lf-button>
</div>
</div>

<div v-else-if="!loading">
<app-empty-state-cta
icon="user-group"
title="No users found"
/>
</div>
<div v-if="loading" class="pt-8 flex justify-center">
<lf-spinner />
</div>
</div>
</template>

<script setup lang="ts">
import LfSearch from '@/ui-kit/search/Search.vue';
import { onMounted, ref } from 'vue';
import { UsersService } from '@/modules/admin/services/users.service';
import { UserModel } from '@/modules/admin/models/User.model';
import LfTable from '@/ui-kit/table/Table.vue';
import LfAvatar from '@/ui-kit/avatar/Avatar.vue';
import LfSpinner from '@/ui-kit/spinner/Spinner.vue';
import LfButton from '@/ui-kit/button/Button.vue';
const search = ref('');
const loading = ref<boolean>(false);
const offset = ref(0);
const limit = ref(20);
const total = ref(0);
const users = ref<UserModel[]>([]);
const fetchUsers = () => {
if (loading.value) {
return;
}
loading.value = true;
UsersService.list({
filter: {
query: search.value,
},
offset: offset.value,
limit: limit.value,
})
.then((res) => {
if (offset.value > 0) {
users.value = [...users.value, ...res.rows];
} else {
users.value = res.rows;
}
if (res.rows.length > 0) {
total.value = res.count;
} else {
total.value = users.value.length;
}
})
.finally(() => {
loading.value = false;
});
};
const searchUsers = () => {
offset.value = 0;
fetchUsers();
};
const loadMore = () => {
offset.value = users.value.length;
fetchUsers();
};
const roleDisplay = (roles: string[]) => {
const role = roles?.[0];
if (role === 'admin') {
return 'Admin';
}
if (role === 'projectAdmin') {
return 'Project Admin';
}
if (role === 'readonly') {
return 'Read-only';
}
return role;
};
onMounted(() => {
searchUsers();
});
</script>

<script lang="ts">
export default {
name: 'LfAdminUsers',
};
</script>
17 changes: 17 additions & 0 deletions frontend/src/modules/admin/services/users.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import authAxios from '@/shared/axios/auth-axios';
import { AuthService } from '@/modules/auth/services/auth.service';

export class UsersService {
static async list(query: any) {
const tenantId = AuthService.getTenantId();

const response = await authAxios.get(
`/tenant/${tenantId}/user`,
{
params: query,
},
);

return response.data;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
v-if="activeTab === 'audit-logs'"
/>
</el-tab-pane>
<el-tab-pane v-if="isAdminUser" label="Users" name="users">
<lf-admin-users v-if="activeTab === 'users'" />
</el-tab-pane>
<el-tab-pane v-if="isDevMode" label="Dev" name="dev">
<lf-devmode v-if="isDevMode && activeTab === 'dev'" />
</el-tab-pane>
Expand All @@ -53,6 +56,7 @@ import AppLfAuditLogsPage from '@/modules/lf/segments/pages/lf-audit-logs-page.v
import LfDevmode from '@/modules/lf/segments/components/dev/devmode.vue';
import { LfRole } from '@/shared/modules/permissions/types/Roles';
import AppOrganizationCommonPage from '@/modules/organization/pages/organization-common-page.vue';
import LfAdminUsers from '@/modules/admin/pages/users.page.vue';
const route = useRoute();
const router = useRouter();
Expand Down
46 changes: 46 additions & 0 deletions frontend/src/ui-kit/search/Search.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<template>
<lf-input v-model="valueProxy">
<template #prefix>
<lf-icon name="search" />
</template>
<template v-if="valueProxy.length" #suffix>
<div @click="valueProxy = ''">
<lf-icon name="xmark" />
</div>
</template>
</lf-input>
</template>

<script setup lang="ts">
import { ref, watch } from 'vue';
import { debounce } from 'lodash';
import LfInput from '@/ui-kit/input/Input.vue';
import LfIcon from '@/ui-kit/icon/Icon.vue';
const props = defineProps<{
modelValue: string | number,
lazy?: boolean,
}>();
const emit = defineEmits<{(e: 'update:modelValue', value: string | number): any }>();
const valueProxy = ref(props.modelValue);
const emitValue = (value: string | number) => emit('update:modelValue', value);
const debouncedEmitValue = debounce(emitValue, 300);
watch(valueProxy, (newVal) => {
if (props.lazy) {
debouncedEmitValue(newVal);
} else {
emitValue(newVal);
}
});
</script>

<script lang="ts">
export default {
name: 'LfSearch',
};
</script>

0 comments on commit cbe0250

Please sign in to comment.