Fixes to NodeJS version.
This commit is contained in:
16
app-new/frontend/package-lock.json
generated
16
app-new/frontend/package-lock.json
generated
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user