Skip to content

Commit

Permalink
feat: new tickets UI (#582)
Browse files Browse the repository at this point in the history
  • Loading branch information
ymarcon authored Jan 4, 2025
1 parent 3aa94f8 commit bbfaa4c
Show file tree
Hide file tree
Showing 12 changed files with 446 additions and 209 deletions.
3 changes: 2 additions & 1 deletion agate-ui/.eslintrc-auto-import.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@
"onWatcherCleanup": true,
"useId": true,
"useModel": true,
"useTemplateRef": true
"useTemplateRef": true,
"useTicketStore": true
}
}
381 changes: 184 additions & 197 deletions agate-ui/src/auto-imports.d.ts

Large diffs are not rendered by default.

6 changes: 1 addition & 5 deletions agate-ui/src/components/GroupsTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
</q-td>
<q-td key="applications" :props="props">
<template v-for="app in props.row.applications" :key="app">
<q-badge :label="getApplicationName(app)" class="on-left" />
<q-badge :label="applicationStore.getApplicationName(app)" class="on-left" />
</template>
</q-td>
</q-tr>
Expand Down Expand Up @@ -134,8 +134,4 @@ function onAdd() {
function onSaved() {
refresh();
}
function getApplicationName(id: string) {
return applicationStore.applications?.find((app) => app.id === id)?.name;
}
</script>
64 changes: 64 additions & 0 deletions agate-ui/src/components/TicketEventsDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<template>
<q-dialog v-model="showDialog" persistent @hide="onHide">
<q-card class="dialog-sm">
<q-card-section>
<div class="text-h6">{{ t('ticket.events') }}</div>
</q-card-section>

<q-separator />

<q-card-section>
<q-list separator class="fields-list">
<q-item v-for="(event, idx) in events" :key="idx">
<q-item-section>
<q-item-label>
<q-badge :label="applicationStore.getApplicationName(event.application)" />
</q-item-label>
<q-item-label caption>{{ getDateLabel(event.time) }}</q-item-label>
</q-item-section>
<q-item-section avatar>
<q-chip :label="event.action" size="sm" color="accent" class="text-white" />
</q-item-section>
</q-item>
</q-list>
</q-card-section>

<q-separator />

<q-card-actions align="right" class="bg-grey-3">
<q-btn flat :label="t('close')" color="primary" v-close-popup />
</q-card-actions>
</q-card>
</q-dialog>
</template>

<script setup lang="ts">
import type { TicketDto } from 'src/models/Agate';
import { getDateLabel } from 'src/utils/dates';
const { t } = useI18n();
const applicationStore = useApplicationStore();
interface DialogProps {
modelValue: boolean;
ticket: TicketDto | undefined;
}
const props = defineProps<DialogProps>();
const emit = defineEmits(['update:modelValue']);
const showDialog = ref(props.modelValue);
const events = ref(props.ticket?.events);
watch(
() => props.modelValue,
(value) => {
showDialog.value = value;
events.value = props.ticket?.events;
},
);
function onHide() {
emit('update:modelValue', false);
}
</script>
125 changes: 125 additions & 0 deletions agate-ui/src/components/TicketsTable.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<template>
<div>
<q-table :rows="tickets" flat row-key="name" :columns="columns" :pagination="initialPagination">
<template v-slot:top-right>
<q-input v-model="filter" debounce="300" :placeholder="t('search')" dense clearable class="q-mr-md">
<template v-slot:prepend>
<q-icon name="search" />
</template>
</q-input>
</template>
<template v-slot:body="props">
<q-tr :props="props" @mouseover="onOverRow(props.row)" @mouseleave="onLeaveRow(props.row)">
<q-td key="id" :props="props">
<span class="text-primary">{{ props.row.id }}</span>
<div class="float-right">
<q-btn
v-if="!props.row.hasDatasource"
rounded
dense
flat
size="sm"
color="secondary"
:title="t('delete')"
:icon="toolsVisible[props.row.id] ? 'delete' : 'none'"
class="q-ml-xs"
@click="onShowDelete(props.row)"
/>
</div>
</q-td>
<q-td key="username" :props="props">
<q-chip size="sm">{{ props.row.username }}</q-chip>
</q-td>
<q-td key="expires" :props="props">
<span>{{ getDateLabel(props.row.expires) }}</span>
</q-td>
<q-td key="login_app" :props="props">
<q-badge :label="applicationStore.getApplicationName(getLoginApp(props.row))" />
</q-td>
<q-td key="events" :props="props">
<q-btn :label="props.row.events?.length ?? 0" color="accent" size="sm" @click="onShowEvents(props.row)" />
</q-td>
</q-tr>
</template>
</q-table>
<confirm-dialog
v-model="showDelete"
:title="t('ticket.remove')"
:text="t('ticket.remove_confirm', { id: selected?.id, username: selected?.username })"
@confirm="onDelete"
/>
<ticket-events-dialog v-model="showEventsDialog" :ticket="selected" />
</div>
</template>

<script setup lang="ts">
import type { TicketDto } from 'src/models/Agate';
import ConfirmDialog from 'src/components/ConfirmDialog.vue';
import TicketEventsDialog from 'src/components/TicketEventsDialog.vue';
import { DefaultAlignment } from 'src/components/models';
import { getDateLabel } from 'src/utils/dates';
const { t } = useI18n();
const ticketStore = useTicketStore();
const applicationStore = useApplicationStore();
const filter = ref('');
const toolsVisible = ref<{ [key: string]: boolean }>({});
const initialPagination = ref({
descending: false,
page: 1,
rowsPerPage: 20,
});
const showDelete = ref(false);
const showEventsDialog = ref(false);
const selected = ref();
const tickets = computed<TicketDto[]>(
() =>
ticketStore.tickets?.filter((tk) =>
filter.value ? tk.username.toLowerCase().includes(filter.value.toLowerCase()) : true,
) || [],
);
const columns = computed(() => [
{ name: 'id', label: 'ID', field: 'id', align: DefaultAlignment },
{ name: 'username', label: t('username'), field: 'username', align: DefaultAlignment },
{ name: 'expires', label: t('ticket.expires'), field: 'expires', align: DefaultAlignment },
{ name: 'login_app', label: t('ticket.login_app'), field: 'events', align: DefaultAlignment },
{ name: 'events', label: t('ticket.events'), field: 'events', align: DefaultAlignment },
]);
onMounted(() => {
applicationStore.init();
refresh();
});
function refresh() {
ticketStore.init();
}
function onOverRow(row: TicketDto) {
toolsVisible.value[row.id] = true;
}
function onLeaveRow(row: TicketDto) {
toolsVisible.value[row.id] = false;
}
function onShowDelete(row: TicketDto) {
selected.value = row;
showDelete.value = true;
}
function onShowEvents(row: TicketDto) {
selected.value = row;
showEventsDialog.value = true;
}
function onDelete() {
ticketStore.remove(selected.value).finally(refresh);
}
function getLoginApp(row: TicketDto) {
return row.events?.find((evt) => evt.action === 'login')?.application;
}
</script>
8 changes: 2 additions & 6 deletions agate-ui/src/components/UsersTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -127,12 +127,12 @@
</q-td>
<q-td key="applications" :props="props">
<template v-for="app in props.row.applications" :key="app">
<q-badge :label="getApplicationName(app)" class="on-left" />
<q-badge :label="applicationStore.getApplicationName(app)" class="on-left" />
</template>
<template v-for="(grpApp, idx) in props.row.groupApplications" :key="idx">
<q-badge
color="secondary"
:label="getApplicationName(grpApp.application)"
:label="applicationStore.getApplicationName(grpApp.application)"
:title="t('inherited_from', { parent: grpApp.group })"
class="on-left"
/>
Expand Down Expand Up @@ -270,10 +270,6 @@ function onSaved() {
refresh();
}
function getApplicationName(id: string) {
return applicationStore.applications?.find((app) => app.id === id)?.name;
}
function onAccount(row: UserDto) {
window.open(row.accountUrl, '_blank');
}
Expand Down
8 changes: 8 additions & 0 deletions agate-ui/src/i18n/en/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,13 @@ export default {
ANY: 'Email or mobile app',
},
},
ticket: {
events: 'Events',
expires: 'Expires',
login_app: 'Login application',
remove: 'Remove ticket',
remove_confirm: 'Please confirm ticket removal: {id} of {username}',
},
add: 'Add',
administration: 'Administration',
applications_caption: 'Manage applications, identifications and identity providers',
Expand Down Expand Up @@ -171,6 +178,7 @@ export default {
source_code: 'Source Code',
status: 'Status',
tickets: 'Tickets',
username: 'Username',
users_caption: 'Manage users, assign role and groups to grant applications access',
users: 'Users',
};
8 changes: 8 additions & 0 deletions agate-ui/src/i18n/fr/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,13 @@ export default {
ANY: 'Courriel ou application mobile',
},
},
ticket: {
events: 'Événements',
expires: 'Expire',
login_app: 'Application de connexion',
remove: 'Supprimer le ticket',
remove_confirm: 'Veuillez confirmer la suppression du ticket: {id} de {username}',
},
add: 'ajouter',
administration: 'Administration',
applications_caption: "Gérer les applications, les identifications et les fournisseurs d'identité",
Expand Down Expand Up @@ -171,6 +178,7 @@ export default {
source_code: 'Code source',
status: 'Statut',
tickets: 'Tickets',
username: 'Nom d\'usager',
users_caption: 'Gérer les utilisateurs, les rôles et les accès aux applications',
users: 'Utilisateurs',
};
19 changes: 19 additions & 0 deletions agate-ui/src/pages/TicketsPage.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<template>
<div>
<q-toolbar class="bg-grey-3">
<q-breadcrumbs>
<q-breadcrumbs-el icon="home" to="/" />
<q-breadcrumbs-el :label="t('tickets')" />
</q-breadcrumbs>
</q-toolbar>
<q-page class="q-pa-md">
<tickets-table />
</q-page>
</div>
</template>

