Skip to content

Commit

Permalink
feat: application table, add, remove and update (#577)
Browse files Browse the repository at this point in the history
  • Loading branch information
ymarcon authored Jan 3, 2025
1 parent 59c0f51 commit 633b538
Show file tree
Hide file tree
Showing 8 changed files with 396 additions and 16 deletions.
165 changes: 165 additions & 0 deletions agate-ui/src/components/ApplicationDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
<template>
<q-dialog v-model="showDialog" persistent @hide="onHide">
<q-card class="dialog-md">
<q-card-section>
<div class="text-h6">{{ t(editMode ? 'application.edit' : 'application.add') }}</div>
</q-card-section>

<q-separator />

<q-card-section>
<q-form>
<q-input
v-model="selected.name"
:label="t('name') + ' *'"
:hint="t('name_hint')"
:disable="editMode"
dense
lazy-rules
:rules="[
(val) => !!val || t('name_required'),
(val) => (val?.trim().length ?? 0) >= 3 || t('name_min_length', { min: 3 }),
]"
/>
<q-input v-model="selected.description" :label="t('description')" dense class="q-mb-md" />
<q-input
v-model="key"
:label="t('application.key') + (editMode ? '' : ' *')"
:hint="t(editMode ? 'application.key_hint_edit' : 'application.key_hint')"
dense
lazy-rules
:rules="
editMode
? []
: [
(val) => !!val || t('application.key_required'),
(val) => (val?.trim().length ?? 0) >= 8 || t('application.key_min_length', { min: 8 }),
]
"
class="q-mb-md"
>
<template v-slot:after>
<q-btn
round
dense
size="sm"
:title="t('application.copy_key')"
flat
icon="content_copy"
@click="copyKeyToClipboard"
/>
<q-btn
round
dense
size="sm"
:title="t('application.generate_key')"
flat
icon="lock_reset"
@click="generateKey"
/>
</template>
</q-input>
<q-input
v-model="selected.redirectURI"
:label="t('application.redirect_uris')"
:hint="t('application.redirect_uris_hint')"
dense
class="q-mb-md"
/>
<q-checkbox v-model="selected.autoApproval" :label="t('application.auto_approval')" dense class="q-mb-xs" />
<div class="text-hint q-mb-md">
{{ t('application.auto_approval_hint') }}
</div>
</q-form>
</q-card-section>

<q-separator />

<q-card-actions align="right" class="bg-grey-3">
<q-btn flat :label="t('cancel')" color="secondary" @click="onCancel" v-close-popup />
<q-btn flat :label="t('save')" :disable="!isValid" color="primary" @click="onSave" />
</q-card-actions>
</q-card>
</q-dialog>
</template>

<script setup lang="ts">
import type { ApplicationDto } from 'src/models/Agate';
import { notifyError, notifyInfo, notifySuccess } from 'src/utils/notify';
import { copyToClipboard } from 'quasar';
const { t } = useI18n();
const applicationStore = useApplicationStore();
interface DialogProps {
modelValue: boolean;
application: ApplicationDto | undefined;
}
const props = defineProps<DialogProps>();
const emit = defineEmits(['update:modelValue', 'saved', 'cancel']);
const showDialog = ref(props.modelValue);
const selected = ref<ApplicationDto>(props.application ?? ({ autoApproval: true } as ApplicationDto));
const editMode = ref(false);
const key = ref('');
const isValid = computed(
() =>
selected.value.name &&
selected.value.name.trim().length >= 3 &&
(editMode || (key.value && key.value.trim().length >= 8)),
);
onMounted(() => {
applicationStore.init();
});
watch(
() => props.modelValue,
(value) => {
showDialog.value = value;
selected.value = props.application ? { ...props.application } : ({ autoApproval: true } as ApplicationDto);
editMode.value = props.application !== undefined;
key.value = '';
},
);
function onHide() {
emit('update:modelValue', false);
}
function onCancel() {
emit('cancel');
}
function onSave() {
if (key.value) {
selected.value.key = key.value;
}
applicationStore
.save(selected.value)
.then(() => {
notifySuccess(t('application.saved'));
emit('saved');
})
.catch(() => {
notifyError(t('application.save_failed'));
})
.finally(() => {
emit('update:modelValue', false);
});
}
function generateKey() {
key.value = applicationStore.generateKey();
}
function copyKeyToClipboard() {
if (key.value) {
copyToClipboard(key.value).then(() => {
notifyInfo(t('application.key_copied'));
});
}
}
</script>
143 changes: 143 additions & 0 deletions agate-ui/src/components/ApplicationsTable.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
<template>
<div>
<q-table :rows="applications" flat row-key="name" :columns="columns" :pagination="initialPagination">
<template v-slot:top-left>
<q-btn size="sm" icon="add" color="primary" :label="t('add')" @click="onAdd" />
</template>
<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">
<code>{{ props.row.id }}</code>
</q-td>
<q-td key="name" :props="props">
<span class="text-primary">{{ props.row.name }}</span>
<div class="float-right">
<q-btn
rounded
dense
flat
size="sm"
color="secondary"
:icon="toolsVisible[props.row.name] ? 'edit' : 'none'"
:title="t('edit')"
class="q-ml-xs"
@click="onShowEdit(props.row)"
/>
<q-btn
v-if="!props.row.hasDatasource"
rounded
dense
flat
size="sm"
color="secondary"
:title="t('delete')"
:icon="toolsVisible[props.row.name] ? 'delete' : 'none'"
class="q-ml-xs"
@click="onShowDelete(props.row)"
/>
</div>
</q-td>
<q-td key="description" :props="props">
<span>{{ props.row.description }}</span>
</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" />
</template>
</q-td>
</q-tr>
</template>
</q-table>
<confirm-dialog
v-model="showDelete"
:title="t('application.remove')"
:text="t('application.remove_confirm', { name: selected?.name })"
@confirm="onDelete"
/>
<application-dialog v-model="showEdit" :application="selected" @saved="onSaved" />
</div>
</template>

