Skip to content

Commit 94a3d23

Browse files
feat(settings): Image source settings to v3 MAASENG-5630 (#5857)
1 parent 743feff commit 94a3d23

File tree

24 files changed

+855
-547
lines changed

24 files changed

+855
-547
lines changed
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import {
2+
useGetImageSource,
3+
useImageSources,
4+
useUpdateImageSource,
5+
} from "./imageSources";
6+
7+
import type { BootSourceRequest } from "@/app/apiclient";
8+
import {
9+
imageSourceResolvers,
10+
mockImageSources,
11+
} from "@/testing/resolvers/imageSources";
12+
import {
13+
renderHookWithProviders,
14+
setupMockServer,
15+
waitFor,
16+
} from "@/testing/utils";
17+
18+
setupMockServer(
19+
imageSourceResolvers.listImageSources.handler(),
20+
imageSourceResolvers.getImageSource.handler(),
21+
imageSourceResolvers.updateImageSource.handler()
22+
);
23+
24+
describe("useImageSources", () => {
25+
it("should return image sources data", async () => {
26+
const { result } = renderHookWithProviders(() => useImageSources());
27+
await waitFor(() => {
28+
expect(result.current.isSuccess).toBe(true);
29+
});
30+
expect(result.current.data?.items).toEqual(mockImageSources.items);
31+
});
32+
});
33+
34+
describe("useGetImageSource", () => {
35+
it("should return the correct image source", async () => {
36+
const expectedImageSource = mockImageSources.items[0];
37+
const { result } = renderHookWithProviders(() =>
38+
useGetImageSource(
39+
{ path: { boot_source_id: expectedImageSource.id } },
40+
true
41+
)
42+
);
43+
await waitFor(() => {
44+
expect(result.current.isSuccess).toBe(true);
45+
});
46+
expect(result.current.data).toMatchObject(expectedImageSource);
47+
});
48+
49+
it("should return error if image source does not exist", async () => {
50+
const { result } = renderHookWithProviders(() =>
51+
useGetImageSource({ path: { boot_source_id: 99 } }, true)
52+
);
53+
await waitFor(() => {
54+
expect(result.current.isError).toBe(true);
55+
});
56+
});
57+
58+
it("should not fetch when enabled is false", async () => {
59+
const { result } = renderHookWithProviders(() =>
60+
useGetImageSource({ path: { boot_source_id: 1 } }, false)
61+
);
62+
// Wait a bit to ensure the query doesn't start
63+
await new Promise((resolve) => setTimeout(resolve, 100));
64+
expect(result.current.isSuccess).toBe(false);
65+
expect(result.current.isLoading).toBe(false);
66+
});
67+
});
68+
69+
describe("useUpdateImageSource", () => {
70+
it("should update an existing image source", async () => {
71+
const updatedImageSource: BootSourceRequest = {
72+
url: "http://updated.images.io/",
73+
keyring_filename: "/usr/share/keyrings/ubuntu-cloudimage-keyring.gpg",
74+
keyring_data: "newdata",
75+
priority: 5,
76+
skip_keyring_verification: false,
77+
};
78+
const { result } = renderHookWithProviders(() => useUpdateImageSource());
79+
result.current.mutate({
80+
body: updatedImageSource,
81+
path: { boot_source_id: 1 },
82+
});
83+
await waitFor(() => {
84+
expect(result.current.isSuccess).toBe(true);
85+
});
86+
});
87+
});

src/app/api/query/imageSources.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { useMutation, useQueryClient } from "@tanstack/react-query";
2+
3+
import { useWebsocketAwareQuery } from "@/app/api/query/base";
4+
import {
5+
mutationOptionsWithHeaders,
6+
queryOptionsWithHeaders,
7+
} from "@/app/api/utils";
8+
import type {
9+
GetBootSourceData,
10+
GetBootSourceErrors,
11+
GetBootSourceResponses,
12+
ListBootSourcesData,
13+
ListBootSourcesErrors,
14+
ListBootSourcesResponses,
15+
Options,
16+
UpdateBootSourceData,
17+
UpdateBootSourceErrors,
18+
UpdateBootSourceResponses,
19+
} from "@/app/apiclient";
20+
import {
21+
updateBootSource,
22+
getBootSource,
23+
listBootSources,
24+
} from "@/app/apiclient";
25+
import {
26+
getBootSourceQueryKey,
27+
listBootSourcesQueryKey,
28+
} from "@/app/apiclient/@tanstack/react-query.gen";
29+
30+
export const useImageSources = (options?: Options<ListBootSourcesData>) => {
31+
return useWebsocketAwareQuery(
32+
queryOptionsWithHeaders<
33+
ListBootSourcesResponses,
34+
ListBootSourcesErrors,
35+
ListBootSourcesData
36+
>(options, listBootSources, listBootSourcesQueryKey(options))
37+
);
38+
};
39+
40+
export const useGetImageSource = (
41+
options: Options<GetBootSourceData>,
42+
enabled: boolean
43+
) => {
44+
return useWebsocketAwareQuery({
45+
...queryOptionsWithHeaders<
46+
GetBootSourceResponses,
47+
GetBootSourceErrors,
48+
GetBootSourceData
49+
>(options, getBootSource, getBootSourceQueryKey(options)),
50+
enabled,
51+
});
52+
};
53+
54+
export const useUpdateImageSource = (
55+
mutationOptions?: Options<UpdateBootSourceData>
56+
) => {
57+
const queryClient = useQueryClient();
58+
return useMutation({
59+
...mutationOptionsWithHeaders<
60+
UpdateBootSourceResponses,
61+
UpdateBootSourceErrors,
62+
UpdateBootSourceData
63+
>(mutationOptions, updateBootSource),
64+
onSuccess: () => {
65+
return queryClient.invalidateQueries({
66+
queryKey: listBootSourcesQueryKey(),
67+
});
68+
},
69+
});
70+
};

src/app/base/components/Placeholder/_index.scss

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
@mixin Placeholder {
22
.p-placeholder {
33
animation: shine 1.5s infinite ease-out;
4-
background-color: $color-mid-x-light;
4+
background-color: var(--vf-color-background-hover);
55
background-image: linear-gradient(
6-
to right,
7-
$color-mid-x-light calc(50% - 2rem),
8-
$color-light 50%,
9-
$color-mid-x-light calc(50% + 2rem)
6+
to right,
7+
var(--vf-color-background-hover) calc(50% - 2rem),
8+
var(--vf-color-background-default) 50%,
9+
var(--vf-color-background-hover) calc(50% + 2rem)
1010
);
1111
background-size: 300% 100%;
1212
color: transparent;

src/app/images/components/SelectUpstreamImagesForm/SelectUpstreamImagesForm.test.tsx

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,20 @@ import { bootResourceActions } from "@/app/store/bootresource";
66
import { BootResourceSourceType } from "@/app/store/bootresource/types";
77
import type { RootState } from "@/app/store/root/types";
88
import * as factory from "@/testing/factories";
9+
import { imageSourceResolvers } from "@/testing/resolvers/imageSources";
910
import {
1011
userEvent,
1112
screen,
1213
within,
1314
renderWithProviders,
15+
setupMockServer,
16+
waitForLoading,
1417
} from "@/testing/utils";
1518

1619
const mockStore = configureStore<RootState>();
20+
const mockServer = setupMockServer(
21+
imageSourceResolvers.listImageSources.handler()
22+
);
1723

1824
describe("SelectUpstreamImagesForm", () => {
1925
const ubuntu = factory.bootResourceUbuntu({
@@ -77,7 +83,7 @@ describe("SelectUpstreamImagesForm", () => {
7783
renderWithProviders(<SelectUpstreamImagesForm />, {
7884
state,
7985
});
80-
86+
await waitForLoading();
8187
const rowUbuntu = within(
8288
screen.getByRole("row", { name: "16.04 LTS", hidden: true })
8389
).getAllByRole("combobox", { hidden: true });
@@ -97,7 +103,7 @@ describe("SelectUpstreamImagesForm", () => {
97103
renderWithProviders(<SelectUpstreamImagesForm />, {
98104
store,
99105
});
100-
106+
await waitForLoading();
101107
await userEvent.click(
102108
screen.getByRole("button", { name: "Save and sync" })
103109
);
@@ -118,17 +124,22 @@ describe("SelectUpstreamImagesForm", () => {
118124
const actualActions = store.getActions();
119125
expect(
120126
actualActions.find((action) => action.type === expectedUbuntuAction.type)
121-
).toStrictEqual(expectedUbuntuAction);
127+
).toMatchObject(expectedUbuntuAction);
122128
expect(
123129
actualActions.find((action) => action.type === expectedOtherAction.type)
124130
).toStrictEqual(expectedOtherAction);
125131
});
126132

127-
it("disables form with a notification if more than one source detected", () => {
128-
const sources = [
129-
factory.bootResourceUbuntuSource(),
130-
factory.bootResourceUbuntuSource(),
131-
];
133+
it("disables form with a notification if more than one source detected", async () => {
134+
mockServer.use(
135+
imageSourceResolvers.listImageSources.handler({
136+
items: [
137+
factory.imageSourceFactory.build(),
138+
factory.imageSourceFactory.build(),
139+
],
140+
total: 2,
141+
})
142+
);
132143
const state = factory.rootState({
133144
bootresource: factory.bootResourceState({
134145
resources: [
@@ -138,14 +149,14 @@ describe("SelectUpstreamImagesForm", () => {
138149
arches: [],
139150
commissioning_series: "focal",
140151
releases: [],
141-
sources,
152+
sources: [], // uses v3
142153
},
143154
}),
144155
});
145156
renderWithProviders(<SelectUpstreamImagesForm />, {
146157
state,
147158
});
148-
159+
await waitForLoading();
149160
expect(
150161
screen.getByText(
151162
"More than one image source exists. The UI does not support updating synced images when more than one source has been defined. Use the API to adjust your sources."

0 commit comments

Comments
 (0)