Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions api/src/graphql/generated/client/gql.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable */
import * as types from './graphql.js';
import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';

Expand Down
1 change: 1 addition & 0 deletions api/src/graphql/generated/client/validators.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable */
import { z } from 'zod'
import { AccessUrlInput, ArrayCapacityBytesInput, ArrayCapacityInput, ClientType, ConfigErrorState, DashboardAppsInput, DashboardArrayInput, DashboardCaseInput, DashboardConfigInput, DashboardDisplayInput, DashboardInput, DashboardOsInput, DashboardServiceInput, DashboardServiceUptimeInput, DashboardTwoFactorInput, DashboardTwoFactorLocalInput, DashboardTwoFactorRemoteInput, DashboardVarsInput, DashboardVersionsInput, DashboardVmsInput, EventType, Importance, NetworkInput, NotificationInput, NotificationStatus, PingEventSource, RegistrationState, RemoteAccessEventActionType, RemoteAccessInput, RemoteGraphQLClientInput, RemoteGraphQLEventType, RemoteGraphQLServerInput, ServerStatus, URL_TYPE, UpdateType } from '@app/graphql/generated/client/graphql'

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ import { NotificationsService } from './notifications.service';
import { Importance } from '@app/graphql/generated/client/graphql';
import { AppError } from '@app/core/errors/app-error';
import { formatTimestamp } from '@app/utils';
import {Inject} from "@nestjs/common";

