Skip to content

Commit

Permalink
feat(jobs): resource exports UI (V4-1114, V4-1163, V4-1164)
Browse files Browse the repository at this point in the history
  • Loading branch information
lukashroch committed May 16, 2024
1 parent b939c6e commit c256fc8
Show file tree
Hide file tree
Showing 31 changed files with 591 additions and 29 deletions.
4 changes: 2 additions & 2 deletions apps/admin/src/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,15 @@
<v-list-item-title>{{ $t('user.profile') }}</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item link :to="{ name: 'user-personal-access-tokens' }">
<v-list-item link :to="{ name: 'user.personal-access-tokens' }">
<v-list-item-action>
<v-icon>fas fa-key</v-icon>
</v-list-item-action>
<v-list-item-content>
<v-list-item-title>{{ $t('user.personalAccessTokens.title') }}</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item link :to="{ name: 'user-jobs' }">
<v-list-item link :to="{ name: 'user.jobs' }">
<v-list-item-action>
<v-icon>$jobs</v-icon>
</v-list-item-action>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,6 @@ export default defineComponent({
computed: {
currentActions(): string[] {
if (this.module === 'user')
return this.actions;
const { ownerId, securables } = this.item;
return this.actions.filter((item) => {
const action = item === 'download' ? 'read' : item;
Expand Down
10 changes: 6 additions & 4 deletions apps/admin/src/components/jobs/params/resource-export.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
v-model="params.resource"
:error-messages="errors.get('params.resource')"
hide-details="auto"
:items="resourceItems"
:items="items"
:label="$t('jobs.types.ResourceExport.resource')"
name="resource"
outlined
Expand All @@ -23,8 +23,9 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { useUser } from '@intake24/admin/stores';
import { type JobParams, resources } from '@intake24/common/types';
import { useI18n } from '@intake24/i18n/index';
import { useI18n } from '@intake24/i18n';
import jobParams from './job-params';
Expand All @@ -35,14 +36,15 @@ export default defineComponent({
setup() {
const { i18n } = useI18n();
const { can } = useUser();
const resourceItems = resources.map(value => ({
const items = resources.filter(item => can(`${item.split('.')[0]}|browse`)).map(value => ({
text: i18n.t(`${value}.title`).toString(),
value,
}));
return {
resourceItems,
items,
};
},
});
Expand Down
2 changes: 1 addition & 1 deletion apps/admin/src/components/jobs/use-polls-for-jobs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { JobType } from '@intake24/common/types';
import type { JobAttributes } from '@intake24/common/types/http/admin';
import { useHttp } from '@intake24/admin/services';

export function usePollsForJobs(jobType: JobType | JobType[], query?: ComputedRef<Record<string, string | number>>) {
export function usePollsForJobs(jobType: JobType | readonly JobType[], query?: ComputedRef<Record<string, string | number>>) {
const http = useHttp();

const dialog = ref<boolean>(false);
Expand Down
19 changes: 13 additions & 6 deletions apps/admin/src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,22 +132,29 @@ const routes: RouteConfig[] = [
},
{
path: '/user/jobs',
name: 'user-jobs',
name: 'user.jobs',
component: views.user.jobs.browse,
meta: { module: { current: 'user' }, title: 'user.jobs._' },
meta: { module: { current: 'user.jobs' }, title: 'user.jobs._' },
},
{
path: '/user/jobs/create',
name: 'user.jobs-create',
component: views.user.jobs.create,
meta: { module: { current: 'user.jobs' }, title: 'user.jobs._' },
props: true,
},
{
path: '/user/jobs/:id',
name: 'user-jobs-read',
name: 'user.jobs-read',
component: views.user.jobs.read,
meta: { module: { current: 'user' }, title: 'user.jobs._' },
meta: { module: { current: 'user.jobs' }, title: 'user.jobs._' },
props: true,
},
{
path: '/user/personal-access-tokens',
name: 'user-personal-access-tokens',
name: 'user.personal-access-tokens',
component: views.user.personalAccessTokens.browse,
meta: { module: { current: 'user' }, title: 'user.personalAccessTokens._' },
meta: { module: { current: 'user.personal-access-tokens' }, title: 'user.personalAccessTokens._' },
},
// Food databases explorer
{
Expand Down
2 changes: 1 addition & 1 deletion apps/admin/src/router/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const routes: string[] = ['create', 'read', 'edit'];
export const resources: Resource[] = [
{
group: 'user',
name: 'user',
name: 'user.jobs',
icon: 'fas fa-circle-user',
api: 'admin/user/jobs',
generateRoutes: false,
Expand Down
3 changes: 3 additions & 0 deletions apps/admin/src/stores/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ export const useUser = defineStore('user', {
const { name } = useResource();
const { resource = name, action, ownerId, securables = [] } = permission;

if (resource.startsWith('user.'))
return true;

if (action) {
if (this.permissions.includes(`${resource}|${action}`))
return true;
Expand Down
2 changes: 1 addition & 1 deletion apps/admin/src/views/locales/tasks/browse.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
</v-col>
</v-row>
<v-row>
<v-col class="px-6" cols="6">
<v-col class="px-6" cols="12" md="6">
<v-btn
block
color="primary"
Expand Down
2 changes: 1 addition & 1 deletion apps/admin/src/views/nutrient-tables/tasks/browse.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
</v-col>
</v-row>
<v-row>
<v-col class="px-6" cols="6">
<v-col class="px-6" cols="12" md="6">
<v-btn
block
color="primary"
Expand Down
2 changes: 1 addition & 1 deletion apps/admin/src/views/surveys/tasks/browse.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
</v-col>
</v-row>
<v-row>
<v-col class="px-6" cols="6">
<v-col class="px-6" cols="12" md="6">
<v-btn
block
color="primary"
Expand Down
2 changes: 1 addition & 1 deletion apps/admin/src/views/user/jobs/browse.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<h2 class="mb-4">
{{ $t('user.profile') }}
</h2>
<data-table :actions="['download', 'read']" api-url="admin/user/jobs" :headers="headers">
<data-table :actions="['create', 'download', 'read']" api-url="admin/user/jobs" :headers="headers">
<template #[`item.userId`]="{ item }">
{{ item.user?.email ?? item.userId }}
</template>
Expand Down
153 changes: 153 additions & 0 deletions apps/admin/src/views/user/jobs/create.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
<template>
<div>
<v-card class="mb-4" outlined>
<v-toolbar flat>
<v-btn color="white" :title="$t(`common.action.back`)" :to="{ name: 'user.jobs' }">
<v-icon left>
$back
</v-icon>{{ $t(`common.action.back`) }}
</v-btn>
</v-toolbar>
</v-card>
<v-card :flat="isMobile" :outlined="!isMobile" :tile="isMobile">
<v-tabs background-color="primary" dark>
<v-tab :key="$t('user.jobs.create')" :title="$t('user.jobs.create')">
{{ $t('user.jobs.create') }}
</v-tab>
</v-tabs>
<v-container fluid>
<v-form @keydown.native="clearError" @submit.prevent="submit">
<v-row>
<v-col cols="12" md="6">
<v-card-title>{{ $t('user.jobs.title') }}</v-card-title>
<v-card-text>
<v-select
v-model="form.type"
hide-details="auto"
:items="jobTypeList"
:label="$t('user.jobs._')"
name="job"
outlined
prepend-inner-icon="$jobs"
@change="updateJob"
/>
</v-card-text>
</v-col>
<v-col cols="12" md="6">
<component
:is="form.type"
v-if="Object.keys(form.params).length"
v-model="form.params"
:disabled="disabledJobParams[form.type]"
:errors="form.errors"
name="params"
@input="form.errors.clear(paramErrors)"
/>
</v-col>
</v-row>
<v-row>
<v-col class="px-6" cols="12" md="6">
<v-btn
block
color="primary"
:disabled="form.errors.any() || jobInProgress || isAppLoading"
:title="$t('common.action.upload')"
type="submit"
x-large
>
<v-icon left>
fas fa-play
</v-icon>{{ $t('common.action.submit') }}
</v-btn>
</v-col>
</v-row>
<polls-job-list v-bind="{ jobs }" />
</v-form>
</v-container>
</v-card>
</div>
</template>

<script lang="ts">
import { computed, defineComponent, onMounted } from 'vue';
import type { GetJobParams, JobParams, UserJob } from '@intake24/common/types';
import type { JobAttributes } from '@intake24/common/types/http/admin';
import { formMixin } from '@intake24/admin/components/entry';
import { jobParams, PollsJobList, usePollsForJobs } from '@intake24/admin/components/jobs';
import { useForm } from '@intake24/admin/composables';
import { userJobs } from '@intake24/common/types';
import { useI18n } from '@intake24/i18n';
type UserJobForm = {
type: UserJob;
params: GetJobParams<UserJob>;
};
export default defineComponent({
name: 'UserJobsSubmit',
components: { ...jobParams, PollsJobList },
mixins: [formMixin],
setup(_props) {
const { i18n } = useI18n();
const jobTypeList = computed(() =>
userJobs.map(value => ({ value, text: i18n.t(`jobs.types.${value}._`) })),
);
const defaultJobsParams = computed<Pick<JobParams, UserJob>>(() => ({
ResourceExport: { resource: 'as-served-sets' },
}));
const disabledJobParams = {
ResourceExport: {},
};
const { clearError, form } = useForm<UserJobForm>({
data: { type: userJobs[0], params: defaultJobsParams.value[userJobs[0]] },
config: { resetOnSubmit: false },
});
const { jobs, jobInProgress, startPolling } = usePollsForJobs(userJobs);
const paramErrors = computed(() => Object.keys(form.params).map(key => `params.${key}`));
onMounted(async () => {
await startPolling(true);
});
const updateJob = () => {
form.errors.clear();
form.params = defaultJobsParams.value[form.type];
};
const submit = async () => {
if (jobInProgress.value)
return;
const job = await form.post<JobAttributes>(`admin/user/jobs`);
jobs.value.unshift(job);
await startPolling();
};
return {
defaultJobsParams,
disabledJobParams,
jobTypeList,
clearError,
paramErrors,
form,
jobs,
jobInProgress,
startPolling,
submit,
updateJob,
};
},
});
</script>

<style lang="scss" scoped></style>
3 changes: 2 additions & 1 deletion apps/admin/src/views/user/jobs/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import browse from './browse.vue';
import create from './create.vue';
import read from './read.vue';

export default { browse, read };
export default { browse, create, read };
1 change: 1 addition & 0 deletions apps/api/__tests__/integration/admin/user/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export default () => {
describe('get /api/admin/user/verify', verify);

describe('get /api/admin/user/jobs', jobs.browse);
describe('post /api/admin/user/jobs', jobs.submit);
describe('get /api/admin/user/jobs/:jobId', jobs.read);
describe('get /api/admin/user/jobs/:jobId/download', jobs.download);

Expand Down
2 changes: 2 additions & 0 deletions apps/api/__tests__/integration/admin/user/jobs/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import browse from './browse.test';
import download from './download.test';
import read from './read.test';
import submit from './submit.test';

export default {
browse,
read,
download,
submit,
};
48 changes: 48 additions & 0 deletions apps/api/__tests__/integration/admin/user/jobs/submit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { UserJobRequest } from '@intake24/common/types/http/admin';
import { suite } from '@intake24/api-tests/integration/helpers';

export default () => {
const url = '/api/admin/user/jobs';
const permissions = ['as-served-sets|browse'];

const input: UserJobRequest = {
type: 'ResourceExport',
params: {
resource: 'as-served-sets',
},
};

it('missing authentication / authorization', async () => {
await suite.sharedTests.assertMissingAuthentication('post', url);
});

describe('authenticated / resource authorized', () => {
it('should return 400 for missing input data', async () => {
await suite.sharedTests.assertInvalidInput('post', url, ['type', 'params']);
});

it('should return 400 for invalid input data', async () => {
await suite.sharedTests.assertInvalidInput(
'post',
url,
['type', 'params'],
{
input: {
type: 'not-a-job-type',
params: 'invalid-params',
},
},
);
});

it('should return 403 when missing permissions', async () => {
await suite.sharedTests.assertMissingAuthorization('post', url, { input });
});

it('should return 200 and data', async () => {
// await suite.util.setPermission('as-served-sets|browse');
await suite.util.setPermission(permissions);
await suite.sharedTests.assertAcknowledged('post', url, { input, result: true });
});
});
};

0 comments on commit c256fc8

Please sign in to comment.