refactor(web): instantiate apollo client statically instead of dynamically

This commit is contained in:
Pujit Mehrotra
2024-10-16 09:44:07 -04:00
parent 4e555021a7
commit 6cac078e15
11 changed files with 406 additions and 275 deletions

View File

@@ -6,10 +6,9 @@ import {
ExclamationTriangleIcon,
LinkIcon,
} from '@heroicons/vue/24/solid';
import type { NotificationFragmentFragment } from '~/composables/gql/graphql';
import type { NotificationItemProps } from '~/types/ui/notification';
const props = defineProps<NotificationItemProps>();
const props = defineProps<NotificationFragmentFragment>();
const icon = computed<{ component: Component, color: string } | null>(() => {
switch (props.importance) {

View File

@@ -9,52 +9,39 @@ import {
SheetTrigger,
} from "@/components/shadcn/sheet";
import type { NotificationItemProps } from "~/types/ui/notification";
import { useUnraidApiStore } from "~/store/unraidApi";
import gql from "graphql-tag";
import {
getNotifications,
NOTIFICATION_FRAGMENT,
} from "./graphql/notification.query";
import {
NotificationType,
} from "~/composables/gql/graphql";
import { useFragment } from "~/composables/gql/fragment-masking";
import { useQuery } from "@vue/apollo-composable";
const getNotifications = gql`
query Notifications($filter: NotificationFilter!) {
notifications {
list(filter: $filter) {
id
title
subject
description
importance
link
type
timestamp
}
}
}
`;
// const notifications = ref<NotificationFragmentFragment[]>([]);
// watch(notifications, (newVal) => {
// console.log("[notifications]", newVal);
// });
const notifications = ref<NotificationItemProps[]>([]);
watch(notifications, (newVal) => {
console.log("[notifications]", newVal);
const fetchType = ref<NotificationType>(NotificationType.Unread);
const setFetchType = (type: NotificationType) => (fetchType.value = type);
const { result, error } = useQuery(getNotifications, {
filter: {
offset: 0,
limit: 10,
type: fetchType.value,
},
});
const fetchType = ref<"UNREAD" | "ARCHIVED">("UNREAD");
const setFetchType = (type: "UNREAD" | "ARCHIVED") => (fetchType.value = type);
const notifications = computed(() => {
if (!result.value?.notifications.list) return [];
return useFragment(NOTIFICATION_FRAGMENT, result.value?.notifications.list);
});
const { unraidApiClient: maybeApi } = storeToRefs(useUnraidApiStore());
watch(maybeApi, async (apiClient) => {
if (apiClient) {
const apiResponse = await apiClient.query({
query: getNotifications,
variables: {
filter: {
offset: 0,
limit: 10,
type: fetchType.value,
},
},
});
notifications.value = apiResponse.data.notifications.list;
}
watch(error, (newVal) => {
console.log("[sidebar error]", newVal);
});
const { teleportTarget, determineTeleportTarget } = useTeleport();
@@ -81,14 +68,14 @@ const { teleportTarget, determineTeleportTarget } = useTeleport();
<TabsTrigger
class="text-[1rem] leading-[1.3rem]"
value="unread"
@click="setFetchType('UNREAD')"
@click="setFetchType(NotificationType.Unread)"
>
Unread
</TabsTrigger>
<TabsTrigger
class="text-[1rem] leading-[1.3rem]"
value="archived"
@="setFetchType('ARCHIVED')"
@="setFetchType(NotificationType.Archive)"
>
Archived
</TabsTrigger>
@@ -97,7 +84,7 @@ const { teleportTarget, determineTeleportTarget } = useTeleport();
<Button
variant="link"
size="sm"
class="text-muted-foreground text-[1rem] leading-[1.3rem] p-0"
class="text-muted-foreground text-sm p-0"
>
{{ `Archive All` }}
</Button>
@@ -119,7 +106,10 @@ const { teleportTarget, determineTeleportTarget } = useTeleport();
<TabsContent value="unread">
<ScrollArea>
<div class="divide-y divide-gray-200">
<div
v-if="notifications?.length > 0"
class="divide-y divide-gray-200"
>
<NotificationsItem
v-for="notification in notifications"
:key="notification.id"
@@ -139,4 +129,4 @@ const { teleportTarget, determineTeleportTarget } = useTeleport();
</SheetFooter>
</SheetContent>
</Sheet>
</template>
</template>

View File

@@ -0,0 +1,26 @@
import { graphql } from "~/composables/gql/gql";
export const NOTIFICATION_FRAGMENT = graphql(/* GraphQL */ `
fragment NotificationFragment on Notification {
id
title
subject
description
importance
link
type
timestamp
}
`);
export const getNotifications = graphql(/* GraphQL */ `
query Notifications($filter: NotificationFilter!) {
notifications {
id
list(filter: $filter) {
...NotificationFragment
}
}
}
`);

View File

@@ -128,7 +128,7 @@ onBeforeMount(() => {
<div class="block w-2px h-24px bg-gamma" />
<!-- Keep the sidebar out of staging/prod builds, but easily accessible for development -->
<!-- <NotificationsSidebar /> -->
<NotificationsSidebar />
<OnClickOutside class="flex items-center justify-end h-full" :options="{ ignore: [clickOutsideIgnoreTarget] }" @trigger="outsideDropdown">
<UpcDropdownTrigger ref="clickOutsideIgnoreTarget" :t="t" />

View File

@@ -13,6 +13,8 @@ import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-
* Therefore it is highly recommended to use the babel or swc plugin for production.
*/
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 query Notifications($filter: NotificationFilter!) {\n notifications {\n id\n list(filter: $filter) {\n ...NotificationFragment\n }\n }\n }\n": types.NotificationsDocument,
"\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,
@@ -37,6 +39,14 @@ const documents = {
*/
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 query Notifications($filter: NotificationFilter!) {\n notifications {\n id\n list(filter: $filter) {\n ...NotificationFragment\n }\n }\n }\n"): (typeof documents)["\n query Notifications($filter: NotificationFilter!) {\n notifications {\n id\n list(filter: $filter) {\n ...NotificationFragment\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/

View File

@@ -1,4 +1,4 @@
/* eslint-disable */
import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>;
@@ -22,10 +22,27 @@ export type Scalars = {
Long: { input: number; output: number; }
/** A field whose value is a valid TCP port within the range of 0 to 65535: https://en.wikipedia.org/wiki/Transmission_Control_Protocol#TCP_ports */
Port: { input: number; output: number; }
/** A field whose value conforms to the standard URL format as specified in RFC3986: https://www.ietf.org/rfc/rfc3986.txt. */
URL: { input: URL; output: URL; }
/** A field whose value is a generic Universally Unique Identifier: https://en.wikipedia.org/wiki/Universally_unique_identifier. */
UUID: { input: string; output: string; }
};
export type AccessUrl = {
__typename?: 'AccessUrl';
ipv4?: Maybe<Scalars['URL']['output']>;
ipv6?: Maybe<Scalars['URL']['output']>;
name?: Maybe<Scalars['String']['output']>;
type: URL_TYPE;
};
export type AccessUrlInput = {
ipv4?: InputMaybe<Scalars['URL']['input']>;
ipv6?: InputMaybe<Scalars['URL']['input']>;
name?: InputMaybe<Scalars['String']['input']>;
type: URL_TYPE;
};
export type AllowedOriginInput = {
origins: Array<Scalars['String']['input']>;
};
@@ -45,7 +62,7 @@ export type ApiKeyResponse = {
valid: Scalars['Boolean']['output'];
};
export type ArrayType = {
export type ArrayType = Node & {
__typename?: 'Array';
/** Current boot disk */
boot?: Maybe<ArrayDisk>;
@@ -55,6 +72,7 @@ export type ArrayType = {
capacity: ArrayCapacity;
/** Data disks in the current array */
disks: Array<ArrayDisk>;
id: Scalars['ID']['output'];
/** Parity disks in the current array */
parities: Array<ArrayDisk>;
/** Array state after this query/mutation */
@@ -235,20 +253,26 @@ export type CloudResponse = {
status: Scalars['String']['output'];
};
export type Config = {
export type Config = Node & {
__typename?: 'Config';
error?: Maybe<ConfigErrorState>;
id: Scalars['ID']['output'];
valid?: Maybe<Scalars['Boolean']['output']>;
};
export enum ConfigErrorState {
Ineligible = 'INELIGIBLE',
Invalid = 'INVALID',
NoKeyServer = 'NO_KEY_SERVER',
UnknownError = 'UNKNOWN_ERROR',
Withdrawn = 'WITHDRAWN'
}
export type Connect = Node & {
__typename?: 'Connect';
dynamicRemoteAccess: DynamicRemoteAccessStatus;
id: Scalars['ID']['output'];
};
export type ConnectSignInInput = {
accessToken?: InputMaybe<Scalars['String']['input']>;
apiKey: Scalars['String']['input'];
@@ -363,6 +387,7 @@ export type Display = {
dashapps?: Maybe<Scalars['String']['output']>;
date?: Maybe<Scalars['String']['output']>;
hot?: Maybe<Scalars['Int']['output']>;
id: Scalars['ID']['output'];
locale?: Maybe<Scalars['String']['output']>;
max?: Maybe<Scalars['Int']['output']>;
number?: Maybe<Scalars['String']['output']>;
@@ -379,6 +404,13 @@ export type Display = {
wwn?: Maybe<Scalars['Boolean']['output']>;
};
export type Docker = Node & {
__typename?: 'Docker';
containers?: Maybe<Array<DockerContainer>>;
id: Scalars['ID']['output'];
networks?: Maybe<Array<DockerNetwork>>;
};
export type DockerContainer = {
__typename?: 'DockerContainer';
autoStart: Scalars['Boolean']['output'];
@@ -418,6 +450,24 @@ export type DockerNetwork = {
scope?: Maybe<Scalars['String']['output']>;
};
export type DynamicRemoteAccessStatus = {
__typename?: 'DynamicRemoteAccessStatus';
enabledType: DynamicRemoteAccessType;
error?: Maybe<Scalars['String']['output']>;
runningType: DynamicRemoteAccessType;
};
export enum DynamicRemoteAccessType {
Disabled = 'DISABLED',
Static = 'STATIC',
Upnp = 'UPNP'
}
export type EnableDynamicRemoteAccessInput = {
enabled: Scalars['Boolean']['input'];
url: AccessUrlInput;
};
export type Flash = {
__typename?: 'Flash';
guid?: Maybe<Scalars['String']['output']>;
@@ -442,7 +492,7 @@ export enum Importance {
Warning = 'WARNING'
}
export type Info = {
export type Info = Node & {
__typename?: 'Info';
/** Count of docker containers */
apps?: Maybe<InfoApps>;
@@ -450,11 +500,13 @@ export type Info = {
cpu?: Maybe<InfoCpu>;
devices?: Maybe<Devices>;
display?: Maybe<Display>;
id: Scalars['ID']['output'];
/** Machine ID */
machineId?: Maybe<Scalars['ID']['output']>;
memory?: Maybe<InfoMemory>;
os?: Maybe<Os>;
system?: Maybe<System>;
time: Scalars['DateTime']['output'];
versions?: Maybe<Versions>;
};
@@ -574,13 +626,20 @@ export type Mutation = {
addDiskToArray?: Maybe<ArrayType>;
/** Add a new user */
addUser?: Maybe<User>;
archiveAll: NotificationOverview;
/** Marks a notification as archived. */
archiveNotification: NotificationOverview;
archiveNotifications: NotificationOverview;
/** Cancel parity check */
cancelParityCheck?: Maybe<Scalars['JSON']['output']>;
clearArrayDiskStatistics?: Maybe<Scalars['JSON']['output']>;
connectSignIn: Scalars['Boolean']['output'];
connectSignOut: Scalars['Boolean']['output'];
createNotification: Notification;
deleteNotification: NotificationOverview;
/** Delete a user */
deleteUser?: Maybe<User>;
enableDynamicRemoteAccess: Scalars['Boolean']['output'];
/** Get an existing API key */
getApiKey?: Maybe<ApiKey>;
login?: Maybe<Scalars['String']['output']>;
@@ -588,11 +647,12 @@ export type Mutation = {
/** Pause parity check */
pauseParityCheck?: Maybe<Scalars['JSON']['output']>;
reboot?: Maybe<Scalars['String']['output']>;
/** Reads each notification to recompute & update the overview. */
recalculateOverview: NotificationOverview;
/** Remove existing disk from array. NOTE: The array must be stopped before running this otherwise it'll throw an error. */
removeDiskFromArray?: Maybe<ArrayType>;
/** Resume parity check */
resumeParityCheck?: Maybe<Scalars['JSON']['output']>;
sendNotification?: Maybe<Notification>;
setAdditionalAllowedOrigins: Array<Scalars['String']['output']>;
setupRemoteAccess: Scalars['Boolean']['output'];
shutdown?: Maybe<Scalars['String']['output']>;
@@ -602,7 +662,11 @@ export type Mutation = {
startParityCheck?: Maybe<Scalars['JSON']['output']>;
/** Stop array */
stopArray?: Maybe<ArrayType>;
unarchiveAll: NotificationOverview;
unarchiveNotifications: NotificationOverview;
unmountArrayDisk?: Maybe<Disk>;
/** Marks a notification as unread. */
unreadNotification: NotificationOverview;
/** Update an existing API key */
updateApikey?: Maybe<ApiKey>;
};
@@ -624,6 +688,21 @@ export type MutationaddUserArgs = {
};
export type MutationarchiveAllArgs = {
importance?: InputMaybe<Importance>;
};
export type MutationarchiveNotificationArgs = {
id: Scalars['String']['input'];
};
export type MutationarchiveNotificationsArgs = {
ids?: InputMaybe<Array<Scalars['String']['input']>>;
};
export type MutationclearArrayDiskStatisticsArgs = {
id: Scalars['ID']['input'];
};
@@ -634,11 +713,27 @@ export type MutationconnectSignInArgs = {
};
export type MutationcreateNotificationArgs = {
input: NotificationData;
};
export type MutationdeleteNotificationArgs = {
id: Scalars['String']['input'];
type: NotificationType;
};
export type MutationdeleteUserArgs = {
input: deleteUserInput;
};
export type MutationenableDynamicRemoteAccessArgs = {
input: EnableDynamicRemoteAccessInput;
};
export type MutationgetApiKeyArgs = {
input?: InputMaybe<authenticateInput>;
name: Scalars['String']['input'];
@@ -661,11 +756,6 @@ export type MutationremoveDiskFromArrayArgs = {
};
export type MutationsendNotificationArgs = {
notification: NotificationInput;
};
export type MutationsetAdditionalAllowedOriginsArgs = {
input: AllowedOriginInput;
};
@@ -681,20 +771,37 @@ export type MutationstartParityCheckArgs = {
};
export type MutationunarchiveAllArgs = {
importance?: InputMaybe<Importance>;
};
export type MutationunarchiveNotificationsArgs = {
ids?: InputMaybe<Array<Scalars['String']['input']>>;
};
export type MutationunmountArrayDiskArgs = {
id: Scalars['ID']['input'];
};
export type MutationunreadNotificationArgs = {
id: Scalars['String']['input'];
};
export type MutationupdateApikeyArgs = {
input?: InputMaybe<updateApikeyInput>;
name: Scalars['String']['input'];
};
export type Network = {
export type Network = Node & {
__typename?: 'Network';
accessUrls?: Maybe<Array<AccessUrl>>;
carrierChanges?: Maybe<Scalars['String']['output']>;
duplex?: Maybe<Scalars['String']['output']>;
id: Scalars['ID']['output'];
iface?: Maybe<Scalars['String']['output']>;
ifaceName?: Maybe<Scalars['String']['output']>;
internal?: Maybe<Scalars['String']['output']>;
@@ -707,19 +814,40 @@ export type Network = {
type?: Maybe<Scalars['String']['output']>;
};
export type Notification = {
export type Node = {
id: Scalars['ID']['output'];
};
export type Notification = Node & {
__typename?: 'Notification';
description: Scalars['String']['output'];
id: Scalars['ID']['output'];
importance: Importance;
link?: Maybe<Scalars['String']['output']>;
subject: Scalars['String']['output'];
/** ISO Timestamp for when the notification occurred */
/** ISO Timestamp for when the notification occurred */
timestamp?: Maybe<Scalars['String']['output']>;
/** Also known as 'event' */
title: Scalars['String']['output'];
type: NotificationType;
};
export type NotificationCounts = {
__typename?: 'NotificationCounts';
alert: Scalars['Int']['output'];
info: Scalars['Int']['output'];
total: Scalars['Int']['output'];
warning: Scalars['Int']['output'];
};
export type NotificationData = {
description: Scalars['String']['input'];
importance: Importance;
link?: InputMaybe<Scalars['String']['input']>;
subject: Scalars['String']['input'];
title: Scalars['String']['input'];
};
export type NotificationFilter = {
importance?: InputMaybe<Importance>;
limit: Scalars['Int']['input'];
@@ -727,23 +855,30 @@ export type NotificationFilter = {
type?: InputMaybe<NotificationType>;
};
export type NotificationInput = {
description?: InputMaybe<Scalars['String']['input']>;
id: Scalars['ID']['input'];
importance: Importance;
link?: InputMaybe<Scalars['String']['input']>;
subject: Scalars['String']['input'];
timestamp?: InputMaybe<Scalars['String']['input']>;
title: Scalars['String']['input'];
type: NotificationType;
export type NotificationOverview = {
__typename?: 'NotificationOverview';
archive: NotificationCounts;
unread: NotificationCounts;
};
export enum NotificationType {
Archived = 'ARCHIVED',
Restored = 'RESTORED',
Archive = 'ARCHIVE',
Unread = 'UNREAD'
}
export type Notifications = Node & {
__typename?: 'Notifications';
id: Scalars['ID']['output'];
list: Array<Notification>;
/** A cached overview of the notifications in the system & their severity. */
overview: NotificationOverview;
};
export type NotificationslistArgs = {
filter: NotificationFilter;
};
export type Os = {
__typename?: 'Os';
arch?: Maybe<Scalars['String']['output']>;
@@ -867,11 +1002,13 @@ export type Query = {
array: ArrayType;
cloud?: Maybe<Cloud>;
config: Config;
connect: Connect;
/** Single disk */
disk?: Maybe<Disk>;
/** Mulitiple disks */
disks: Array<Maybe<Disk>>;
display?: Maybe<Display>;
docker: Docker;
/** All Docker containers */
dockerContainers: Array<DockerContainer>;
/** Docker network */
@@ -883,7 +1020,8 @@ export type Query = {
info?: Maybe<Info>;
/** Current user account */
me?: Maybe<Me>;
notifications: Array<Notification>;
network?: Maybe<Network>;
notifications: Notifications;
online?: Maybe<Scalars['Boolean']['output']>;
owner?: Maybe<Owner>;
parityHistory?: Maybe<Array<Maybe<ParityCheck>>>;
@@ -891,6 +1029,7 @@ export type Query = {
remoteAccess: RemoteAccess;
server?: Maybe<Server>;
servers: Array<Server>;
services: Array<Service>;
/** Network Shares */
shares?: Maybe<Array<Maybe<Share>>>;
unassignedDevices?: Maybe<Array<Maybe<UnassignedDevice>>>;
@@ -924,11 +1063,6 @@ export type QuerydockerNetworksArgs = {
};
export type QuerynotificationsArgs = {
filter: NotificationFilter;
};
export type QueryuserArgs = {
id: Scalars['ID']['input'];
};
@@ -1022,8 +1156,9 @@ export enum ServerStatus {
Online = 'online'
}
export type Service = {
export type Service = Node & {
__typename?: 'Service';
id: Scalars['ID']['output'];
name?: Maybe<Scalars['String']['output']>;
online?: Maybe<Scalars['Boolean']['output']>;
uptime?: Maybe<Uptime>;
@@ -1077,6 +1212,7 @@ export type Subscription = {
info: Info;
me?: Maybe<Me>;
notificationAdded: Notification;
notificationsOverview: NotificationOverview;
online: Scalars['Boolean']['output'];
owner: Owner;
parityHistory: ParityCheck;
@@ -1137,6 +1273,15 @@ export enum Theme {
White = 'white'
}
export enum URL_TYPE {
Default = 'DEFAULT',
Lan = 'LAN',
Mdns = 'MDNS',
Other = 'OTHER',
Wan = 'WAN',
Wireguard = 'WIREGUARD'
}
export type UnassignedDevice = {
__typename?: 'UnassignedDevice';
devlinks?: Maybe<Scalars['String']['output']>;
@@ -1223,7 +1368,7 @@ export type UserAccount = {
roles: Scalars['String']['output'];
};
export type Vars = {
export type Vars = Node & {
__typename?: 'Vars';
bindMgt?: Maybe<Scalars['Boolean']['output']>;
cacheNumDevices?: Maybe<Scalars['Int']['output']>;
@@ -1257,6 +1402,7 @@ export type Vars = {
fuseRememberDefault?: Maybe<Scalars['String']['output']>;
fuseRememberStatus?: Maybe<Scalars['String']['output']>;
hideDotFiles?: Maybe<Scalars['Boolean']['output']>;
id: Scalars['ID']['output'];
joinStatus?: Maybe<Scalars['String']['output']>;
localMaster?: Maybe<Scalars['Boolean']['output']>;
localTld?: Maybe<Scalars['String']['output']>;
@@ -1510,6 +1656,18 @@ export type usersInput = {
slim?: InputMaybe<Scalars['Boolean']['input']>;
};
export type NotificationFragmentFragment = { __typename?: 'Notification', id: string, title: string, subject: string, description: string, importance: Importance, link?: string | null, type: NotificationType, timestamp?: string | null } & { ' $fragmentName'?: 'NotificationFragmentFragment' };
export type NotificationsQueryVariables = Exact<{
filter: NotificationFilter;
}>;
export type NotificationsQuery = { __typename?: 'Query', notifications: { __typename?: 'Notifications', id: string, list: Array<(
{ __typename?: 'Notification' }
& { ' $fragmentRefs'?: { 'NotificationFragmentFragment': NotificationFragmentFragment } }
)> } };
export type ConnectSignInMutationVariables = Exact<{
input: ConnectSignInInput;
}>;
@@ -1556,7 +1714,9 @@ export type setupRemoteAccessMutationVariables = Exact<{
export type setupRemoteAccessMutation = { __typename?: 'Mutation', setupRemoteAccess: boolean };
export const NotificationFragmentFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"NotificationFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Notification"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"subject"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"importance"}},{"kind":"Field","name":{"kind":"Name","value":"link"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"timestamp"}}]}}]} as unknown as DocumentNode<NotificationFragmentFragment, unknown>;
export const PartialCloudFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PartialCloud"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Cloud"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"error"}},{"kind":"Field","name":{"kind":"Name","value":"apiKey"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"valid"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"cloud"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"minigraphql"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"relay"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}}]}}]} as unknown as DocumentNode<PartialCloudFragment, unknown>;
export const NotificationsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Notifications"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"NotificationFilter"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"notifications"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"list"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"NotificationFragment"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"NotificationFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Notification"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"subject"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"importance"}},{"kind":"Field","name":{"kind":"Name","value":"link"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"timestamp"}}]}}]} as unknown as DocumentNode<NotificationsQuery, NotificationsQueryVariables>;
export const ConnectSignInDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ConnectSignIn"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ConnectSignInInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"connectSignIn"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode<ConnectSignInMutation, ConnectSignInMutationVariables>;
export const SignOutDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SignOut"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"connectSignOut"}}]}}]} as unknown as DocumentNode<SignOutMutation, SignOutMutationVariables>;
export const serverStateDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"serverState"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cloud"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"PartialCloud"}}]}},{"kind":"Field","name":{"kind":"Name","value":"config"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"error"}},{"kind":"Field","name":{"kind":"Name","value":"valid"}}]}},{"kind":"Field","name":{"kind":"Name","value":"info"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"os"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hostname"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"owner"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}},{"kind":"Field","name":{"kind":"Name","value":"registration"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"expiration"}},{"kind":"Field","name":{"kind":"Name","value":"keyFile"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"contents"}}]}},{"kind":"Field","name":{"kind":"Name","value":"updateExpiration"}}]}},{"kind":"Field","name":{"kind":"Name","value":"vars"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"regGen"}},{"kind":"Field","name":{"kind":"Name","value":"regState"}},{"kind":"Field","name":{"kind":"Name","value":"configError"}},{"kind":"Field","name":{"kind":"Name","value":"configValid"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PartialCloud"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Cloud"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"error"}},{"kind":"Field","name":{"kind":"Name","value":"apiKey"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"valid"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"cloud"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"minigraphql"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"relay"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}}]}}]} as unknown as DocumentNode<serverStateQuery, serverStateQueryVariables>;

View File

@@ -0,0 +1,112 @@
import type {
split as SplitType,
ApolloClient as ApolloClientType,
InMemoryCache as InMemoryCacheType,
} from "@apollo/client";
import {
from,
ApolloClient,
createHttpLink,
InMemoryCache,
split,
// @ts-expect-error - CommonJS doesn't have type definitions
} from "@apollo/client/core/core.cjs";
import { onError } from "@apollo/client/link/error";
import { RetryLink } from "@apollo/client/link/retry";
import { GraphQLWsLink } from "@apollo/client/link/subscriptions";
import { getMainDefinition } from "@apollo/client/utilities";
import { provideApolloClient } from "@vue/apollo-composable";
import { createClient } from "graphql-ws";
import { WEBGUI_GRAPHQL } from "./urls";
const httpEndpoint = WEBGUI_GRAPHQL;
const wsEndpoint = new URL(WEBGUI_GRAPHQL.toString().replace("http", "ws"));
// const headers = { 'x-api-key': serverStore.apiKey };
const headers = {};
const httpLink = createHttpLink({
uri: httpEndpoint.toString(),
headers,
credentials: "include",
});
const wsLink = new GraphQLWsLink(
createClient({
url: wsEndpoint.toString(),
connectionParams: () => ({
headers,
}),
})
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const errorLink = onError(({ graphQLErrors, networkError }: any) => {
if (graphQLErrors) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
graphQLErrors.map((error: any) => {
console.error("[GraphQL error]", error);
const errorMsg =
error.error && error.error.message
? error.error.message
: error.message;
if (errorMsg && errorMsg.includes("offline")) {
// @todo restart the api
}
return error.message;
});
}
if (networkError) {
console.error(`[Network error]: ${networkError}`);
const msg = networkError.message ? networkError.message : networkError;
if (
typeof msg === "string" &&
msg.includes("Unexpected token < in JSON at position 0")
) {
return "Unraid API • CORS Error";
}
return msg;
}
});
const retryLink = new RetryLink({
attempts: {
max: 20,
retryIf: (error, _operation) => {
return Boolean(error);
},
},
delay: {
initial: 300,
max: 10000,
jitter: true,
},
});
const splitLinks = (split as typeof SplitType)(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === "OperationDefinition" &&
definition.operation === "subscription"
);
},
wsLink,
httpLink
);
/**
* @todo as we add retries, determine which we'll need
* https://www.apollographql.com/docs/react/api/link/introduction/#additive-composition
* https://www.apollographql.com/docs/react/api/link/introduction/#directional-composition
*/
const additiveLink = from([errorLink, retryLink, splitLinks]);
const client: ApolloClientType<InMemoryCacheType> = new ApolloClient({
link: additiveLink,
cache: new InMemoryCache(),
});
provideApolloClient(client);

View File

@@ -63,14 +63,6 @@ export const useServerStore = defineStore('server', () => {
* State
*/
const apiKey = ref<string>('');
watch(apiKey, (newVal, oldVal) => {
if (newVal) {
return unraidApiStore.createApolloClient();
}
if (oldVal) {
return unraidApiStore.closeUnraidApiClient();
}
});
const apiVersion = ref<string>('');
const array = ref<ServerStateArray | undefined>();
// helps to display warning next to array status
@@ -958,8 +950,9 @@ export const useServerStore = defineStore('server', () => {
regGen: data.vars && data.vars.regGen ? parseInt(data.vars.regGen) : undefined,
state: data.vars && data.vars.regState ? data.vars.regState : undefined,
config: data.config
? data.config
? { id: 'config', ...data.config }
: {
id: 'config',
error: data.vars && data.vars.configError ? data.vars.configError : undefined,
valid: data.vars && data.vars.configValid ? data.vars.configValid : true,
},

View File

@@ -64,6 +64,7 @@ export const useThemeStore = defineStore('theme', () => {
body.style.setProperty('--shadow-beta', `0 25px 50px -12px ${hexToRgba(beta, 0.15)}`);
body.style.setProperty('--ring-offset-shadow', `0 0 ${beta}`);
body.style.setProperty('--ring-shadow', `0 0 ${beta}`);
body.style.setProperty('--dev-test', `0 0 ${beta}`);
};
watch(theme, () => {

View File

@@ -1,33 +1,17 @@
import {
from,
ApolloClient,
createHttpLink,
InMemoryCache,
split,
// @ts-expect-error - CommonJS doesn't have type definitions
} from "@apollo/client/core/core.cjs";
import {
type ApolloClient as ApolloClientType,
type InMemoryCache as InMemoryCacheType,
} from '@apollo/client';
import { onError } from '@apollo/client/link/error';
import { RetryLink } from '@apollo/client/link/retry';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { getMainDefinition } from '@apollo/client/utilities';
import { ArrowPathIcon, CogIcon } from '@heroicons/vue/24/solid';
import { provideApolloClient } from '@vue/apollo-composable';
} from "@apollo/client";
import { ArrowPathIcon } from "@heroicons/vue/24/solid";
// import { logErrorMessages } from '@vue/apollo-util';
import { createClient } from 'graphql-ws';
import { defineStore, createPinia, setActivePinia } from 'pinia';
import type { UserProfileLink } from '~/types/userProfile';
import { defineStore, createPinia, setActivePinia } from "pinia";
import type { UserProfileLink } from "~/types/userProfile";
import { WebguiUnraidApiCommand } from '~/composables/services/webgui';
import {
WEBGUI_GRAPHQL,
WEBGUI_SETTINGS_MANAGMENT_ACCESS,
} from '~/helpers/urls';
import { useErrorsStore } from '~/store/errors';
import { useServerStore } from '~/store/server';
import { WebguiUnraidApiCommand } from "~/composables/services/webgui";
import { useErrorsStore } from "~/store/errors";
import { useServerStore } from "~/store/server";
import "~/helpers/create-apollo-client";
/**
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
@@ -35,13 +19,7 @@ import { useServerStore } from '~/store/server';
*/
setActivePinia(createPinia());
const ERROR_CORS_403 =
'The CORS policy for this site does not allow access from the specified Origin';
const httpEndpoint = WEBGUI_GRAPHQL;
const wsEndpoint = new URL(WEBGUI_GRAPHQL.toString().replace('http', 'ws'));
export const useUnraidApiStore = defineStore('unraidApi', () => {
export const useUnraidApiStore = defineStore("unraidApi", () => {
const errorsStore = useErrorsStore();
const serverStore = useServerStore();
const unraidApiClient = ref<ApolloClientType<InMemoryCacheType> | null>(null);
@@ -50,21 +28,21 @@ export const useUnraidApiStore = defineStore('unraidApi', () => {
const apiResponse = serverStore.fetchServerFromApi();
if (apiResponse) {
// we have a response, so we're online
unraidApiStatus.value = 'online';
unraidApiStatus.value = "online";
}
}
});
// const unraidApiErrors = ref<any[]>([]);
const unraidApiStatus = ref<
'connecting' | 'offline' | 'online' | 'restarting'
>('offline');
"connecting" | "offline" | "online" | "restarting"
>("offline");
const prioritizeCorsError = ref(false); // Ensures we don't overwrite this specific error message with a non-descriptive network error message
const unraidApiRestartAction = computed((): UserProfileLink | undefined => {
const { connectPluginInstalled, stateDataError } = serverStore;
if (
unraidApiStatus.value !== 'offline' ||
unraidApiStatus.value !== "offline" ||
!connectPluginInstalled ||
stateDataError
) {
@@ -74,132 +52,10 @@ export const useUnraidApiStore = defineStore('unraidApi', () => {
click: () => restartUnraidApiClient(),
emphasize: true,
icon: ArrowPathIcon,
text: 'Restart unraid-api',
text: "Restart unraid-api",
};
});
/**
* Automatically called when an apiKey is set in the serverStore
*/
const createApolloClient = () => {
// return; // @todo remove
unraidApiStatus.value = 'connecting';
// const headers = { 'x-api-key': serverStore.apiKey };
const headers = {};
const httpLink = createHttpLink({
uri: httpEndpoint.toString(),
headers,
credentials: "include",
});
const wsLink = new GraphQLWsLink(
createClient({
url: wsEndpoint.toString(),
connectionParams: () => ({
headers,
}),
})
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const errorLink = onError(({ graphQLErrors, networkError }: any) => {
if (graphQLErrors) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
graphQLErrors.map((error: any) => {
console.error('[GraphQL error]', error);
const errorMsg =
error.error && error.error.message
? error.error.message
: error.message;
if (errorMsg && errorMsg.includes('offline')) {
unraidApiStatus.value = 'offline';
// attempt to automatically restart the unraid-api
if (unraidApiRestartAction) {
restartUnraidApiClient();
}
}
if (errorMsg && errorMsg.includes(ERROR_CORS_403)) {
prioritizeCorsError.value = true;
const msg = `<p>The CORS policy for the unraid-api does not allow access from the specified origin.</p><p>If you are using a reverse proxy, you need to copy your origin <strong class="font-mono"><em>${window.location.origin}</em></strong> and paste it into the "Extra Origins" list in the Connect settings.</p>`;
errorsStore.setError({
heading: 'Unraid API • CORS Error',
message: msg,
level: 'error',
ref: 'unraidApiCorsError',
type: 'unraidApiGQL',
actions: [
{
href: `${WEBGUI_SETTINGS_MANAGMENT_ACCESS.toString()}#extraOriginsSettings`,
icon: CogIcon,
text: 'Go to Connect Settings',
},
],
});
}
return error.message;
});
}
if (networkError && !prioritizeCorsError) {
console.error(`[Network error]: ${networkError}`);
const msg = networkError.message ? networkError.message : networkError;
if (
typeof msg === 'string' &&
msg.includes('Unexpected token < in JSON at position 0')
) {
return 'Unraid API • CORS Error';
}
return msg;
}
});
const retryLink = new RetryLink({
attempts: {
max: 20,
retryIf: (error, _operation) => {
return !!error && !prioritizeCorsError; // don't retry when ERROR_CORS_403
},
},
delay: {
initial: prioritizeCorsError ? 3000 : 300,
max: 10000,
jitter: true,
},
});
interface Definintion {
kind: string;
operation?: string;
}
const splitLinks = split(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
({ query }: any) => {
const definition: Definintion = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
httpLink
);
/**
* @todo as we add retries, determine which we'll need
* https://www.apollographql.com/docs/react/api/link/introduction/#additive-composition
* https://www.apollographql.com/docs/react/api/link/introduction/#directional-composition
*/
const additiveLink = from([errorLink, retryLink, splitLinks]);
const client: ApolloClientType<InMemoryCacheType> = new ApolloClient({
link: additiveLink,
cache: new InMemoryCache(),
});
unraidApiClient.value = client;
provideApolloClient(client);
};
/**
* Automatically called when an apiKey is unset in the serverStore
*/
@@ -219,31 +75,26 @@ export const useUnraidApiStore = defineStore('unraidApi', () => {
* Can both start and restart the unraid-api depending on it's current status
*/
const restartUnraidApiClient = async () => {
const command = unraidApiStatus.value === 'offline' ? 'start' : 'restart';
unraidApiStatus.value = 'restarting';
const command = unraidApiStatus.value === "offline" ? "start" : "restart";
unraidApiStatus.value = "restarting";
try {
await WebguiUnraidApiCommand({
csrf_token: serverStore.csrf,
command,
});
return setTimeout(() => {
if (unraidApiClient.value) {
createApolloClient();
}
}, 5000);
} catch (error) {
let errorMessage = 'Unknown error';
if (typeof error === 'string') {
let errorMessage = "Unknown error";
if (typeof error === "string") {
errorMessage = error.toUpperCase();
} else if (error instanceof Error) {
errorMessage = error.message;
}
errorsStore.setError({
heading: 'Error: unraid-api restart',
heading: "Error: unraid-api restart",
message: errorMessage,
level: 'error',
ref: 'restartUnraidApiClient',
type: 'request',
level: "error",
ref: "restartUnraidApiClient",
type: "request",
});
}
};
@@ -253,7 +104,6 @@ export const useUnraidApiStore = defineStore('unraidApi', () => {
unraidApiStatus,
prioritizeCorsError,
unraidApiRestartAction,
createApolloClient,
closeUnraidApiClient,
restartUnraidApiClient,
};

View File

@@ -1,10 +0,0 @@
export interface NotificationItemProps {
id: string;
title: string;
subject: string;
description: string;
importance: string;
link: string;
type: 'success' | 'warning' | 'alert';
timestamp: string;
}