@Resolver('Notifications')
export class NotificationsResolver {
constructor(readonly notificationsService: NotificationsService) {}
constructor(@Inject('NOTIFICATIONS_SERVICE') readonly notificationsService: NotificationsService) {}

/**============================================
* Queries
Expand Down
2 changes: 1 addition & 1 deletion api/src/unraid-api/graph/resolvers/resolvers.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import { NotificationsService } from './notifications/notifications.service';
ServerResolver,
VarsResolver,
VmsResolver,
NotificationsService,
{ provide: 'NOTIFICATIONS_SERVICE', useClass: NotificationsService },
],
})
export class ResolversModule {}
98 changes: 98 additions & 0 deletions web/components/Notifications/Indicator.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<script setup lang="ts">
import { useQuery } from "@vue/apollo-composable";
import { unreadOverview } from "./graphql/notification.query";
import { Importance } from "~/composables/gql/graphql";
import {
BellIcon,
ExclamationTriangleIcon,
ShieldExclamationIcon,
} from "@heroicons/vue/24/solid";
import { cn } from '~/components/shadcn/utils'
import { onWatcherCleanup } from "vue";
const { result } = useQuery(unreadOverview, null, {
pollInterval: 2_000, // 2 seconds
});
const overview = computed(() => {
if (!result.value) {
return;
}
return result.value.notifications.overview.unread;
});
const indicatorLevel = computed(() => {
if (!overview.value) {
return undefined;
}
switch (true) {
case overview.value.alert > 0:
return Importance.Alert;
case overview.value.warning > 0:
return Importance.Warning;
case overview.value.total > 0:
return "UNREAD";
default:
return undefined;
}
});
const icon = computed<{ component: Component; color: string } | null>(() => {
switch (indicatorLevel.value) {
case Importance.Warning:
return {
component: ExclamationTriangleIcon,
color: "text-yellow-500 translate-y-0.5",
};
case Importance.Alert:
return {
component: ShieldExclamationIcon,
color: "text-red-500",
};
}
return null;
});
/** whether new notifications ocurred */
const hasNewNotifications = ref(false);
// watch for new notifications, set a temporary indicator when they're reveived
watch(overview, (newVal, oldVal) => {
if (!newVal || !oldVal) {
return;
}
hasNewNotifications.value = newVal.total > oldVal.total;
// lifetime of 'new notification' state
const msToLive = 30_000;
const timeout = setTimeout(() => {
hasNewNotifications.value = false;
}, msToLive);
onWatcherCleanup(() => clearTimeout(timeout));
});
</script>

<template>
<div class="flex items-end gap-1">
<div class="relative">
<BellIcon class="w-6 h-6" />
<div
v-if="indicatorLevel"
:class="
cn('absolute top-0 right-0 size-2.5 rounded-full', {
'bg-unraid-red': indicatorLevel === Importance.Alert,
'bg-yellow-500': indicatorLevel === Importance.Warning,
'bg-green-500': indicatorLevel === 'UNREAD',
})
"
/>
<div
v-if="hasNewNotifications || indicatorLevel === Importance.Alert"
class="absolute top-0 right-0 size-2.5 rounded-full bg-unraid-red animate-ping"
/>
</div>
<component
:is="icon.component"
v-if="icon"
:class="cn('size-6', icon.color)"
/>
</div>
</template>
3 changes: 1 addition & 2 deletions web/components/Notifications/Sidebar.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<script setup lang="ts">
import { BellIcon } from "@heroicons/vue/24/solid";
import {
Sheet,
SheetContent,
Expand All @@ -24,7 +23,7 @@ const importance = ref<Importance | undefined>(undefined);
<Sheet>
<SheetTrigger @click="determineTeleportTarget">
<span class="sr-only">Notifications</span>
<BellIcon class="w-6 h-6" />
<NotificationsIndicator />
</SheetTrigger>

<!-- We remove the horizontal padding from the container to keep the NotificationList's scrollbar in the right place -->
Expand Down
24 changes: 24 additions & 0 deletions web/components/Notifications/graphql/notification.query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ export const NOTIFICATION_FRAGMENT = graphql(/* GraphQL */ `
}
`);

export const NOTIFICATION_COUNT_FRAGMENT = graphql(/* GraphQL */ `
fragment NotificationCountFragment on NotificationCounts {
total
info
warning
alert
}
`);

export const getNotifications = graphql(/* GraphQL */ `
query Notifications($filter: NotificationFilter!) {
notifications {
Expand Down Expand Up @@ -57,3 +66,18 @@ export const deleteNotification = graphql(/* GraphQL */ `
}
}
`);

export const unreadOverview = graphql(/* GraphQL */ `
query Overview {
notifications {
overview {
unread {
info
warning
alert
total
}
}
}
}
`);
26 changes: 23 additions & 3 deletions web/composables/gql/fragment-masking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,45 @@ export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>>
): TType;
// return nullable if `fragmentType` is undefined
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | undefined
): TType | undefined;
// return nullable if `fragmentType` is nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | null
): TType | null;
// return nullable if `fragmentType` is nullable or undefined
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | null | undefined
): TType | null | undefined;
// return array of non-nullable if `fragmentType` is array of non-nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: Array<FragmentType<DocumentTypeDecoration<TType, any>>>
): Array<TType>;
// return array of nullable if `fragmentType` is array of nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: Array<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined
): Array<TType> | null | undefined;
// return readonly array of non-nullable if `fragmentType` is array of non-nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>
): ReadonlyArray<TType>;
// return array of nullable if `fragmentType` is array of nullable
// return readonly array of nullable if `fragmentType` is array of nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined
): ReadonlyArray<TType> | null | undefined;
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined
): TType | ReadonlyArray<TType> | null | undefined {
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | Array<FragmentType<DocumentTypeDecoration<TType, any>>> | ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined
): TType | Array<TType> | ReadonlyArray<TType> | null | undefined {
return fragmentType as any;
}

Expand Down
11 changes: 11 additions & 0 deletions web/composables/gql/gql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@ import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-
* 3. It does not support dead code elimination, so it will add unused operations.
*
* Therefore it is highly recommended to use the babel or swc plugin for production.
* Learn more about it here: https://the-guild.dev/graphql/codegen/plugins/presets/preset-client#reducing-bundle-size
*/
const documents = {
"\n fragment NotificationFragment on Notification {\n id\n title\n subject\n description\n importance\n link\n type\n timestamp\n }\n": types.NotificationFragmentFragmentDoc,
"\n fragment NotificationCountFragment on NotificationCounts {\n total\n info\n warning\n alert\n }\n": types.NotificationCountFragmentFragmentDoc,
"\n query Notifications($filter: NotificationFilter!) {\n notifications {\n id\n list(filter: $filter) {\n ...NotificationFragment\n }\n }\n }\n": types.NotificationsDocument,
"\n mutation ArchiveNotification($id: String!) {\n archiveNotification(id: $id) {\n ...NotificationFragment\n }\n }\n": types.ArchiveNotificationDocument,
"\n mutation ArchiveAllNotifications {\n archiveAll {\n unread {\n total\n }\n archive {\n info\n warning\n alert\n total\n }\n }\n }\n": types.ArchiveAllNotificationsDocument,
"\n mutation DeleteNotification($id: String!, $type: NotificationType!) {\n deleteNotification(id: $id, type: $type) {\n archive {\n total\n }\n }\n }\n": types.DeleteNotificationDocument,
"\n query Overview {\n notifications {\n overview {\n unread {\n info\n warning\n alert\n total\n }\n }\n }\n }\n": types.OverviewDocument,
"\n mutation ConnectSignIn($input: ConnectSignInInput!) {\n connectSignIn(input: $input)\n }\n": types.ConnectSignInDocument,
"\n mutation SignOut {\n connectSignOut\n }\n": types.SignOutDocument,
"\n fragment PartialCloud on Cloud {\n error\n apiKey {\n valid\n error\n }\n cloud {\n status\n error\n }\n minigraphql {\n status\n error\n }\n relay {\n status\n error\n }\n }\n": types.PartialCloudFragmentDoc,
Expand Down Expand Up @@ -46,6 +49,10 @@ export function graphql(source: string): unknown;
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment NotificationFragment on Notification {\n id\n title\n subject\n description\n importance\n link\n type\n timestamp\n }\n"): (typeof documents)["\n fragment NotificationFragment on Notification {\n id\n title\n subject\n description\n importance\n link\n type\n timestamp\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment NotificationCountFragment on NotificationCounts {\n total\n info\n warning\n alert\n }\n"): (typeof documents)["\n fragment NotificationCountFragment on NotificationCounts {\n total\n info\n warning\n alert\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
Expand All @@ -62,6 +69,10 @@ export function graphql(source: "\n mutation ArchiveAllNotifications {\n arc
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n mutation DeleteNotification($id: String!, $type: NotificationType!) {\n deleteNotification(id: $id, type: $type) {\n archive {\n total\n }\n }\n }\n"): (typeof documents)["\n mutation DeleteNotification($id: String!, $type: NotificationType!) {\n deleteNotification(id: $id, type: $type) {\n archive {\n total\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query Overview {\n notifications {\n overview {\n unread {\n info\n warning\n alert\n total\n }\n }\n }\n }\n"): (typeof documents)["\n query Overview {\n notifications {\n overview {\n unread {\n info\n warning\n alert\n total\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
Expand Down
Loading
Loading