mirror of
https://github.com/unraid/api.git
synced 2026-01-02 14:40:01 -06:00
Compare commits
2 Commits
v4.29.0
...
codex/upda
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
291fb24e16 | ||
|
|
53851e4c9d |
@@ -40,6 +40,11 @@ export class RCloneMutations {
|
||||
deleteRCloneRemote!: boolean;
|
||||
}
|
||||
|
||||
@ObjectType({
|
||||
description: 'Notification related mutations',
|
||||
})
|
||||
export class NotificationMutations {}
|
||||
|
||||
@ObjectType()
|
||||
export class RootMutations {
|
||||
@Field(() => ArrayMutations, { description: 'Array related mutations' })
|
||||
@@ -59,4 +64,7 @@ export class RootMutations {
|
||||
|
||||
@Field(() => RCloneMutations, { description: 'RClone related mutations' })
|
||||
rclone: RCloneMutations = new RCloneMutations();
|
||||
|
||||
@Field(() => NotificationMutations, { description: 'Notification related mutations' })
|
||||
notifications: NotificationMutations = new NotificationMutations();
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
ApiKeyMutations,
|
||||
ArrayMutations,
|
||||
DockerMutations,
|
||||
NotificationMutations,
|
||||
ParityCheckMutations,
|
||||
RCloneMutations,
|
||||
RootMutations,
|
||||
@@ -41,4 +42,9 @@ export class RootMutationsResolver {
|
||||
rclone(): RCloneMutations {
|
||||
return new RCloneMutations();
|
||||
}
|
||||
|
||||
@Mutation(() => NotificationMutations, { name: 'notifications' })
|
||||
notifications(): NotificationMutations {
|
||||
return new NotificationMutations();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Field, InputType, Int, ObjectType, registerEnumType } from '@nestjs/graphql';
|
||||
import { Field, ID, InputType, Int, ObjectType, registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
import { Node } from '@unraid/shared/graphql.model.js';
|
||||
import { IsEnum, IsInt, IsNotEmpty, IsOptional, IsString, Min } from 'class-validator';
|
||||
@@ -14,6 +14,18 @@ export enum NotificationImportance {
|
||||
WARNING = 'WARNING',
|
||||
}
|
||||
|
||||
export enum NotificationJobState {
|
||||
QUEUED = 'QUEUED',
|
||||
RUNNING = 'RUNNING',
|
||||
SUCCEEDED = 'SUCCEEDED',
|
||||
FAILED = 'FAILED',
|
||||
}
|
||||
|
||||
export enum NotificationJobOperation {
|
||||
ARCHIVE_ALL = 'ARCHIVE_ALL',
|
||||
DELETE = 'DELETE',
|
||||
}
|
||||
|
||||
// Register enums with GraphQL
|
||||
registerEnumType(NotificationType, {
|
||||
name: 'NotificationType',
|
||||
@@ -23,6 +35,14 @@ registerEnumType(NotificationImportance, {
|
||||
name: 'NotificationImportance',
|
||||
});
|
||||
|
||||
registerEnumType(NotificationJobState, {
|
||||
name: 'NotificationJobState',
|
||||
});
|
||||
|
||||
registerEnumType(NotificationJobOperation, {
|
||||
name: 'NotificationJobOperation',
|
||||
});
|
||||
|
||||
@InputType('NotificationFilter')
|
||||
export class NotificationFilter {
|
||||
@Field(() => NotificationImportance, { nullable: true })
|
||||
@@ -164,4 +184,28 @@ export class Notifications extends Node {
|
||||
@Field(() => [Notification])
|
||||
@IsNotEmpty()
|
||||
list!: Notification[];
|
||||
|
||||
@Field(() => NotificationJob, { nullable: true })
|
||||
job?: NotificationJob;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class NotificationJob {
|
||||
@Field(() => ID)
|
||||
id!: string;
|
||||
|
||||
@Field(() => NotificationJobOperation)
|
||||
operation!: NotificationJobOperation;
|
||||
|
||||
@Field(() => NotificationJobState)
|
||||
state!: NotificationJobState;
|
||||
|
||||
@Field(() => Int)
|
||||
processed!: number;
|
||||
|
||||
@Field(() => Int)
|
||||
total!: number;
|
||||
|
||||
@Field({ nullable: true })
|
||||
error?: string | null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Args, ResolveField, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { AuthAction, Resource } from '@unraid/shared/graphql.model.js';
|
||||
import { PrefixedID } from '@unraid/shared/prefixed-id-scalar.js';
|
||||
import { UsePermissions } from '@unraid/shared/use-permissions.directive.js';
|
||||
|
||||
import { AppError } from '@app/core/errors/app-error.js';
|
||||
import {
|
||||
NotificationImportance,
|
||||
NotificationJob,
|
||||
NotificationJobState,
|
||||
NotificationMutations,
|
||||
NotificationOverview,
|
||||
NotificationType,
|
||||
} from '@app/unraid-api/graph/resolvers/notifications/notifications.model.js';
|
||||
import { NotificationsService } from '@app/unraid-api/graph/resolvers/notifications/notifications.service.js';
|
||||
|
||||
@Resolver(() => NotificationMutations)
|
||||
export class NotificationMutationsResolver {
|
||||
constructor(private readonly notificationsService: NotificationsService) {}
|
||||
|
||||
@ResolveField(() => NotificationOverview)
|
||||
@UsePermissions({
|
||||
action: AuthAction.DELETE_ANY,
|
||||
resource: Resource.NOTIFICATIONS,
|
||||
})
|
||||
public async delete(
|
||||
@Args('id', { type: () => PrefixedID }) id: string,
|
||||
@Args('type', { type: () => NotificationType }) type: NotificationType
|
||||
): Promise<NotificationOverview> {
|
||||
const { overview } = await this.notificationsService.deleteNotification({ id, type });
|
||||
return overview;
|
||||
}
|
||||
|
||||
@ResolveField(() => NotificationJob)
|
||||
@UsePermissions({
|
||||
action: AuthAction.DELETE_ANY,
|
||||
resource: Resource.NOTIFICATIONS,
|
||||
})
|
||||
public async startArchiveAll(
|
||||
@Args('importance', { type: () => NotificationImportance, nullable: true })
|
||||
importance?: NotificationImportance
|
||||
): Promise<NotificationJob> {
|
||||
return this.notificationsService.startArchiveAllJob(importance);
|
||||
}
|
||||
|
||||
@ResolveField(() => NotificationJob)
|
||||
@UsePermissions({
|
||||
action: AuthAction.DELETE_ANY,
|
||||
resource: Resource.NOTIFICATIONS,
|
||||
})
|
||||
public async startDeleteAll(
|
||||
@Args('type', { type: () => NotificationType, nullable: true }) type?: NotificationType
|
||||
): Promise<NotificationJob> {
|
||||
const job = await this.notificationsService.startDeleteAllJob(type);
|
||||
if (job.state === NotificationJobState.FAILED) {
|
||||
throw new AppError(job.error ?? 'Failed to delete notifications', 500);
|
||||
}
|
||||
return job;
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
NotificationData,
|
||||
NotificationFilter,
|
||||
NotificationImportance,
|
||||
NotificationJob,
|
||||
NotificationOverview,
|
||||
Notifications,
|
||||
NotificationType,
|
||||
@@ -41,6 +42,11 @@ export class NotificationsResolver {
|
||||
return this.notificationsService.getOverview();
|
||||
}
|
||||
|
||||
@ResolveField(() => NotificationJob, { nullable: true })
|
||||
public job(@Args('id') jobId: string): NotificationJob {
|
||||
return this.notificationsService.getJob(jobId);
|
||||
}
|
||||
|
||||
@ResolveField(() => [Notification])
|
||||
public async list(
|
||||
@Args('filter', { type: () => NotificationFilter })
|
||||
|
||||
@@ -24,10 +24,14 @@ import {
|
||||
NotificationData,
|
||||
NotificationFilter,
|
||||
NotificationImportance,
|
||||
NotificationJob,
|
||||
NotificationJobOperation,
|
||||
NotificationJobState,
|
||||
NotificationOverview,
|
||||
NotificationType,
|
||||
} from '@app/unraid-api/graph/resolvers/notifications/notifications.model.js';
|
||||
import { validateObject } from '@app/unraid-api/graph/resolvers/validation.utils.js';
|
||||
import { BackgroundJobsService } from '@app/unraid-api/graph/services/background-jobs.service.js';
|
||||
import { SortFn } from '@app/unraid-api/types/util.js';
|
||||
import { batchProcess, formatDatetime, isFulfilled, isRejected, unraidTimestamp } from '@app/utils.js';
|
||||
|
||||
@@ -55,7 +59,36 @@ export class NotificationsService {
|
||||
},
|
||||
};
|
||||
|
||||
constructor() {
|
||||
private createJob(operation: NotificationJobOperation, total: number): NotificationJob {
|
||||
const state = total === 0 ? NotificationJobState.SUCCEEDED : NotificationJobState.QUEUED;
|
||||
return this.backgroundJobs.createJob<NotificationJobOperation, NotificationJobState>({
|
||||
operation,
|
||||
total,
|
||||
initialState: state,
|
||||
prefix: operation.toLowerCase(),
|
||||
});
|
||||
}
|
||||
|
||||
private updateJob(job: NotificationJob) {
|
||||
this.backgroundJobs.updateJob(job);
|
||||
}
|
||||
|
||||
private async runJob(job: NotificationJob, work: () => Promise<void>) {
|
||||
await this.backgroundJobs.runJob<NotificationJobState>({
|
||||
job,
|
||||
work,
|
||||
runningState: NotificationJobState.RUNNING,
|
||||
successState: NotificationJobState.SUCCEEDED,
|
||||
failureState: NotificationJobState.FAILED,
|
||||
logContext: 'notification-job',
|
||||
});
|
||||
}
|
||||
|
||||
public getJob(jobId: string): NotificationJob {
|
||||
return this.backgroundJobs.getJob<NotificationJobOperation, NotificationJobState>(jobId);
|
||||
}
|
||||
|
||||
constructor(private readonly backgroundJobs: BackgroundJobsService) {
|
||||
this.path = getters.dynamix().notify!.path;
|
||||
void this.getNotificationsWatcher(this.path);
|
||||
}
|
||||
@@ -323,6 +356,30 @@ export class NotificationsService {
|
||||
return this.getOverview();
|
||||
}
|
||||
|
||||
public async startDeleteAllJob(type?: NotificationType): Promise<NotificationJob> {
|
||||
const targets = type ? [type] : [NotificationType.ARCHIVE, NotificationType.UNREAD];
|
||||
const queue: Array<{ id: string; type: NotificationType }> = [];
|
||||
|
||||
for (const targetType of targets) {
|
||||
const ids = await this.listFilesInFolder(this.paths()[targetType]);
|
||||
ids.forEach((id) => queue.push({ id, type: targetType }));
|
||||
}
|
||||
|
||||
const job = this.createJob(NotificationJobOperation.DELETE, queue.length);
|
||||
|
||||
setImmediate(() =>
|
||||
this.runJob(job, async () => {
|
||||
for (const entry of queue) {
|
||||
await this.deleteNotification({ id: entry.id, type: entry.type });
|
||||
job.processed += 1;
|
||||
this.updateJob(job);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return job;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all notifications from disk while preserving the directory structure.
|
||||
* Resets overview stats to zero.
|
||||
@@ -485,6 +542,35 @@ export class NotificationsService {
|
||||
return { ...stats, overview: overviewSnapshot };
|
||||
}
|
||||
|
||||
public async startArchiveAllJob(importance?: NotificationImportance): Promise<NotificationJob> {
|
||||
const { UNREAD } = this.paths();
|
||||
const unreads = await this.listFilesInFolder(UNREAD);
|
||||
const [notifications] = await this.loadNotificationsFromPaths(
|
||||
unreads,
|
||||
importance ? { importance } : {}
|
||||
);
|
||||
const job = this.createJob(NotificationJobOperation.ARCHIVE_ALL, notifications.length);
|
||||
|
||||
setImmediate(() =>
|
||||
this.runJob(job, async () => {
|
||||
const overviewSnapshot = this.getOverview();
|
||||
const archive = this.moveNotification({
|
||||
from: NotificationType.UNREAD,
|
||||
to: NotificationType.ARCHIVE,
|
||||
snapshot: overviewSnapshot,
|
||||
});
|
||||
|
||||
for (const notification of notifications) {
|
||||
await archive(notification);
|
||||
job.processed += 1;
|
||||
this.updateJob(job);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return job;
|
||||
}
|
||||
|
||||
public async unarchiveAll(importance?: NotificationImportance) {
|
||||
const { ARCHIVE } = this.paths();
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import { InfoModule } from '@app/unraid-api/graph/resolvers/info/info.module.js'
|
||||
import { LogsModule } from '@app/unraid-api/graph/resolvers/logs/logs.module.js';
|
||||
import { MetricsModule } from '@app/unraid-api/graph/resolvers/metrics/metrics.module.js';
|
||||
import { RootMutationsResolver } from '@app/unraid-api/graph/resolvers/mutation/mutation.resolver.js';
|
||||
import { NotificationMutationsResolver } from '@app/unraid-api/graph/resolvers/notifications/notifications.mutations.resolver.js';
|
||||
import { NotificationsResolver } from '@app/unraid-api/graph/resolvers/notifications/notifications.resolver.js';
|
||||
import { NotificationsService } from '@app/unraid-api/graph/resolvers/notifications/notifications.service.js';
|
||||
import { OnlineResolver } from '@app/unraid-api/graph/resolvers/online/online.resolver.js';
|
||||
@@ -29,6 +30,7 @@ import { VarsResolver } from '@app/unraid-api/graph/resolvers/vars/vars.resolver
|
||||
import { VmMutationsResolver } from '@app/unraid-api/graph/resolvers/vms/vms.mutations.resolver.js';
|
||||
import { VmsResolver } from '@app/unraid-api/graph/resolvers/vms/vms.resolver.js';
|
||||
import { VmsService } from '@app/unraid-api/graph/resolvers/vms/vms.service.js';
|
||||
import { BackgroundJobsService } from '@app/unraid-api/graph/services/background-jobs.service.js';
|
||||
import { ServicesModule } from '@app/unraid-api/graph/services/services.module.js';
|
||||
import { ServicesResolver } from '@app/unraid-api/graph/services/services.resolver.js';
|
||||
import { SharesResolver } from '@app/unraid-api/graph/shares/shares.resolver.js';
|
||||
@@ -57,8 +59,10 @@ import { MeResolver } from '@app/unraid-api/graph/user/user.resolver.js';
|
||||
ConfigResolver,
|
||||
FlashResolver,
|
||||
MeResolver,
|
||||
NotificationMutationsResolver,
|
||||
NotificationsResolver,
|
||||
NotificationsService,
|
||||
BackgroundJobsService,
|
||||
OnlineResolver,
|
||||
OwnerResolver,
|
||||
RegistrationResolver,
|
||||
|
||||
94
api/src/unraid-api/graph/services/background-jobs.service.ts
Normal file
94
api/src/unraid-api/graph/services/background-jobs.service.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { AppError } from '@app/core/errors/app-error.js';
|
||||
|
||||
export interface BackgroundJob<Operation = string, State = string> {
|
||||
id: string;
|
||||
operation: Operation;
|
||||
state: State;
|
||||
processed: number;
|
||||
total: number;
|
||||
error?: string | null;
|
||||
meta?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface CreateJobOptions<Operation, State> {
|
||||
operation: Operation;
|
||||
total: number;
|
||||
initialState: State;
|
||||
prefix?: string;
|
||||
meta?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface RunJobOptions<State> {
|
||||
job: BackgroundJob<unknown, State>;
|
||||
work: () => Promise<void>;
|
||||
runningState: State;
|
||||
successState: State;
|
||||
failureState: State;
|
||||
logContext?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class BackgroundJobsService {
|
||||
private readonly logger = new Logger(BackgroundJobsService.name);
|
||||
private readonly jobs = new Map<string, BackgroundJob>();
|
||||
|
||||
public createJob<Operation, State>(options: CreateJobOptions<Operation, State>) {
|
||||
const { operation, total, initialState, prefix, meta } = options;
|
||||
const idBase = typeof operation === 'string' ? operation.toLowerCase() : 'job';
|
||||
const id = `${prefix ?? idBase}-${Date.now().toString(36)}`;
|
||||
|
||||
const job: BackgroundJob<Operation, State> = {
|
||||
id,
|
||||
operation,
|
||||
state: initialState,
|
||||
processed: 0,
|
||||
total,
|
||||
error: null,
|
||||
meta,
|
||||
};
|
||||
|
||||
this.jobs.set(job.id, job);
|
||||
return job;
|
||||
}
|
||||
|
||||
public updateJob<Operation, State>(job: BackgroundJob<Operation, State>) {
|
||||
this.jobs.set(job.id, { ...job });
|
||||
return this.jobs.get(job.id) as BackgroundJob<Operation, State>;
|
||||
}
|
||||
|
||||
public getJob<Operation, State>(jobId: string) {
|
||||
const job = this.jobs.get(jobId) as BackgroundJob<Operation, State> | undefined;
|
||||
if (!job) {
|
||||
throw new AppError(`Background job ${jobId} not found`, 404);
|
||||
}
|
||||
return job;
|
||||
}
|
||||
|
||||
public async runJob<State>(options: RunJobOptions<State>) {
|
||||
const { job, work, runningState, successState, failureState, logContext } = options;
|
||||
if (job.state === successState) {
|
||||
return job;
|
||||
}
|
||||
|
||||
job.state = runningState;
|
||||
this.updateJob(job);
|
||||
|
||||
try {
|
||||
await work();
|
||||
job.state = successState;
|
||||
} catch (error) {
|
||||
job.state = failureState;
|
||||
job.error = error instanceof Error ? error.message : 'Unknown error while processing job';
|
||||
this.logger.error(
|
||||
`[${logContext ?? 'background-job'}] failed ${job.id}: ${job.error}`,
|
||||
error as Error
|
||||
);
|
||||
} finally {
|
||||
this.updateJob(job);
|
||||
}
|
||||
|
||||
return job;
|
||||
}
|
||||
}
|
||||
@@ -142,7 +142,6 @@ const reformattedTimestamp = computed<string>(() => {
|
||||
<span class="text-sm">{{ t('notifications.item.archive') }}</span>
|
||||
</Button>
|
||||
<Button
|
||||
v-if="type === NotificationType.ARCHIVE"
|
||||
:disabled="deleteNotification.loading"
|
||||
@click="() => deleteNotification.mutate({ id: props.id, type: props.type })"
|
||||
>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { computed, onBeforeUnmount, reactive, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useMutation, useQuery, useSubscription } from '@vue/apollo-composable';
|
||||
import { useApolloClient, useMutation, useQuery, useSubscription } from '@vue/apollo-composable';
|
||||
|
||||
import {
|
||||
Button,
|
||||
@@ -24,10 +24,12 @@ import { Settings } from 'lucide-vue-next';
|
||||
import ConfirmDialog from '~/components/ConfirmDialog.vue';
|
||||
import {
|
||||
archiveAllNotifications,
|
||||
deleteArchivedNotifications,
|
||||
NOTIFICATION_FRAGMENT,
|
||||
NOTIFICATION_JOB_FRAGMENT,
|
||||
notificationJobStatus,
|
||||
notificationsOverview,
|
||||
resetOverview,
|
||||
startDeleteNotifications,
|
||||
} from '~/components/Notifications/graphql/notification.query';
|
||||
import {
|
||||
notificationAddedSubscription,
|
||||
@@ -37,15 +39,100 @@ import NotificationsIndicator from '~/components/Notifications/Indicator.vue';
|
||||
import NotificationsList from '~/components/Notifications/List.vue';
|
||||
import { useTrackLatestSeenNotification } from '~/composables/api/use-notifications';
|
||||
import { useFragment } from '~/composables/gql';
|
||||
import { NotificationImportance as Importance, NotificationType } from '~/composables/gql/graphql';
|
||||
import {
|
||||
NotificationImportance as Importance,
|
||||
NotificationJobState,
|
||||
NotificationType,
|
||||
} from '~/composables/gql/graphql';
|
||||
import { useConfirm } from '~/composables/useConfirm';
|
||||
|
||||
const { mutate: archiveAll, loading: loadingArchiveAll } = useMutation(archiveAllNotifications);
|
||||
const { mutate: deleteArchives, loading: loadingDeleteAll } = useMutation(deleteArchivedNotifications);
|
||||
const { mutate: startArchiveAllJob, loading: loadingArchiveAll } = useMutation(archiveAllNotifications);
|
||||
const { mutate: startDeleteAll, loading: loadingDeleteAll } = useMutation(startDeleteNotifications);
|
||||
const { client } = useApolloClient();
|
||||
const { mutate: recalculateOverview } = useMutation(resetOverview);
|
||||
const { confirm } = useConfirm();
|
||||
const importance = ref<Importance | undefined>(undefined);
|
||||
|
||||
const jobPollers: Record<
|
||||
'archiveAll' | 'deleteArchived' | 'deleteUnread',
|
||||
ReturnType<typeof setInterval> | null
|
||||
> = {
|
||||
archiveAll: null,
|
||||
deleteArchived: null,
|
||||
deleteUnread: null,
|
||||
};
|
||||
|
||||
const activeJobs = reactive<
|
||||
Record<
|
||||
'archiveAll' | 'deleteArchived' | 'deleteUnread',
|
||||
{
|
||||
id: string;
|
||||
state: NotificationJobState;
|
||||
processed: number;
|
||||
total: number;
|
||||
error?: string | null;
|
||||
} | null
|
||||
>
|
||||
>({
|
||||
archiveAll: null,
|
||||
deleteArchived: null,
|
||||
deleteUnread: null,
|
||||
});
|
||||
|
||||
const activeStates = [NotificationJobState.QUEUED, NotificationJobState.RUNNING];
|
||||
|
||||
const stopPolling = (key: keyof typeof jobPollers) => {
|
||||
const interval = jobPollers[key];
|
||||
if (interval) {
|
||||
clearInterval(interval);
|
||||
jobPollers[key] = null;
|
||||
}
|
||||
};
|
||||
|
||||
const setJob = (key: keyof typeof jobPollers, job?: unknown) => {
|
||||
const parsed = job ? useFragment(NOTIFICATION_JOB_FRAGMENT, job) : null;
|
||||
activeJobs[key] = parsed;
|
||||
if (!parsed || !activeStates.includes(parsed.state)) {
|
||||
stopPolling(key);
|
||||
}
|
||||
};
|
||||
|
||||
const pollJob = (key: keyof typeof jobPollers) => {
|
||||
const job = activeJobs[key];
|
||||
if (!job) return;
|
||||
|
||||
stopPolling(key);
|
||||
if (!activeStates.includes(job.state)) return;
|
||||
|
||||
jobPollers[key] = setInterval(async () => {
|
||||
const { data } = await client.query({
|
||||
query: notificationJobStatus,
|
||||
variables: { id: job.id },
|
||||
fetchPolicy: 'network-only',
|
||||
});
|
||||
const updated = data?.notifications.job;
|
||||
if (updated) {
|
||||
setJob(key, updated);
|
||||
}
|
||||
}, 750);
|
||||
};
|
||||
|
||||
const isJobActive = (key: keyof typeof jobPollers) => {
|
||||
const job = activeJobs[key];
|
||||
return Boolean(job && activeStates.includes(job.state));
|
||||
};
|
||||
|
||||
const jobLabel = (key: keyof typeof jobPollers, fallback: string) => {
|
||||
const job = activeJobs[key];
|
||||
if (job && activeStates.includes(job.state)) {
|
||||
return t('notifications.sidebar.processingStatus', {
|
||||
processed: job.processed,
|
||||
total: job.total,
|
||||
});
|
||||
}
|
||||
return fallback;
|
||||
};
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const filterOptions = computed<Array<{ label: string; value?: Importance }>>(() => [
|
||||
@@ -63,7 +150,12 @@ const confirmAndArchiveAll = async () => {
|
||||
confirmVariant: 'primary',
|
||||
});
|
||||
if (confirmed) {
|
||||
await archiveAll();
|
||||
const { data } = await startArchiveAllJob({ importance: importance.value });
|
||||
const job = data?.notifications.startArchiveAll;
|
||||
if (job) {
|
||||
setJob('archiveAll', job);
|
||||
pollJob('archiveAll');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -75,7 +167,29 @@ const confirmAndDeleteArchives = async () => {
|
||||
confirmVariant: 'destructive',
|
||||
});
|
||||
if (confirmed) {
|
||||
await deleteArchives();
|
||||
const { data } = await startDeleteAll({ type: NotificationType.ARCHIVE });
|
||||
const job = data?.notifications.startDeleteAll;
|
||||
if (job) {
|
||||
setJob('deleteArchived', job);
|
||||
pollJob('deleteArchived');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const confirmAndDeleteUnread = async () => {
|
||||
const confirmed = await confirm({
|
||||
title: t('notifications.sidebar.confirmDeleteUnread.title'),
|
||||
description: t('notifications.sidebar.confirmDeleteUnread.description'),
|
||||
confirmText: t('notifications.sidebar.confirmDeleteUnread.confirmText'),
|
||||
confirmVariant: 'destructive',
|
||||
});
|
||||
if (confirmed) {
|
||||
const { data } = await startDeleteAll({ type: NotificationType.UNREAD });
|
||||
const job = data?.notifications.startDeleteAll;
|
||||
if (job) {
|
||||
setJob('deleteUnread', job);
|
||||
pollJob('deleteUnread');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -139,6 +253,10 @@ const readArchivedCount = computed(() => {
|
||||
const prepareToViewNotifications = () => {
|
||||
void recalculateOverview();
|
||||
};
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
Object.values(jobPollers).forEach((interval) => interval && clearInterval(interval));
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -179,24 +297,33 @@ const prepareToViewNotifications = () => {
|
||||
</TabsList>
|
||||
<TabsContent value="unread" class="flex-col items-end">
|
||||
<Button
|
||||
:disabled="loadingArchiveAll"
|
||||
:disabled="loadingArchiveAll || isJobActive('archiveAll')"
|
||||
variant="link"
|
||||
size="sm"
|
||||
class="text-foreground hover:text-destructive transition-none"
|
||||
@click="confirmAndArchiveAll"
|
||||
>
|
||||
{{ t('notifications.sidebar.archiveAllAction') }}
|
||||
{{ jobLabel('archiveAll', t('notifications.sidebar.archiveAllAction')) }}
|
||||
</Button>
|
||||
<Button
|
||||
:disabled="loadingDeleteAll || isJobActive('deleteUnread')"
|
||||
variant="link"
|
||||
size="sm"
|
||||
class="text-foreground hover:text-destructive transition-none"
|
||||
@click="confirmAndDeleteUnread"
|
||||
>
|
||||
{{ jobLabel('deleteUnread', t('notifications.sidebar.deleteAllAction')) }}
|
||||
</Button>
|
||||
</TabsContent>
|
||||
<TabsContent value="archived" class="flex-col items-end">
|
||||
<Button
|
||||
:disabled="loadingDeleteAll"
|
||||
:disabled="loadingDeleteAll || isJobActive('deleteArchived')"
|
||||
variant="link"
|
||||
size="sm"
|
||||
class="text-foreground hover:text-destructive transition-none"
|
||||
@click="confirmAndDeleteArchives"
|
||||
>
|
||||
{{ t('notifications.sidebar.deleteAllAction') }}
|
||||
{{ jobLabel('deleteArchived', t('notifications.sidebar.deleteAllAction')) }}
|
||||
</Button>
|
||||
</TabsContent>
|
||||
</div>
|
||||
|
||||
@@ -23,6 +23,17 @@ export const NOTIFICATION_COUNT_FRAGMENT = graphql(/* GraphQL */ `
|
||||
}
|
||||
`);
|
||||
|
||||
export const NOTIFICATION_JOB_FRAGMENT = graphql(/* GraphQL */ `
|
||||
fragment NotificationJobFragment on NotificationJob {
|
||||
id
|
||||
operation
|
||||
state
|
||||
processed
|
||||
total
|
||||
error
|
||||
}
|
||||
`);
|
||||
|
||||
export const getNotifications = graphql(/* GraphQL */ `
|
||||
query Notifications($filter: NotificationFilter!) {
|
||||
notifications {
|
||||
@@ -42,40 +53,36 @@ export const archiveNotification = graphql(/* GraphQL */ `
|
||||
}
|
||||
`);
|
||||
|
||||
export const archiveAllNotifications = graphql(/* GraphQL */ `
|
||||
mutation ArchiveAllNotifications {
|
||||
archiveAll {
|
||||
unread {
|
||||
total
|
||||
}
|
||||
archive {
|
||||
info
|
||||
warning
|
||||
alert
|
||||
total
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
export const deleteNotification = graphql(/* GraphQL */ `
|
||||
mutation DeleteNotification($id: PrefixedID!, $type: NotificationType!) {
|
||||
deleteNotification(id: $id, type: $type) {
|
||||
archive {
|
||||
total
|
||||
notifications {
|
||||
delete(id: $id, type: $type) {
|
||||
archive {
|
||||
total
|
||||
}
|
||||
unread {
|
||||
total
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
export const deleteArchivedNotifications = graphql(/* GraphQL */ `
|
||||
mutation DeleteAllNotifications {
|
||||
deleteArchivedNotifications {
|
||||
archive {
|
||||
total
|
||||
export const archiveAllNotifications = graphql(/* GraphQL */ `
|
||||
mutation StartArchiveAllNotifications($importance: NotificationImportance) {
|
||||
notifications {
|
||||
startArchiveAll(importance: $importance) {
|
||||
...NotificationJobFragment
|
||||
}
|
||||
unread {
|
||||
total
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
export const startDeleteNotifications = graphql(/* GraphQL */ `
|
||||
mutation StartDeleteNotifications($type: NotificationType) {
|
||||
notifications {
|
||||
startDeleteAll(type: $type) {
|
||||
...NotificationJobFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,6 +107,16 @@ export const notificationsOverview = graphql(/* GraphQL */ `
|
||||
}
|
||||
`);
|
||||
|
||||
export const notificationJobStatus = graphql(/* GraphQL */ `
|
||||
query NotificationJobStatus($id: ID!) {
|
||||
notifications {
|
||||
job(id: $id) {
|
||||
...NotificationJobFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
/** Re-calculates the notifications overview (i.e. notification counts) */
|
||||
export const resetOverview = graphql(/* GraphQL */ `
|
||||
mutation RecomputeOverview {
|
||||
|
||||
@@ -304,7 +304,11 @@
|
||||
"notifications.sidebar.confirmDeleteAll.confirmText": "Delete All",
|
||||
"notifications.sidebar.confirmDeleteAll.description": "This will permanently delete all archived notifications currently on your Unraid server. This action cannot be undone.",
|
||||
"notifications.sidebar.confirmDeleteAll.title": "Delete All Archived Notifications",
|
||||
"notifications.sidebar.confirmDeleteUnread.confirmText": "Delete",
|
||||
"notifications.sidebar.confirmDeleteUnread.description": "Are you sure you want to permanently delete all unread notifications? This cannot be undone.",
|
||||
"notifications.sidebar.confirmDeleteUnread.title": "Delete unread notifications",
|
||||
"notifications.sidebar.deleteAllAction": "Delete All",
|
||||
"notifications.sidebar.processingStatus": "Processing {{processed}} / {{total}}",
|
||||
"notifications.sidebar.editSettingsTooltip": "Edit Notification Settings",
|
||||
"notifications.sidebar.filters.alert": "Alert",
|
||||
"notifications.sidebar.filters.all": "All Types",
|
||||
|
||||
Reference in New Issue
Block a user