<script setup lang="ts">
import type { ApplicationDto } from 'src/models/Agate';
import ApplicationDialog from 'src/components/ApplicationDialog.vue';
import ConfirmDialog from 'src/components/ConfirmDialog.vue';
import { DefaultAlignment } from 'src/components/models';
const { t } = useI18n();
const applicationStore = useApplicationStore();
const filter = ref('');
const toolsVisible = ref<{ [key: string]: boolean }>({});
const initialPagination = ref({
descending: false,
page: 1,
rowsPerPage: 20,
});
const showEdit = ref(false);
const showDelete = ref(false);
const selected = ref();
const applications = computed(
() =>
applicationStore.applications?.filter((app) =>
filter.value ? app.name.toLowerCase().includes(filter.value.toLowerCase()) : true,
) || [],
);
const columns = computed(() => [
{ name: 'id', label: 'ID', field: 'id', align: DefaultAlignment },
{ name: 'name', label: t('name'), field: 'name', align: DefaultAlignment },
{ name: 'description', label: t('description'), field: 'description', align: DefaultAlignment },
]);
onMounted(() => {
applicationStore.init();
refresh();
});
function refresh() {
applicationStore.init();
}
function onOverRow(row: ApplicationDto) {
toolsVisible.value[row.name] = true;
}
function onLeaveRow(row: ApplicationDto) {
toolsVisible.value[row.name] = false;
}
function onShowEdit(row: ApplicationDto) {
selected.value = row;
showEdit.value = true;
}
function onShowDelete(row: ApplicationDto) {
selected.value = row;
showDelete.value = true;
}
function onDelete() {
applicationStore.remove(selected.value).finally(refresh);
}
function onAdd() {
selected.value = undefined;
showEdit.value = true;
}
function onSaved() {
refresh();
}
function getApplicationName(id: string) {
return applicationStore.applications?.find((app) => app.id === id)?.name;
}
</script>
5 changes: 3 additions & 2 deletions agate-ui/src/components/GroupDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<q-input
v-model="selected.name"
:label="t('name') + ' *'"
:hint="t('name_hint')"
:disable="editMode"
dense
lazy-rules
Expand Down Expand Up @@ -56,14 +57,14 @@ const applicationStore = useApplicationStore();
interface DialogProps {
modelValue: boolean;
group: GroupDto;
group: GroupDto | undefined;
}
const props = defineProps<DialogProps>();
const emit = defineEmits(['update:modelValue', 'saved', 'cancel']);
const showDialog = ref(props.modelValue);
const selected = ref<GroupDto>(props.group);
const selected = ref<GroupDto>(props.group ?? ({} as GroupDto));
const editMode = ref(false);
const applicationOptions = computed(
Expand Down
7 changes: 0 additions & 7 deletions agate-ui/src/components/GroupsTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,6 @@
<q-tr :props="props" @mouseover="onOverRow(props.row)" @mouseleave="onLeaveRow(props.row)">
<q-td key="name" :props="props">
<span class="text-primary">{{ props.row.name }}</span>
<q-icon
v-if="props.row.defaultStorage"
name="check"
size="sm"
class="on-right"
:title="t('default_storage')"
/>
<div class="float-right">
<q-btn
rounded
Expand Down
20 changes: 20 additions & 0 deletions agate-ui/src/i18n/en/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,26 @@ export default {
main: {
powered_by: 'Powered by',
},
application: {
add: 'Add application',
edit: 'Edit application',
remove: 'Remove application',
remove_confirm: 'Please confirm application removal: {name}',
saved: 'Application saved',
save_failed: 'Failed to save application',
key: 'Key',
copy_key: 'Copy key',
generate_key: 'Generate key',
redirect_uris: 'Redirect URIs',
redirect_uris_hint: "Callback URL to the application's server, required in the OAuth context. Use commas to separate multiple allowed callback URLs.",
key_required: 'Key is required',
key_hint: 'This key is used to authenticate the application with the API.',
key_hint_edit: "Leave blank to not modify application's secret key.",
key_min_length: 'Key must be at least {min} characters',
key_copied: 'Key copied',
auto_approval: 'User approved on sign up',
auto_approval_hint: 'Automatically approve a user who signed up through the application. Otherwise the user will be in "Pending" state, requiring manual approval.',
},
group: {
add: 'Add group',
applications_hint: 'Members of a group get access to the applications associated to this group.',
Expand Down
Loading

0 comments on commit 633b538

Please sign in to comment.