Skip to content

Commit

Permalink
Merge branch 'main' into MAASENG-4310-update-docs-links
Browse files Browse the repository at this point in the history
  • Loading branch information
ndv99 authored Feb 6, 2025
2 parents c764c03 + bea6910 commit 15b94d6
Show file tree
Hide file tree
Showing 114 changed files with 8,000 additions and 5,072 deletions.
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ VITE_APP_BASENAME=${BASENAME}
VITE_APP_VITE_BASENAME=${VITE_BASENAME}
VITE_APP_WEBSOCKET_DEBUG=false
VITE_APP_USABILLA_ID=fd6cf482fbbb
VITE_APP_MAAS_URL=${MAAS_URL}

# Feature flags

Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/coverage.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ jobs:
# See:
# https://github.com/tiobe/tics-github-action?tab=readme-ov-file#tics-github-action
mode: ${{ github.event_name == 'pull_request' && 'client' || 'qserver' }}
# In case of a scheduled run, we want TiCS to analyze main,
# otherwise the ref (branch/tag) we just pushed to
branchname: ${{ github.event_name == 'schedule' && 'main' || github.ref_name }}
project: maas-ui
viewerUrl: https://canonical.tiobe.com/tiobeweb/TICS/api/cfg?name=default
ticsAuthToken: ${{ secrets.TICSAUTHTOKEN }}
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/pr-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ jobs:
base
controllers
deps
deps-dev
devices
domains
images
Expand Down
1 change: 1 addition & 0 deletions openapi-ts.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ export default defineConfig({
lint: "eslint",
},
experimentalParser: true,
plugins: ["@hey-api/typescript", "@hey-api/sdk", "@tanstack/react-query"],
});
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
"typed-redux-saga": "1.5.0",
"typescript": "5.6.3",
"vanilla-framework": "4.16.0",
"vite": "5.3.6",
"vite": "5.4.13",
"vite-plugin-svgr": "4.2.0",
"vite-tsconfig-paths": "5.1.3",
"yup": "0.32.11"
Expand Down Expand Up @@ -153,7 +153,7 @@
"mock-socket": "9.3.1",
"mockdate": "3.0.5",
"msw": "2.3.1",
"nanoid": "5.0.7",
"nanoid": "5.0.9",
"nodemon": "3.1.3",
"npm-package-json-lint": "8.0.0",
"postcss-normalize": "10.0.1",
Expand All @@ -165,10 +165,10 @@
"storybook": "7.6.19",
"timezone-mock": "1.3.6",
"vite-plugin-eslint": "1.8.1",
"vitest": "1.6.0",
"vitest": "1.6.1",
"vitest-fetch-mock": "0.2.2",
"wait-on": "7.2.0",
"webpack": "5.91.0"
"webpack": "5.94.0"
},
"resolutions": {
"node_modules/@types/react-router-dom/@types/react": "18.3.3",
Expand Down
1 change: 1 addition & 0 deletions scripts/proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ app.use(
onProxyReq(proxyReq) {
// Django's CSRF protection requires requests to come from the correct
// protocol, so this makes XHR requests work when using TLS certs.
proxyReq.setHeader("Origin", `${process.env.MAAS_URL.replace(/\/$/, "")}`);
proxyReq.setHeader("Referer", `${process.env.MAAS_URL}${proxyReq.path}`);
},
secure: false,
Expand Down
4 changes: 0 additions & 4 deletions src/app/Routes.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,6 @@ const routes: { title: string; path: string }[] = [
title: "Zones",
path: urls.zones.index,
},
{
title: "test-zone",
path: urls.zones.details({ id: 1 }),
},
{
title: "Network Discovery",
path: urls.networkDiscovery.index,
Expand Down
11 changes: 1 addition & 10 deletions src/app/Routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ const SubnetDetails = lazy(() => import("@/app/subnets/views/SubnetDetails"));
const SubnetsList = lazy(() => import("@/app/subnets/views/SubnetsList"));
const VLANDetails = lazy(() => import("@/app/subnets/views/VLANDetails"));
const Tags = lazy(() => import("@/app/tags/views/Tags"));
const ZoneDetails = lazy(() => import("@/app/zones/views/ZoneDetails"));
const ZonesList = lazy(() => import("@/app/zones/views/ZonesList"));

const Routes = (): JSX.Element => (
Expand All @@ -52,21 +51,13 @@ const Routes = (): JSX.Element => (
}
path={urls.machines.index}
/>
<Route
element={
<ErrorBoundary>
<ZoneDetails />
</ErrorBoundary>
}
path={`${urls.zones.details(null)}/*`}
/>
<Route
element={
<ErrorBoundary>
<ZonesList />
</ErrorBoundary>
}
path={`${urls.zones.index}/*`}
path={`${urls.zones.index}`}
/>
<Route
element={
Expand Down
23 changes: 9 additions & 14 deletions src/app/api/query/base.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as reactQuery from "@tanstack/react-query";
import type { UseQueryOptions } from "@tanstack/react-query";

import { useWebsocketAwareQuery } from "./base";

Expand All @@ -7,8 +8,7 @@ import { renderHookWithMockStore } from "@/testing/utils";

vi.mock("@tanstack/react-query");

const mockQueryFn = vi.fn();
const mockQueryKey = ["zones"] as const;
const mockOptions = {} as UseQueryOptions;

beforeEach(() => {
vi.resetAllMocks();
Expand All @@ -25,21 +25,16 @@ beforeEach(() => {
});

it("calls useQuery with correct parameters", () => {
renderHookWithMockStore(() =>
useWebsocketAwareQuery(mockQueryKey, mockQueryFn)
);
expect(reactQuery.useQuery).toHaveBeenCalledWith({
queryKey: mockQueryKey,
queryFn: mockQueryFn,
});
renderHookWithMockStore(() => useWebsocketAwareQuery(mockOptions));
expect(reactQuery.useQuery).toHaveBeenCalledWith(mockOptions);
});

it("skips query invalidation when connectedCount is unchanged", () => {
const initialState = rootState({
status: statusState({ connectedCount: 0 }),
});
const { rerender } = renderHookWithMockStore(
() => useWebsocketAwareQuery(mockQueryKey, mockQueryFn),
() => useWebsocketAwareQuery(mockOptions),
{ initialState }
);

Expand All @@ -51,7 +46,7 @@ it("skips query invalidation when connectedCount is unchanged", () => {
mockQueryClient as reactQuery.QueryClient
);

rerender(() => useWebsocketAwareQuery(mockQueryKey, mockQueryFn), {
rerender(() => useWebsocketAwareQuery(mockOptions), {
state: rootState({
status: statusState({ connectedCount: 0 }),
}),
Expand All @@ -64,7 +59,7 @@ it("invalidates queries when connectedCount changes", () => {
status: statusState({ connectedCount: 0 }),
});
const { rerender } = renderHookWithMockStore(
() => useWebsocketAwareQuery(mockQueryKey, mockQueryFn),
() => useWebsocketAwareQuery(mockOptions),
{ initialState }
);

Expand All @@ -76,7 +71,7 @@ it("invalidates queries when connectedCount changes", () => {
mockQueryClient as reactQuery.QueryClient
);

rerender(() => useWebsocketAwareQuery(mockQueryKey, mockQueryFn), {
rerender(() => useWebsocketAwareQuery(mockOptions), {
state: rootState({
status: statusState({ connectedCount: 1 }),
}),
Expand All @@ -86,7 +81,7 @@ it("invalidates queries when connectedCount changes", () => {

it("returns the result of useQuery", () => {
const { result } = renderHookWithMockStore(() =>
useWebsocketAwareQuery(mockQueryKey, mockQueryFn)
useWebsocketAwareQuery(mockOptions)
);
expect(result.current).not.toBeNull();
expect(result.current).toEqual({ data: "testData", isLoading: false });
Expand Down
62 changes: 21 additions & 41 deletions src/app/api/query/base.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { useEffect, useCallback, useContext } from "react";

import { usePrevious } from "@canonical/react-components";
import type { QueryFunction, UseQueryOptions } from "@tanstack/react-query";
import type { UseQueryOptions } from "@tanstack/react-query";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useSelector } from "react-redux";

import type { QueryKey } from "@/app/api/query-client";
import { WebSocketContext } from "@/app/base/websocket-context";
import statusSelectors from "@/app/store/status/selectors";
import type { WebSocketEndpointModel } from "@/websocket-client";
import { WebSocketMessageType } from "@/websocket-client";

/**
Expand Down Expand Up @@ -46,65 +44,47 @@ export const useWebSocket = () => {
return { subscribe };
};

const wsToQueryKeyMapping: Partial<Record<WebSocketEndpointModel, string>> = {
zone: "zones",
// Add more mappings as needed
} as const;

/**
* A function to run a query which invalidates the query cache when a
* websocket message is received, or when the websocket reconnects.
*
* @param queryKey The query key to use
* @param queryFn The query function to run
* @param options Options for useQuery
* @template TQueryFnData The type of the data which the query function will return
* @template TError The type of error the query function might throw
* @template TData The type of query data
* @param options The options for useQuery
* @returns The return value of useQuery
*/
export function useWebsocketAwareQuery<
export const useWebsocketAwareQuery = <
TQueryFnData = unknown,
TError = unknown,
TData = TQueryFnData,
>(
queryKey: QueryKey,
queryFn: QueryFunction<TQueryFnData>,
options?: Omit<
UseQueryOptions<TQueryFnData, TError, TData>,
"queryKey" | "queryFn"
>
) {
options?: UseQueryOptions<TQueryFnData, TError, TData>
) => {
const queryClient = useQueryClient();
const connectedCount = useSelector(statusSelectors.connectedCount);
const { subscribe } = useWebSocket();

const queryModelKey = Array.isArray(queryKey) ? queryKey[0] : "";
const previousConnectedCount = usePrevious(connectedCount);

useEffect(() => {
// connectedCount will change if the websocket reconnects - if this happens, we should invalidate the query
if (connectedCount !== previousConnectedCount) {
queryClient.invalidateQueries({ queryKey });
void queryClient.invalidateQueries({ queryKey: options?.queryKey });
}
}, [connectedCount, previousConnectedCount, queryClient, queryKey]);
}, [connectedCount, previousConnectedCount, queryClient, options]);

useEffect(() => {
// subscribe returns a function to remove the event listener for NOTIFY messages;
// This function will be used as the cleanup function for the effect.
return subscribe(
// This callback function will be called when a NOTIFY message is received
({ name: model }: { action: string; name: WebSocketEndpointModel }) => {
const mappedKey = wsToQueryKeyMapping[model];
const modelQueryKey = queryKey[0];
return subscribe(() => {
// This mapped key is the key for the websocket notifications
// TODO: replace with a function call to deduce the key/condition using the parameters
const mappedKey = "zones";
const modelQueryKey = options?.queryKey[0];

if (mappedKey && mappedKey === modelQueryKey) {
queryClient.invalidateQueries({ queryKey });
}
if (mappedKey && mappedKey === modelQueryKey) {
void queryClient.invalidateQueries({ queryKey: options?.queryKey });
}
);
}, [queryClient, subscribe, queryModelKey, queryKey]);
});
}, [queryClient, subscribe, options]);

return useQuery<TQueryFnData, TError, TData>({
queryKey,
queryFn,
...options,
});
}
return useQuery<TQueryFnData, TError, TData>(options!);
};
Loading

0 comments on commit 15b94d6

Please sign in to comment.