Skip to content

Commit

Permalink
Merge pull request #52 from cesmii/feature/custom-graphql-endpoints
Browse files Browse the repository at this point in the history
Custom GraphQL Endpoints
  • Loading branch information
jwise-mfg authored Dec 9, 2024
2 parents f452b52 + 959fb28 commit 2320875
Show file tree
Hide file tree
Showing 18 changed files with 174 additions and 101 deletions.
10 changes: 9 additions & 1 deletion frontend/app.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
<template>
<NuxtPwaManifest/>
<NuxtPwaManifest />
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>

<script setup lang="ts">
import { provideApolloClient } from "./lib/graphql";
provideApolloClient();
</script>
Binary file modified frontend/bun.lockb
Binary file not shown.
2 changes: 1 addition & 1 deletion frontend/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const config: CodegenConfig = {
JSON: "string",
JwtClaim: "string",
UUID: "string",
BitString: "string"
BitString: "string",
},
},
},
Expand Down
7 changes: 2 additions & 5 deletions frontend/layouts/default.vue
Original file line number Diff line number Diff line change
Expand Up @@ -45,20 +45,17 @@
<script setup lang="ts">
import { useTheme } from "vuetify";
import { useGraphQLUser } from "~/lib/auth";
import { logoutGraphQL, useGraphQLUser } from "~/lib/auth";
const route = useRoute();
const theme = useTheme();
const user = useGraphQLUser();
const { onLogout } = useApollo();
function logout() {
onLogout().then(() => {
navigateTo("/login");
});
logoutGraphQL().then(() => navigateTo("/login"));
}
</script>
31 changes: 20 additions & 11 deletions frontend/lib/auth.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -14,7 +14,6 @@ export type OnLoginFn = (token: string | undefined) => Promise<void>;


export interface AuthenticatorLoginInput {
endpoint: string;
authenticator: string;
role: string;
username: string;
Expand All @@ -29,13 +28,7 @@ export interface AuthenticatorLoginInput {
export async function loginWithAuthenticator(
data: AuthenticatorLoginInput,
): Promise<void> {
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,
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -115,3 +113,14 @@ export function useGraphQLUser(): ComputedRef<GraphQLUser | undefined> {
}
});
}



export async function logoutGraphQL() {
const jwtCookie = useCookie(GRAPHL_TOKEN_KEY, {
sameSite: "strict",
httpOnly: false,
});

jwtCookie.value = null;
}
47 changes: 47 additions & 0 deletions frontend/lib/graphql/index.ts
Original file line number Diff line number Diff line change
@@ -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);
}
3 changes: 3 additions & 0 deletions frontend/lib/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./useEquipmentDetailWithOEE";
export * from "./useEquipmentIds";
export * from "./useEquipmentWithOEE";
Original file line number Diff line number Diff line change
@@ -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),
};
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useQuery } from "@vue/apollo-composable";
import { isNonNullish, unique } from "remeda";

import type { GetOeeEquipmentTypesWithEquipmentIdsQuery } from "~/generated/graphql/operations";
Expand Down Expand Up @@ -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)),
};
}

Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -22,34 +19,28 @@ export default function useAsyncEquipmentWithOEE() {
// Making this `computed` so the query is reactive to changes in `equipmentIds`.
const variables = computed<VariablesOf<typeof GetEquipmentsDocument>>(() => ({
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)),
};
}
30 changes: 2 additions & 28 deletions frontend/nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -36,7 +34,6 @@ export default defineNuxtConfig({
});
},
"@nuxt/eslint",
"@nuxtjs/apollo",
"@vite-pwa/nuxt",
],
pwa: {
Expand All @@ -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: {
Expand Down
7 changes: 4 additions & 3 deletions frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "smip-oee-dashboard",
"version": "1.0.0",
"version": "0.2.0",
"private": true,
"type": "module",
"contributors": [
Expand All @@ -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"
Expand All @@ -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",
Expand Down
Loading

0 comments on commit 2320875

Please sign in to comment.