Migrated to NodeJS/Vite/Express/Material UI 2 #1
6
.gitignore
vendored
6
.gitignore
vendored
@@ -12,12 +12,12 @@
|
|||||||
**/playground.py
|
**/playground.py
|
||||||
|
|
||||||
# Azure Secrets and Configuration.
|
# Azure Secrets and Configuration.
|
||||||
/.acr-pat
|
.acr-pat
|
||||||
/azure.env
|
azure.env
|
||||||
|
|
||||||
# MacOS Finder files
|
# MacOS Finder files
|
||||||
**/.DS_Store
|
**/.DS_Store
|
||||||
|
|
||||||
# Node/React rewrite outputs
|
# Node/React rewrite outputs
|
||||||
**/node_modules
|
**/node_modules
|
||||||
/dist
|
**/dist
|
||||||
|
|||||||
@@ -42,27 +42,32 @@ const makeApp = () => {
|
|||||||
app.use(cors());
|
app.use(cors());
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
if (!subscriptionId) {
|
const azure = subscriptionId ? new AzureImageService(subscriptionId) : null;
|
||||||
|
const templates = new TemplateService();
|
||||||
|
|
||||||
app.get("/api/health", (_req, res) => {
|
app.get("/api/health", (_req, res) => {
|
||||||
|
if (!subscriptionId) {
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
status: "error",
|
status: "error",
|
||||||
message: "Missing AZURE_SUBSCRIPTION_ID"
|
message: "Missing AZURE_SUBSCRIPTION_ID"
|
||||||
});
|
});
|
||||||
});
|
return;
|
||||||
|
|
||||||
return { app, port };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const azure = new AzureImageService(subscriptionId);
|
|
||||||
const templates = new TemplateService();
|
|
||||||
|
|
||||||
app.get("/api/health", (_req, res) => {
|
|
||||||
res.json({ status: "ok" });
|
res.json({ status: "ok" });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const requireAzure = (): AzureImageService => {
|
||||||
|
if (!azure) {
|
||||||
|
throw new Error("Missing AZURE_SUBSCRIPTION_ID");
|
||||||
|
}
|
||||||
|
|
||||||
|
return azure;
|
||||||
|
};
|
||||||
|
|
||||||
app.get("/api/locations", async (_req, res, next) => {
|
app.get("/api/locations", async (_req, res, next) => {
|
||||||
try {
|
try {
|
||||||
res.json(await azure.getLocations());
|
res.json(await requireAzure().getLocations());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
@@ -71,7 +76,7 @@ const makeApp = () => {
|
|||||||
app.get("/api/publishers", async (req, res, next) => {
|
app.get("/api/publishers", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { location } = queryLocation.parse(req.query);
|
const { location } = queryLocation.parse(req.query);
|
||||||
res.json(await azure.getPublishers(location));
|
res.json(await requireAzure().getPublishers(location));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
@@ -80,7 +85,7 @@ const makeApp = () => {
|
|||||||
app.get("/api/offers", async (req, res, next) => {
|
app.get("/api/offers", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { location, publisher } = queryOffer.parse(req.query);
|
const { location, publisher } = queryOffer.parse(req.query);
|
||||||
res.json(await azure.getOffers(location, publisher));
|
res.json(await requireAzure().getOffers(location, publisher));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
@@ -89,7 +94,7 @@ const makeApp = () => {
|
|||||||
app.get("/api/skus", async (req, res, next) => {
|
app.get("/api/skus", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { location, publisher, offer } = querySku.parse(req.query);
|
const { location, publisher, offer } = querySku.parse(req.query);
|
||||||
res.json(await azure.getSkus(location, publisher, offer));
|
res.json(await requireAzure().getSkus(location, publisher, offer));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
@@ -98,7 +103,7 @@ const makeApp = () => {
|
|||||||
app.get("/api/versions", async (req, res, next) => {
|
app.get("/api/versions", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { location, publisher, offer, sku } = queryVersion.parse(req.query);
|
const { location, publisher, offer, sku } = queryVersion.parse(req.query);
|
||||||
res.json(await azure.getVersions(location, publisher, offer, sku));
|
res.json(await requireAzure().getVersions(location, publisher, offer, sku));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
@@ -121,7 +126,7 @@ const makeApp = () => {
|
|||||||
app.get("/api/sku-export", async (req, res, next) => {
|
app.get("/api/sku-export", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { location, publisher, offer } = querySku.parse(req.query);
|
const { location, publisher, offer } = querySku.parse(req.query);
|
||||||
const skus = await azure.getSkus(location, publisher, offer);
|
const skus = await requireAzure().getSkus(location, publisher, offer);
|
||||||
res.json({ rendered: templates.buildSkuExport(skus) });
|
res.json({ rendered: templates.buildSkuExport(skus) });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
@@ -151,10 +156,40 @@ const makeApp = () => {
|
|||||||
|
|
||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
const { app, port } = makeApp();
|
const { app, port } = makeApp();
|
||||||
app.listen(port, () => {
|
const server = app.listen(port, () => {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log(`azure-image-chooser listening on ${port}`);
|
console.log(`azure-image-chooser listening on ${port}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let shuttingDown = false;
|
||||||
|
const shutdown = (signal: NodeJS.Signals) => {
|
||||||
|
if (shuttingDown) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
shuttingDown = true;
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(`received ${signal}, shutting down`);
|
||||||
|
server.close((error) => {
|
||||||
|
if (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error("graceful shutdown failed", error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Force-exit if connections do not close in time.
|
||||||
|
setTimeout(() => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error("shutdown timeout reached, forcing exit");
|
||||||
|
process.exit(1);
|
||||||
|
}, 10_000).unref();
|
||||||
|
};
|
||||||
|
|
||||||
|
process.on("SIGTERM", shutdown);
|
||||||
|
process.on("SIGINT", shutdown);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { makeApp };
|
export { makeApp };
|
||||||
|
|||||||
107
app-new/dist/backend/azure-service.js
vendored
107
app-new/dist/backend/azure-service.js
vendored
@@ -1,107 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
exports.AzureImageService = void 0;
|
|
||||||
const arm_compute_1 = require("@azure/arm-compute");
|
|
||||||
const identity_1 = require("@azure/identity");
|
|
||||||
const cache_1 = require("./cache");
|
|
||||||
const version_1 = require("./version");
|
|
||||||
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
||||||
class AzureImageService {
|
|
||||||
subscriptionId;
|
|
||||||
credential = new identity_1.DefaultAzureCredential();
|
|
||||||
computeClient;
|
|
||||||
cache = new cache_1.MemoryCache();
|
|
||||||
constructor(subscriptionId) {
|
|
||||||
this.subscriptionId = subscriptionId;
|
|
||||||
this.computeClient = new arm_compute_1.ComputeManagementClient(this.credential, subscriptionId);
|
|
||||||
}
|
|
||||||
async getLocations() {
|
|
||||||
const cacheKey = "locations";
|
|
||||||
const cached = this.cache.get(cacheKey);
|
|
||||||
if (cached) {
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
const token = await this.credential.getToken("https://management.azure.com/.default");
|
|
||||||
const url = `https://management.azure.com/subscriptions/${this.subscriptionId}/locations?api-version=2022-12-01`;
|
|
||||||
const response = await fetch(url, {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token?.token ?? ""}`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to fetch Azure locations: ${response.status} ${response.statusText}`);
|
|
||||||
}
|
|
||||||
const payload = (await response.json());
|
|
||||||
const allLocations = (payload.value ?? [])
|
|
||||||
.filter((loc) => Boolean(loc.name))
|
|
||||||
.map((loc) => ({
|
|
||||||
name: loc.name,
|
|
||||||
displayName: loc.displayName ?? loc.name,
|
|
||||||
regionType: loc.metadata?.regionType
|
|
||||||
}));
|
|
||||||
const physical = allLocations.filter((loc) => loc.regionType?.toLowerCase() === "physical");
|
|
||||||
const locations = (physical.length > 0 ? physical : allLocations).map((loc) => ({
|
|
||||||
name: loc.name,
|
|
||||||
displayName: loc.displayName
|
|
||||||
}));
|
|
||||||
locations.sort((a, b) => a.name.localeCompare(b.name));
|
|
||||||
this.cache.set(cacheKey, locations, CACHE_TTL_MS);
|
|
||||||
return locations;
|
|
||||||
}
|
|
||||||
async getPublishers(location) {
|
|
||||||
const cacheKey = `publishers:${location}`;
|
|
||||||
const cached = this.cache.get(cacheKey);
|
|
||||||
if (cached) {
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
const response = await this.computeClient.virtualMachineImages.listPublishers(location);
|
|
||||||
const publishers = this.extractNames(response).sort((a, b) => a.localeCompare(b));
|
|
||||||
this.cache.set(cacheKey, publishers, CACHE_TTL_MS);
|
|
||||||
return publishers;
|
|
||||||
}
|
|
||||||
async getOffers(location, publisher) {
|
|
||||||
const cacheKey = `offers:${location}:${publisher}`;
|
|
||||||
const cached = this.cache.get(cacheKey);
|
|
||||||
if (cached) {
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
const response = await this.computeClient.virtualMachineImages.listOffers(location, publisher);
|
|
||||||
const offers = this.extractNames(response).sort((a, b) => a.localeCompare(b));
|
|
||||||
this.cache.set(cacheKey, offers, CACHE_TTL_MS);
|
|
||||||
return offers;
|
|
||||||
}
|
|
||||||
async getSkus(location, publisher, offer) {
|
|
||||||
const cacheKey = `skus:${location}:${publisher}:${offer}`;
|
|
||||||
const cached = this.cache.get(cacheKey);
|
|
||||||
if (cached) {
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
const response = await this.computeClient.virtualMachineImages.listSkus(location, publisher, offer);
|
|
||||||
const skus = this.extractNames(response).sort((a, b) => a.localeCompare(b));
|
|
||||||
this.cache.set(cacheKey, skus, CACHE_TTL_MS);
|
|
||||||
return skus;
|
|
||||||
}
|
|
||||||
async getVersions(location, publisher, offer, sku) {
|
|
||||||
const cacheKey = `versions:${location}:${publisher}:${offer}:${sku}`;
|
|
||||||
const cached = this.cache.get(cacheKey);
|
|
||||||
if (cached) {
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
const response = await this.computeClient.virtualMachineImages.list(location, publisher, offer, sku);
|
|
||||||
const versions = this.extractNames(response);
|
|
||||||
const sorted = (0, version_1.sortImageVersionsIfSemantic)(versions);
|
|
||||||
this.cache.set(cacheKey, sorted, CACHE_TTL_MS);
|
|
||||||
return sorted;
|
|
||||||
}
|
|
||||||
extractNames(source) {
|
|
||||||
const items = Array.isArray(source)
|
|
||||||
? source
|
|
||||||
: typeof source === "object" && source !== null && "value" in source && Array.isArray(source.value)
|
|
||||||
? source.value
|
|
||||||
: [];
|
|
||||||
return items
|
|
||||||
.map((item) => (typeof item === "object" && item !== null && "name" in item ? item.name : undefined))
|
|
||||||
.filter((value) => Boolean(value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
exports.AzureImageService = AzureImageService;
|
|
||||||
24
app-new/dist/backend/cache.js
vendored
24
app-new/dist/backend/cache.js
vendored
@@ -1,24 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
exports.MemoryCache = void 0;
|
|
||||||
class MemoryCache {
|
|
||||||
store = new Map();
|
|
||||||
get(key) {
|
|
||||||
const hit = this.store.get(key);
|
|
||||||
if (!hit) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
if (Date.now() >= hit.expiresAt) {
|
|
||||||
this.store.delete(key);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return hit.value;
|
|
||||||
}
|
|
||||||
set(key, value, ttlMs) {
|
|
||||||
this.store.set(key, {
|
|
||||||
value,
|
|
||||||
expiresAt: Date.now() + ttlMs
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
exports.MemoryCache = MemoryCache;
|
|
||||||
148
app-new/dist/backend/server.js
vendored
148
app-new/dist/backend/server.js
vendored
@@ -1,148 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
||||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
||||||
};
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
exports.makeApp = void 0;
|
|
||||||
const node_fs_1 = require("node:fs");
|
|
||||||
const node_path_1 = require("node:path");
|
|
||||||
const cors_1 = __importDefault(require("cors"));
|
|
||||||
const express_1 = __importDefault(require("express"));
|
|
||||||
const zod_1 = require("zod");
|
|
||||||
const azure_service_1 = require("./azure-service");
|
|
||||||
const template_service_1 = require("./template-service");
|
|
||||||
const findAppNewRoot = () => {
|
|
||||||
const candidates = [(0, node_path_1.join)(__dirname, "../../.."), (0, node_path_1.join)(__dirname, "../..")];
|
|
||||||
for (const candidate of candidates) {
|
|
||||||
if ((0, node_fs_1.existsSync)((0, node_path_1.join)(candidate, "templates.json"))) {
|
|
||||||
return candidate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new Error("Unable to resolve app-new root");
|
|
||||||
};
|
|
||||||
const queryLocation = zod_1.z.object({ location: zod_1.z.string().min(1) });
|
|
||||||
const queryOffer = zod_1.z.object({ location: zod_1.z.string().min(1), publisher: zod_1.z.string().min(1) });
|
|
||||||
const querySku = zod_1.z.object({ location: zod_1.z.string().min(1), publisher: zod_1.z.string().min(1), offer: zod_1.z.string().min(1) });
|
|
||||||
const queryVersion = zod_1.z.object({ location: zod_1.z.string().min(1), publisher: zod_1.z.string().min(1), offer: zod_1.z.string().min(1), sku: zod_1.z.string().min(1) });
|
|
||||||
const renderBody = zod_1.z.object({
|
|
||||||
templateFile: zod_1.z.string().min(1),
|
|
||||||
selection: zod_1.z.object({
|
|
||||||
location: zod_1.z.string().min(1),
|
|
||||||
publisher: zod_1.z.string().min(1),
|
|
||||||
offer: zod_1.z.string().min(1),
|
|
||||||
sku: zod_1.z.string().min(1),
|
|
||||||
version: zod_1.z.string().min(1)
|
|
||||||
})
|
|
||||||
});
|
|
||||||
const makeApp = () => {
|
|
||||||
const app = (0, express_1.default)();
|
|
||||||
const port = Number.parseInt(process.env.PORT ?? "3000", 10);
|
|
||||||
const subscriptionId = process.env.AZURE_SUBSCRIPTION_ID;
|
|
||||||
app.use((0, cors_1.default)());
|
|
||||||
app.use(express_1.default.json());
|
|
||||||
if (!subscriptionId) {
|
|
||||||
app.get("/api/health", (_req, res) => {
|
|
||||||
res.status(500).json({
|
|
||||||
status: "error",
|
|
||||||
message: "Missing AZURE_SUBSCRIPTION_ID"
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return { app, port };
|
|
||||||
}
|
|
||||||
const azure = new azure_service_1.AzureImageService(subscriptionId);
|
|
||||||
const templates = new template_service_1.TemplateService();
|
|
||||||
app.get("/api/health", (_req, res) => {
|
|
||||||
res.json({ status: "ok" });
|
|
||||||
});
|
|
||||||
app.get("/api/locations", async (_req, res, next) => {
|
|
||||||
try {
|
|
||||||
res.json(await azure.getLocations());
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
next(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
app.get("/api/publishers", async (req, res, next) => {
|
|
||||||
try {
|
|
||||||
const { location } = queryLocation.parse(req.query);
|
|
||||||
res.json(await azure.getPublishers(location));
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
next(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
app.get("/api/offers", async (req, res, next) => {
|
|
||||||
try {
|
|
||||||
const { location, publisher } = queryOffer.parse(req.query);
|
|
||||||
res.json(await azure.getOffers(location, publisher));
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
next(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
app.get("/api/skus", async (req, res, next) => {
|
|
||||||
try {
|
|
||||||
const { location, publisher, offer } = querySku.parse(req.query);
|
|
||||||
res.json(await azure.getSkus(location, publisher, offer));
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
next(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
app.get("/api/versions", async (req, res, next) => {
|
|
||||||
try {
|
|
||||||
const { location, publisher, offer, sku } = queryVersion.parse(req.query);
|
|
||||||
res.json(await azure.getVersions(location, publisher, offer, sku));
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
next(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
app.get("/api/templates", (_req, res) => {
|
|
||||||
res.json(templates.getTemplates());
|
|
||||||
});
|
|
||||||
app.post("/api/render", (req, res, next) => {
|
|
||||||
try {
|
|
||||||
const payload = renderBody.parse(req.body);
|
|
||||||
const rendered = templates.render(payload.templateFile, payload.selection);
|
|
||||||
res.json({ rendered });
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
next(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
app.get("/api/sku-export", async (req, res, next) => {
|
|
||||||
try {
|
|
||||||
const { location, publisher, offer } = querySku.parse(req.query);
|
|
||||||
const skus = await azure.getSkus(location, publisher, offer);
|
|
||||||
res.json({ rendered: templates.buildSkuExport(skus) });
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
next(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
app.use((err, _req, res, _next) => {
|
|
||||||
if (err instanceof zod_1.z.ZodError) {
|
|
||||||
res.status(400).json({ message: "Invalid request", issues: err.issues });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const message = err instanceof Error ? err.message : "Unexpected error";
|
|
||||||
res.status(500).json({ message });
|
|
||||||
});
|
|
||||||
const frontendRoot = (0, node_path_1.join)(findAppNewRoot(), "dist/frontend");
|
|
||||||
if ((0, node_fs_1.existsSync)(frontendRoot)) {
|
|
||||||
app.use(express_1.default.static(frontendRoot));
|
|
||||||
app.get(/^(?!\/api).*/, (_req, res) => {
|
|
||||||
res.sendFile((0, node_path_1.join)(frontendRoot, "index.html"));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return { app, port };
|
|
||||||
};
|
|
||||||
exports.makeApp = makeApp;
|
|
||||||
if (require.main === module) {
|
|
||||||
const { app, port } = makeApp();
|
|
||||||
app.listen(port, () => {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log(`azure-image-chooser listening on ${port}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
36
app-new/dist/backend/template-service.js
vendored
36
app-new/dist/backend/template-service.js
vendored
@@ -1,36 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
||||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
||||||
};
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
exports.TemplateService = void 0;
|
|
||||||
const node_fs_1 = require("node:fs");
|
|
||||||
const node_path_1 = require("node:path");
|
|
||||||
const nunjucks_1 = __importDefault(require("nunjucks"));
|
|
||||||
const findAppNewRoot = () => {
|
|
||||||
const candidates = [(0, node_path_1.join)(__dirname, "../../.."), (0, node_path_1.join)(__dirname, "../..")];
|
|
||||||
for (const candidate of candidates) {
|
|
||||||
if ((0, node_fs_1.existsSync)((0, node_path_1.join)(candidate, "templates.json"))) {
|
|
||||||
return candidate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new Error("Unable to resolve app-new template root");
|
|
||||||
};
|
|
||||||
class TemplateService {
|
|
||||||
appNewRoot = findAppNewRoot();
|
|
||||||
env = nunjucks_1.default.configure((0, node_path_1.join)(this.appNewRoot, "templates"), {
|
|
||||||
autoescape: false,
|
|
||||||
noCache: true
|
|
||||||
});
|
|
||||||
templates = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(this.appNewRoot, "templates.json"), "utf8"));
|
|
||||||
getTemplates() {
|
|
||||||
return this.templates;
|
|
||||||
}
|
|
||||||
render(templateFile, selection) {
|
|
||||||
return this.env.render(templateFile, selection);
|
|
||||||
}
|
|
||||||
buildSkuExport(skus) {
|
|
||||||
return `[\n${skus.map((sku) => `\t\"${sku}\"`).join(",\n")}\n]`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
exports.TemplateService = TemplateService;
|
|
||||||
2
app-new/dist/backend/types.js
vendored
2
app-new/dist/backend/types.js
vendored
@@ -1,2 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
21
app-new/dist/backend/version.js
vendored
21
app-new/dist/backend/version.js
vendored
@@ -1,21 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
exports.sortImageVersionsIfSemantic = void 0;
|
|
||||||
const SEMVER_PATTERN = /^[0-9]+\.[0-9]+\.[0-9]+$/;
|
|
||||||
const semverSortKey = (value) => value.split(".").map((part) => Number.parseInt(part, 10));
|
|
||||||
const sortImageVersionsIfSemantic = (versions) => {
|
|
||||||
if (!versions.every((value) => SEMVER_PATTERN.test(value))) {
|
|
||||||
return [...versions].sort((a, b) => a.localeCompare(b, undefined, { numeric: true, sensitivity: "base" }));
|
|
||||||
}
|
|
||||||
return [...versions].sort((a, b) => {
|
|
||||||
const aParts = semverSortKey(a);
|
|
||||||
const bParts = semverSortKey(b);
|
|
||||||
for (let i = 0; i < aParts.length; i += 1) {
|
|
||||||
if (aParts[i] !== bParts[i]) {
|
|
||||||
return aParts[i] - bParts[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
exports.sortImageVersionsIfSemantic = sortImageVersionsIfSemantic;
|
|
||||||
119
app-new/dist/frontend/assets/index-DPvVOxFj.js
vendored
119
app-new/dist/frontend/assets/index-DPvVOxFj.js
vendored
File diff suppressed because one or more lines are too long
12
app-new/dist/frontend/index.html
vendored
12
app-new/dist/frontend/index.html
vendored
@@ -1,12 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Azure Image Chooser</title>
|
|
||||||
<script type="module" crossorigin src="/assets/index-DPvVOxFj.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -36,6 +36,7 @@ const App = () => {
|
|||||||
const [skuExport, setSkuExport] = useState("");
|
const [skuExport, setSkuExport] = useState("");
|
||||||
const [renderError, setRenderError] = useState("");
|
const [renderError, setRenderError] = useState("");
|
||||||
|
|
||||||
|
const healthQuery = useQuery({ queryKey: ["health"], queryFn: api.health });
|
||||||
const locationsQuery = useQuery({ queryKey: ["locations"], queryFn: api.locations });
|
const locationsQuery = useQuery({ queryKey: ["locations"], queryFn: api.locations });
|
||||||
const publishersQuery = useQuery({
|
const publishersQuery = useQuery({
|
||||||
queryKey: ["publishers", selection.location],
|
queryKey: ["publishers", selection.location],
|
||||||
@@ -127,6 +128,8 @@ const App = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const loading = locationsQuery.isLoading || templatesQuery.isLoading;
|
const loading = locationsQuery.isLoading || templatesQuery.isLoading;
|
||||||
|
const healthError = healthQuery.error instanceof Error ? healthQuery.error.message : "";
|
||||||
|
const appNotConfigured = healthError.includes("Missing AZURE_SUBSCRIPTION_ID");
|
||||||
const locationsError = locationsQuery.error instanceof Error ? locationsQuery.error.message : "Failed to load locations";
|
const locationsError = locationsQuery.error instanceof Error ? locationsQuery.error.message : "Failed to load locations";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -140,10 +143,16 @@ const App = () => {
|
|||||||
Select a marketplace image and generate reusable snippets.
|
Select a marketplace image and generate reusable snippets.
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
|
{appNotConfigured ? (
|
||||||
|
<Alert severity="warning">
|
||||||
|
App is not configured. Set AZURE_SUBSCRIPTION_ID (and Azure credentials) in the container start environment, then restart the app.
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<CircularProgress />
|
<CircularProgress />
|
||||||
) : locationsQuery.isError ? (
|
) : locationsQuery.isError ? (
|
||||||
<Alert severity="error">{locationsError}</Alert>
|
<Alert severity={appNotConfigured ? "warning" : "error"}>{locationsError}</Alert>
|
||||||
) : (
|
) : (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const json = async <T>(path: string): Promise<T> => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const api = {
|
export const api = {
|
||||||
|
health: () => json<{ status: string; message?: string }>("/api/health"),
|
||||||
locations: () => json<LocationOption[]>("/api/locations"),
|
locations: () => json<LocationOption[]>("/api/locations"),
|
||||||
publishers: (location: string) => json<string[]>(`/api/publishers?location=${encodeURIComponent(location)}`),
|
publishers: (location: string) => json<string[]>(`/api/publishers?location=${encodeURIComponent(location)}`),
|
||||||
offers: (location: string, publisher: string) =>
|
offers: (location: string, publisher: string) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user