<script setup lang="ts">
import TicketsTable from 'src/components/TicketsTable.vue';
const { t } = useI18n();
</script>
1 change: 1 addition & 0 deletions agate-ui/src/router/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const routes: RouteRecordRaw[] = [
{ path: 'groups', component: () => import('pages/GroupsPage.vue') },
{ path: 'applications', component: () => import('pages/ApplicationsPage.vue') },
{ path: 'realms', component: () => import('pages/RealmsPage.vue') },
{ path: 'tickets', component: () => import('pages/TicketsPage.vue') },
{ path: 'settings', component: () => import('pages/SettingsPage.vue') },
],
},
Expand Down
6 changes: 6 additions & 0 deletions agate-ui/src/stores/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,17 @@ export const useApplicationStore = defineStore('application', () => {
return key.join('');
}


function getApplicationName(id: string | undefined) {
return applications.value?.find((app) => app.id === id)?.name;
}

return {
applications,
init,
save,
remove,
generateKey,
getApplicationName,
};
});
26 changes: 26 additions & 0 deletions agate-ui/src/stores/tickets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { defineStore } from 'pinia';
import { api } from 'src/boot/api';
import type { TicketDto } from 'src/models/Agate';

export const useTicketStore = defineStore('ticket', () => {
const tickets = ref<TicketDto[]>([]);

async function init() {
return api.get('/tickets').then((response) => {
if (response.status === 200) {
tickets.value = response.data;
}
return response;
});
}

async function remove(ticket: TicketDto) {
return api.delete(`/tickets/${ticket.id}`);
}

return {
tickets,
init,
remove,
};
});

0 comments on commit bbfaa4c

Please sign in to comment.