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 @@