diff --git a/frontend/app.vue b/frontend/app.vue index 7209909..49bfbbb 100644 --- a/frontend/app.vue +++ b/frontend/app.vue @@ -1,6 +1,14 @@ + + diff --git a/frontend/bun.lockb b/frontend/bun.lockb index 964c7ba..ffcd954 100755 Binary files a/frontend/bun.lockb and b/frontend/bun.lockb differ diff --git a/frontend/codegen.ts b/frontend/codegen.ts index 14b8e77..051ef50 100644 --- a/frontend/codegen.ts +++ b/frontend/codegen.ts @@ -22,7 +22,7 @@ const config: CodegenConfig = { JSON: "string", JwtClaim: "string", UUID: "string", - BitString: "string" + BitString: "string", }, }, }, diff --git a/frontend/layouts/default.vue b/frontend/layouts/default.vue index 3d6d013..3f00461 100644 --- a/frontend/layouts/default.vue +++ b/frontend/layouts/default.vue @@ -45,20 +45,17 @@ diff --git a/frontend/lib/auth.ts b/frontend/lib/auth.ts index 3c83d95..67f66ce 100644 --- a/frontend/lib/auth.ts +++ b/frontend/lib/auth.ts @@ -1,4 +1,4 @@ -import { ApolloClient, InMemoryCache } from "@apollo/client/core/index.js"; +import { useApolloClient } from "@vue/apollo-composable"; import { jwtDecode } from "jwt-decode"; import { z } from "zod"; @@ -14,7 +14,6 @@ export type OnLoginFn = (token: string | undefined) => Promise; export interface AuthenticatorLoginInput { - endpoint: string; authenticator: string; role: string; username: string; @@ -29,13 +28,7 @@ export interface AuthenticatorLoginInput { export async function loginWithAuthenticator( data: AuthenticatorLoginInput, ): Promise { - const client = new ApolloClient({ - uri: data.endpoint, - cache: new InMemoryCache(), - }); - - // define onLogin as a reference to the useApollo composable https://apollo.nuxtjs.org/getting-started/composables - const { onLogin } = useApollo(); + const { client } = useApolloClient(); const authenticationRes = await client.mutate({ mutation: AuthenticationRequestDocument, @@ -80,11 +73,16 @@ export async function loginWithAuthenticator( if(!jwt) { throw new AuthenticationError("Received empty JWT claim from authentication verification mutation"); } - // This applies the jwt to the Apollo client, but the client has a hardcoded endpoint :-( - return onLogin(jwt); + + const jwtCookie = useCookie(GRAPHL_TOKEN_KEY, { + sameSite: "strict", + httpOnly: false, + }); + jwtCookie.value = jwt; } + const GraphQLUserSchema = z.object({ authenticator: z.string(), role: z.string(), @@ -115,3 +113,14 @@ export function useGraphQLUser(): ComputedRef { } }); } + + + +export async function logoutGraphQL() { + const jwtCookie = useCookie(GRAPHL_TOKEN_KEY, { + sameSite: "strict", + httpOnly: false, + }); + + jwtCookie.value = null; +} diff --git a/frontend/lib/graphql/index.ts b/frontend/lib/graphql/index.ts new file mode 100644 index 0000000..1b2c5e5 --- /dev/null +++ b/frontend/lib/graphql/index.ts @@ -0,0 +1,47 @@ +import { InMemoryCache, HttpLink, ApolloClient } from "@apollo/client/core"; +import { setContext } from "@apollo/client/link/context"; +import { DefaultApolloClient } from "@vue/apollo-composable"; + +import { GRAPHL_TOKEN_KEY } from "../consts"; +import { useGraphQLStore } from "~/stores/graphql"; + + + +export function provideApolloClient() { + const graphqlStore = useGraphQLStore(); + const jwtCookie = useCookie(GRAPHL_TOKEN_KEY); + + const cache = new InMemoryCache({ + typePolicies: { + AttributesGetTimeSeriesRecord: { + keyFields: false, + }, + }, + }); + + const httpLink = new HttpLink({ + // Get latest GraphQL endpoint, in case the user switched to a different server. + uri: () => graphqlStore.endpoint, + }); + + // Add JWT to GraphQL requests. + const authLink = setContext((_, { headers }) => { + if(!jwtCookie.value) { + return { headers }; + } + + return { + headers: { + ...headers, + authorization: `Bearer ${jwtCookie.value}`, + }, + }; + }); + + const apolloClient = new ApolloClient({ + cache, + link: authLink.concat(httpLink), + }); + + provide(DefaultApolloClient, apolloClient); +} diff --git a/frontend/lib/hooks/index.ts b/frontend/lib/hooks/index.ts new file mode 100644 index 0000000..592b974 --- /dev/null +++ b/frontend/lib/hooks/index.ts @@ -0,0 +1,3 @@ +export * from "./useEquipmentDetailWithOEE"; +export * from "./useEquipmentIds"; +export * from "./useEquipmentWithOEE"; diff --git a/frontend/composables/useAsyncEquipmentDetailWithOEE.ts b/frontend/lib/hooks/useEquipmentDetailWithOEE.ts similarity index 55% rename from frontend/composables/useAsyncEquipmentDetailWithOEE.ts rename to frontend/lib/hooks/useEquipmentDetailWithOEE.ts index 241a570..bf928ec 100644 --- a/frontend/composables/useAsyncEquipmentDetailWithOEE.ts +++ b/frontend/lib/hooks/useEquipmentDetailWithOEE.ts @@ -1,28 +1,28 @@ +import { useQuery } from "@vue/apollo-composable"; + import { GetEquipmentDetailDocument } from "~/generated/graphql/operations"; -import { parseEquipmentWithOEE } from "~/lib/equipment"; +import { parseEquipmentWithOEE } from "~/lib/equipment"; -export default function useAsyncEquipmentDetailWithOEE(id: string) { +export function useEquipmentDetailWithOEE(id: string) { // TODO: Use equipment timezone instead of user timezone. const startTime = new Date(); startTime.setHours(0, 0, 0, 0); const endTime = new Date(startTime); endTime.setDate(startTime.getDate() + 1); - return useAsyncQuery( + const query = useQuery( GetEquipmentDetailDocument, { id, startTime: startTime.toISOString(), endTime: endTime.toISOString(), }, - "default", - { }, - { - transform: (res) => { - return res.equipment ? parseEquipmentWithOEE(res.equipment) : undefined; - }, - }, ); + + return { + query, + data: computed(() => query.result.value?.equipment ? parseEquipmentWithOEE(query.result.value.equipment) : undefined), + }; } diff --git a/frontend/composables/useAsyncEquipmentIds.ts b/frontend/lib/hooks/useEquipmentIds.ts similarity index 91% rename from frontend/composables/useAsyncEquipmentIds.ts rename to frontend/lib/hooks/useEquipmentIds.ts index 2f4b67f..21a9296 100644 --- a/frontend/composables/useAsyncEquipmentIds.ts +++ b/frontend/lib/hooks/useEquipmentIds.ts @@ -1,3 +1,4 @@ +import { useQuery } from "@vue/apollo-composable"; import { isNonNullish, unique } from "remeda"; import type { GetOeeEquipmentTypesWithEquipmentIdsQuery } from "~/generated/graphql/operations"; @@ -79,15 +80,16 @@ function deriveOeeEquipmentIds( * A hook to retrieve all equipment with OEE metrics. * Makes a couple GraphQL calls then transforms the returned equipment into a standard shape. */ -export default function useAsyncEquipmentIds() { - return useAsyncQuery( +export function useEquipmentIds() { + const query = useQuery( GetOeeEquipmentTypesWithEquipmentIdsDocument, {}, - "default", { errorPolicy: "ignore" }, - { - transform: deriveOeeEquipmentIds, - }, ); + + return { + query, + data: computed(() => query.result.value && deriveOeeEquipmentIds(query.result.value)), + }; } diff --git a/frontend/composables/useAsyncEquipmentWithOEE.ts b/frontend/lib/hooks/useEquipmentWithOEE.ts similarity index 56% rename from frontend/composables/useAsyncEquipmentWithOEE.ts rename to frontend/lib/hooks/useEquipmentWithOEE.ts index f1a0281..057f75e 100644 --- a/frontend/composables/useAsyncEquipmentWithOEE.ts +++ b/frontend/lib/hooks/useEquipmentWithOEE.ts @@ -1,17 +1,14 @@ import type { VariablesOf } from "@graphql-typed-document-node/core"; +import { useQuery } from "@vue/apollo-composable"; +import { useEquipmentIds } from "./useEquipmentIds"; import { GetEquipmentsDocument } from "~/generated/graphql/operations"; import { parseEquipmentWithOEE } from "~/lib/equipment"; -export default function useAsyncEquipmentWithOEE() { - const { - data: equipmentIds, - refresh: idRefresh, - status: idStatus, - pending: idPending, - } = useAsyncEquipmentIds(); +export function useEquipmentWithOEE() { + const idQuery = useEquipmentIds(); // TODO: Use equipment timezone instead of user timezone. const startTime = new Date(); @@ -22,34 +19,28 @@ export default function useAsyncEquipmentWithOEE() { // Making this `computed` so the query is reactive to changes in `equipmentIds`. const variables = computed>(() => ({ filter: { - id: { in: equipmentIds.value ?? [] }, + id: { in: idQuery.data.value ?? [] }, }, startTime: startTime.toISOString(), endTime: endTime.toISOString(), })); - const res = useAsyncQuery( + const query = useQuery( GetEquipmentsDocument, variables, - "default", { errorPolicy: "ignore", }, - { - transform: (eqRes) => { - return eqRes.equipments?.map(parseEquipmentWithOEE); - }, - }, ); // Compound status involving both queries. - const status = computed(() => idStatus.value === "success" ? res.status.value : idStatus.value); - const pending = computed(() => idPending.value || res.pending.value); + const error = computed(() => idQuery.query.error.value ?? query.error.value); + const loading = computed(() => idQuery.query.loading.value || query.loading.value); return { - ...res, - status, - pending, - refresh: () => idRefresh().then(() => res.refresh()), + query, + error, + loading, + data: computed(() => query.result.value?.equipments?.map(parseEquipmentWithOEE)), }; } diff --git a/frontend/nuxt.config.ts b/frontend/nuxt.config.ts index 99dd5b3..a7119c5 100644 --- a/frontend/nuxt.config.ts +++ b/frontend/nuxt.config.ts @@ -1,7 +1,5 @@ import vuetify, { transformAssetUrls } from "vite-plugin-vuetify"; -import { GRAPHL_TOKEN_KEY } from "./lib/consts"; - const DESCRIPTION = "CESMII's OEE app provides a glanceable view of a KPI used in many manufacturing environments, and works where ever the OEE Profile is implemented"; @@ -36,7 +34,6 @@ export default defineNuxtConfig({ }); }, "@nuxt/eslint", - "@nuxtjs/apollo", "@vite-pwa/nuxt", ], pwa: { @@ -60,31 +57,8 @@ export default defineNuxtConfig({ ], }, }, - apollo: { - autoImports: true, - clients: { - default: { - httpEndpoint: "https://east.cesmii.net/graphql", - authHeader: "Authorization", - authType: "Bearer", - tokenStorage: "cookie", - tokenName: GRAPHL_TOKEN_KEY, - cookieAttributes: { - sameSite: "strict", - httpOnly: false, - }, - inMemoryCacheOptions: { - typePolicies: { - AttributesGetTimeSeriesRecord: { - keyFields: false, - }, - }, - }, - }, - noauth: { - httpEndpoint: "https://east.cesmii.net/graphql", - }, - }, + routeRules: { + "/": { ssr: false }, }, eslint: { config: { diff --git a/frontend/package.json b/frontend/package.json index 5cf3533..7f234fd 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "smip-oee-dashboard", - "version": "1.0.0", + "version": "0.2.0", "private": true, "type": "module", "contributors": [ @@ -22,14 +22,16 @@ "dependencies": { "@apollo/client": "^3.10.2", "@mdi/font": "^7.4.47", + "@vue/apollo-composable": "^4.2.1", "apexcharts": "^3.49.1", "dayjs": "^1.11.11", "graphql": "^16.8.1", "graphql-tag": "^2.12.6", "jwt-decode": "^4.0.0", "nuxt": "^3.11.2", + "pinia": "^2.3.0", "remeda": "^1.61.0", - "vue": "^3.4.21", + "vue": "^3.4.26", "vue-router": "^4.3.0", "vue3-apexcharts": "^1.5.3", "zod": "^3.23.8" @@ -38,7 +40,6 @@ "@graphql-codegen/cli": "^5.0.2", "@graphql-codegen/typed-document-node": "^5.0.6", "@nuxt/eslint": "^0.3.10", - "@nuxtjs/apollo": "^5.0.0-alpha.14", "@nuxtjs/eslint-config-typescript": "^12.1.0", "@vite-pwa/nuxt": "^0.8.0", "eslint": "^8.57.0", diff --git a/frontend/pages/equipment/[equipmentID].vue b/frontend/pages/equipment/[equipmentID].vue index 577b88f..6bb52a2 100644 --- a/frontend/pages/equipment/[equipmentID].vue +++ b/frontend/pages/equipment/[equipmentID].vue @@ -109,12 +109,14 @@ diff --git a/frontend/pages/login.vue b/frontend/pages/login.vue index 15b1710..ad4ec6d 100644 --- a/frontend/pages/login.vue +++ b/frontend/pages/login.vue @@ -1,5 +1,8 @@