Fixes to NodeJS version.

This commit is contained in:
2026-04-19 20:47:47 +02:00
parent aca4998da7
commit 176fa5ead2
28 changed files with 686 additions and 134 deletions

View File

@@ -4334,22 +4334,6 @@
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
"dev": true,
"license": "MIT"
},
"node_modules/yaml": {
"version": "2.8.3",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
"integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==",
"extraneous": true,
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
},
"funding": {
"url": "https://github.com/sponsors/eemeli"
}
}
}
}

View File

@@ -1,6 +1,7 @@
import { useEffect, useMemo, useState } from "react";
import {
Alert,
Autocomplete,
Box,
Button,
Card,
@@ -13,6 +14,7 @@ import {
MenuItem,
Select,
Stack,
TextField,
Typography
} from "@mui/material";
import { useQuery } from "@tanstack/react-query";
@@ -84,6 +86,32 @@ const App = () => {
[selection, templateFile]
);
const publishers = useMemo(
() => [...(publishersQuery.data ?? [])].sort((a, b) => a.localeCompare(b)),
[publishersQuery.data]
);
const offers = useMemo(
() => [...(offersQuery.data ?? [])].sort((a, b) => a.localeCompare(b)),
[offersQuery.data]
);
const skus = useMemo(
() => [...(skusQuery.data ?? [])].sort((a, b) => a.localeCompare(b)),
[skusQuery.data]
);
const versions = useMemo(
() => [...(versionsQuery.data ?? [])].sort((a, b) => a.localeCompare(b, undefined, { numeric: true, sensitivity: "base" })),
[versionsQuery.data]
);
const containsFilter = (options: string[], inputValue: string) => {
const query = inputValue.trim().toLowerCase();
if (!query) {
return options;
}
return options.filter((option) => option.toLowerCase().includes(query));
};
const onRender = async () => {
setRenderError("");
try {
@@ -99,12 +127,13 @@ const App = () => {
};
const loading = locationsQuery.isLoading || templatesQuery.isLoading;
const locationsError = locationsQuery.error instanceof Error ? locationsQuery.error.message : "Failed to load locations";
return (
<Box sx={{ minHeight: "100vh", py: 6 }}>
<Box sx={{ minHeight: "100vh", py: 4 }}>
<Container maxWidth="lg">
<Stack spacing={3}>
<Typography variant="h3" sx={{ fontWeight: 700 }}>
<Typography variant="h4" sx={{ fontWeight: 500 }}>
Azure Image Chooser
</Typography>
<Typography color="text.secondary">
@@ -113,6 +142,8 @@ const App = () => {
{loading ? (
<CircularProgress />
) : locationsQuery.isError ? (
<Alert severity="error">{locationsError}</Alert>
) : (
<Card>
<CardContent>
@@ -136,57 +167,36 @@ const App = () => {
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<FormControl fullWidth disabled={!selection.location}>
<InputLabel id="publisher-label">Publisher</InputLabel>
<Select
labelId="publisher-label"
label="Publisher"
value={selection.publisher}
onChange={(event) => setSelection((prev) => ({ ...prev, publisher: event.target.value }))}
>
{(publishersQuery.data ?? []).map((publisher) => (
<MenuItem key={publisher} value={publisher}>
{publisher}
</MenuItem>
))}
</Select>
</FormControl>
<Autocomplete
options={publishers}
value={selection.publisher || null}
disabled={!selection.location}
onChange={(_event, value) => setSelection((prev) => ({ ...prev, publisher: value ?? "" }))}
filterOptions={(options, state) => containsFilter(options, state.inputValue)}
renderInput={(params) => <TextField {...params} label="Publisher" />}
/>
</Grid>
<Grid size={{ xs: 12, md: 4 }}>
<FormControl fullWidth disabled={!selection.publisher}>
<InputLabel id="offer-label">Offer</InputLabel>
<Select
labelId="offer-label"
label="Offer"
value={selection.offer}
onChange={(event) => setSelection((prev) => ({ ...prev, offer: event.target.value }))}
>
{(offersQuery.data ?? []).map((offer) => (
<MenuItem key={offer} value={offer}>
{offer}
</MenuItem>
))}
</Select>
</FormControl>
<Autocomplete
options={offers}
value={selection.offer || null}
disabled={!selection.publisher}
onChange={(_event, value) => setSelection((prev) => ({ ...prev, offer: value ?? "" }))}
filterOptions={(options, state) => containsFilter(options, state.inputValue)}
renderInput={(params) => <TextField {...params} label="Offer" />}
/>
</Grid>
<Grid size={{ xs: 12, md: 4 }}>
<FormControl fullWidth disabled={!selection.offer}>
<InputLabel id="sku-label">SKU</InputLabel>
<Select
labelId="sku-label"
label="SKU"
value={selection.sku}
onChange={(event) => setSelection((prev) => ({ ...prev, sku: event.target.value }))}
>
{(skusQuery.data ?? []).map((sku) => (
<MenuItem key={sku} value={sku}>
{sku}
</MenuItem>
))}
</Select>
</FormControl>
<Autocomplete
options={skus}
value={selection.sku || null}
disabled={!selection.offer}
onChange={(_event, value) => setSelection((prev) => ({ ...prev, sku: value ?? "" }))}
filterOptions={(options, state) => containsFilter(options, state.inputValue)}
renderInput={(params) => <TextField {...params} label="SKU" />}
/>
</Grid>
<Grid size={{ xs: 12, md: 4 }}>
@@ -198,7 +208,7 @@ const App = () => {
value={selection.version}
onChange={(event) => setSelection((prev) => ({ ...prev, version: event.target.value }))}
>
{(versionsQuery.data ?? []).map((version) => (
{versions.map((version) => (
<MenuItem key={version} value={version}>
{version}
</MenuItem>
@@ -226,7 +236,7 @@ const App = () => {
</Grid>
<Grid size={{ xs: 12, md: 4 }}>
<Button fullWidth variant="contained" sx={{ height: "100%" }} onClick={onRender} disabled={!canRender}>
<Button fullWidth variant="contained" sx={{ height: "100%" }} color="primary" onClick={onRender} disabled={!canRender}>
Generate usage
</Button>
</Grid>
@@ -241,7 +251,10 @@ const App = () => {
<Card>
<CardContent>
<Typography variant="h6">Usage snippet</Typography>
<Box component="pre" sx={{ overflowX: "auto", p: 2, bgcolor: "#0f172a", color: "#e2e8f0", borderRadius: 2 }}>
<Box
component="pre"
sx={{ overflowX: "auto", p: 2, bgcolor: "grey.100", color: "text.primary", borderRadius: 1, border: 1, borderColor: "divider" }}
>
{renderedUsage}
</Box>
</CardContent>
@@ -252,7 +265,10 @@ const App = () => {
<Card>
<CardContent>
<Typography variant="h6">Available SKUs</Typography>
<Box component="pre" sx={{ overflowX: "auto", p: 2, bgcolor: "#111827", color: "#d1fae5", borderRadius: 2 }}>
<Box
component="pre"
sx={{ overflowX: "auto", p: 2, bgcolor: "grey.100", color: "text.primary", borderRadius: 1, border: 1, borderColor: "divider" }}
>
{skuExport}
</Box>
</CardContent>

View File

@@ -3,7 +3,15 @@ import type { LocationOption, SelectionState, UsageTemplate } from "./types";
const json = async <T>(path: string): Promise<T> => {
const response = await fetch(path);
if (!response.ok) {
throw new Error(`Request failed for ${path}`);
let details = "";
try {
const payload = (await response.json()) as { message?: string };
details = payload.message ?? "";
} catch {
// Ignore non-JSON error payloads.
}
throw new Error(details || `Request failed for ${path} (${response.status})`);
}
return (await response.json()) as T;

View File

@@ -1,33 +1,16 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { CssBaseline, ThemeProvider, createTheme } from "@mui/material";
import { CssBaseline } from "@mui/material";
import App from "./App";
const queryClient = new QueryClient();
const theme = createTheme({
palette: {
mode: "light",
primary: { main: "#00695f" },
secondary: { main: "#0d47a1" },
background: {
default: "#f4f8f7",
paper: "#ffffff"
}
},
shape: {
borderRadius: 12
}
});
createRoot(document.getElementById("root")!).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<ThemeProvider theme={theme}>
<CssBaseline />
<App />
</ThemeProvider>
<CssBaseline />
<App />
</QueryClientProvider>
</StrictMode>
);

View File

@@ -32,10 +32,17 @@ describe("App", () => {
});
it("renders application heading", async () => {
const client = new QueryClient();
const client = new QueryClient({
defaultOptions: {
queries: {
retry: false,
gcTime: 0
}
}
});
const theme = createTheme();
render(
const view = render(
<QueryClientProvider client={client}>
<ThemeProvider theme={theme}>
<App />
@@ -44,5 +51,7 @@ describe("App", () => {
);
expect(await screen.findByText("Azure Image Chooser")).toBeInTheDocument();
view.unmount();
client.clear();
});
});

View File

@@ -4,7 +4,7 @@ import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
build: {
outDir: "../../dist/frontend",
outDir: "../dist/frontend",
emptyOutDir: true
},
server: {