diff --git a/web/packages/scripts/src/cherry-pick.ts b/web/packages/scripts/src/cherry-pick.ts index 1a110b88241..2d424a3cfcf 100644 --- a/web/packages/scripts/src/cherry-pick.ts +++ b/web/packages/scripts/src/cherry-pick.ts @@ -1,11 +1,13 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { execSync } from 'child_process'; +import { execFileSync } from 'child_process'; import * as readline from 'readline'; import * as process from 'process'; import { openBrowser, getBaseUrl } from './git-utils.js'; +const git = (...args: string[]) => execFileSync('git', args, { stdio: 'inherit' }); + // Helper to prompt the user for input. function prompt(question: string): Promise { const rl = readline.createInterface({ @@ -29,9 +31,9 @@ async function handleMergeConflicts() { if (response === 'yes') { try { console.log('Staging resolved changes...'); - execSync('git add .', { stdio: 'inherit' }); + git('add', '.'); console.log('Attempting to continue cherry-pick...'); - execSync('git cherry-pick --continue', { stdio: 'inherit' }); + git('cherry-pick', '--continue'); console.log('Cherry-pick completed successfully after resolving conflicts.'); resolved = true; } catch { @@ -43,7 +45,7 @@ async function handleMergeConflicts() { await prompt('Waiting for you to resolve conflicts. Press enter to check again...'); } else if (response === 'abort') { console.log('Aborting cherry-pick...'); - execSync('git cherry-pick --abort', { stdio: 'inherit' }); + git('cherry-pick', '--abort'); process.exit(1); } else { console.log("Please answer 'yes', 'no', or 'abort'."); @@ -63,20 +65,20 @@ async function main() { try { console.log('Fetching latest changes from origin...'); - execSync('git fetch origin', { stdio: 'inherit' }); + git('fetch', 'origin'); console.log(`Checking out the release branch: ${releaseBranch}`); - execSync(`git checkout ${releaseBranch}`, { stdio: 'inherit' }); - execSync(`git pull origin ${releaseBranch}`, { stdio: 'inherit' }); + git('checkout', releaseBranch); + git('pull', 'origin', releaseBranch); // Create a new branch based on the release branch. const newBranchName = `cherry-pick-${commitHash.substring(0, 7)}`; console.log(`Creating and switching to new branch: ${newBranchName}`); - execSync(`git checkout -b ${newBranchName}`, { stdio: 'inherit' }); + git('checkout', '-b', newBranchName); console.log(`Attempting to cherry-pick commit: ${commitHash}`); try { - execSync(`git cherry-pick ${commitHash}`, { stdio: 'inherit' }); + git('cherry-pick', commitHash); console.log('Cherry-pick completed successfully without conflicts.'); } catch { console.error('Merge conflicts detected during cherry-pick!'); @@ -85,10 +87,10 @@ async function main() { // Push the new branch to origin. console.log(`Pushing branch ${newBranchName} to origin...`); - execSync(`git push origin ${newBranchName}`, { stdio: 'inherit' }); + git('push', 'origin', newBranchName); // Retrieve the remote URL to construct the merge request URL. - const remoteUrlRaw = execSync('git remote get-url origin').toString().trim(); + const remoteUrlRaw = execFileSync('git', ['remote', 'get-url', 'origin']).toString().trim(); const baseUrl = getBaseUrl(remoteUrlRaw); const mergeRequestUrl = `${baseUrl}/-/merge_requests/new?merge_request[source_branch]=${newBranchName}&merge_request[target_branch]=${releaseBranch}`; diff --git a/web/packages/scripts/src/git-utils.ts b/web/packages/scripts/src/git-utils.ts index 4b062a08898..92892d28de6 100644 --- a/web/packages/scripts/src/git-utils.ts +++ b/web/packages/scripts/src/git-utils.ts @@ -1,24 +1,39 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { exec, execSync } from 'child_process'; +import { execFile, execFileSync } from 'child_process'; import * as process from 'process'; /** - * Open a URL in the default browser (works on macOS, Windows, and most Linux distros). + * Open an HTTP/HTTPS URL in the default browser (works on macOS, Windows, and most Linux distros). */ export function openBrowser(url: string): void { - let command = ''; - // Escape the URL to prevent shell interpretation of special characters - const escapedUrl = url.replace(/"/g, '\\"'); + let parsed: URL; + try { + parsed = new URL(url); + } catch { + console.error('Refusing to open invalid URL:', url); + return; + } + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { + console.error('Refusing to open non-http(s) URL:', parsed.protocol); + return; + } + const safeUrl = parsed.toString(); + + let cmd: string; + let args: string[]; if (process.platform === 'darwin') { - command = `open "${escapedUrl}"`; + cmd = 'open'; + args = [safeUrl]; } else if (process.platform === 'win32') { - command = `start "" "${escapedUrl}"`; + cmd = 'rundll32'; + args = ['url.dll,FileProtocolHandler', safeUrl]; } else { - command = `xdg-open "${escapedUrl}"`; + cmd = 'xdg-open'; + args = [safeUrl]; } - exec(command, (error) => { + execFile(cmd, args, (error) => { if (error) { console.error('Failed to open browser:', error); } @@ -56,7 +71,7 @@ export function getBaseUrl(remoteUrl: string): string { // Check if git status is clean (no uncommitted changes) export function isGitStatusClean(): boolean { try { - const status = execSync('git status --porcelain').toString().trim(); + const status = execFileSync('git', ['status', '--porcelain']).toString().trim(); return status === ''; } catch (error) { console.error('Failed to check git status:', error); @@ -67,7 +82,7 @@ export function isGitStatusClean(): boolean { // Get the current branch name export function getCurrentBranch(): string { try { - return execSync('git rev-parse --abbrev-ref HEAD').toString().trim(); + return execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD']).toString().trim(); } catch (error) { console.error('Failed to get current branch:', error); throw error; diff --git a/web/packages/sdk/orval/format-generated.ts b/web/packages/sdk/orval/format-generated.ts index 85b860b8113..c06b20424e7 100755 --- a/web/packages/sdk/orval/format-generated.ts +++ b/web/packages/sdk/orval/format-generated.ts @@ -7,24 +7,34 @@ * Runs prettier and eslint fix on generated API files, and prefixes unused parameters with underscores. */ -import { execSync } from 'child_process'; -import { mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'fs'; +import { mkdirSync, readdirSync, readFileSync, writeFileSync, type Dirent } from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; +import prettier from 'prettier'; +import { serviceConfigs } from './constants'; // Get __dirname equivalent for ES modules const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -// Get the service path from command line args -const servicePath = process.argv[2]; +const ALLOWED_SERVICE_PATHS: ReadonlySet = new Set( + Object.values(serviceConfigs).map((c) => c.path) +); +const rawServicePath = process.argv[2]; -if (!servicePath) { +if (!rawServicePath) { console.error('Error: Service path is required'); console.error('Usage: node format-generated.js '); process.exit(1); } +if (!ALLOWED_SERVICE_PATHS.has(rawServicePath)) { + console.error(`Error: Unknown service path: ${rawServicePath}`); + console.error(`Allowed: ${[...ALLOWED_SERVICE_PATHS].join(', ')}`); + process.exit(1); +} + +const servicePath = rawServicePath; const generatedPath = path.join(__dirname, '..', 'generated', servicePath); console.log(`\nšŸ“ Processing generated files in ${generatedPath}...`); @@ -36,15 +46,14 @@ function getTsFiles(dir: string): string[] { const files: string[] = []; try { - const entries = readdirSync(dir); + const entries = readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { - const fullPath = path.join(dir, entry); - const stat = statSync(fullPath); + const fullPath = path.join(dir, entry.name); - if (stat.isDirectory()) { + if (entry.isDirectory()) { files.push(...getTsFiles(fullPath)); - } else if (entry.endsWith('.ts')) { + } else if (entry.isFile() && entry.name.endsWith('.ts')) { files.push(fullPath); } } @@ -435,53 +444,64 @@ function splitZodTagFile(filePath: string): number { } function splitZodTagFilesIn(zodDir: string): void { - let entries: string[]; + let entries: Dirent[]; try { - entries = readdirSync(zodDir); + entries = readdirSync(zodDir, { withFileTypes: true }); } catch { return; } for (const entry of entries) { - const fullPath = path.join(zodDir, entry); - if (!entry.endsWith('.ts')) continue; - if (!statSync(fullPath).isFile()) continue; + if (!entry.isFile() || !entry.name.endsWith('.ts')) continue; + const fullPath = path.join(zodDir, entry.name); const count = splitZodTagFile(fullPath); if (count > 0) { - console.log(` Split ${entry} into ${count} operation files`); + console.log(` Split ${entry.name} into ${count} operation files`); } } } -try { - // Step 1: Prefix unused parameters with underscore +async function formatWithPrettier(dir: string): Promise { + const entries = readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + await formatWithPrettier(fullPath); + continue; + } + if (!entry.isFile()) continue; + const fileInfo = await prettier.getFileInfo(fullPath); + if (fileInfo.ignored || !fileInfo.inferredParser) continue; + const opts = (await prettier.resolveConfig(fullPath)) ?? {}; + const source = readFileSync(fullPath, 'utf-8'); + const formatted = await prettier.format(source, { ...opts, filepath: fullPath }); + if (formatted !== source) { + writeFileSync(fullPath, formatted, 'utf-8'); + } + } +} + +async function run(): Promise { console.log('Prefixing unused parameters...'); const tsFiles = getTsFiles(generatedPath); let modifiedCount = 0; - for (const file of tsFiles) { if (prefixUnusedParameters(file)) { modifiedCount++; } } - console.log(` Modified ${modifiedCount} file(s)`); - // Step 2: Split large orval-generated zod tag files into per-operation files. - // Each tag file (e.g. zod/evaluator.ts) gets replaced with a barrel and a - // sibling directory of per-operation files (zod/evaluator/.ts). const zodDir = path.join(generatedPath, 'zod'); console.log('Splitting zod tag files by operation...'); splitZodTagFilesIn(zodDir); - // Step 3: Run prettier console.log('Running prettier...'); - execSync(`prettier --write ${generatedPath}`, { - stdio: 'inherit', - cwd: path.join(__dirname, '..'), - }); + await formatWithPrettier(generatedPath); console.log('āœ… Successfully processed generated files\n'); -} catch (error) { +} + +run().catch((error) => { console.error('āŒ Error during processing:', (error as Error).message); process.exit(1); -} +}); diff --git a/web/packages/sdk/orval/generate.ts b/web/packages/sdk/orval/generate.ts index 6645502343f..2b6c57163c1 100644 --- a/web/packages/sdk/orval/generate.ts +++ b/web/packages/sdk/orval/generate.ts @@ -2,20 +2,15 @@ // SPDX-License-Identifier: Apache-2.0 /** - * This script generates the types for the openapi specs. - * - * For private GitHub raw URLs, set the GITHUB_TOKEN environment variable. - * For local files, no token is required. + * This script generates the types for the openapi specs from local YAML files. */ -import { execSync } from 'child_process'; +import { execFileSync } from 'child_process'; import fs from 'fs'; import os from 'os'; import { serviceConfigs } from './constants'; import path from 'path'; import { generateCustomFetcher } from './generateCustomFetcher'; -import { getGithubTokenHeaders } from './githubTokenHeaders'; -const githubToken = process.env.GITHUB_TOKEN; const client = process.env.ORVAL_CLIENT; const service = process.argv[2] as keyof typeof serviceConfigs; @@ -25,21 +20,16 @@ if (!config) { throw new Error('Unsupported OpenAPI Spec.'); } -const getFile = async () => { - if (config.url.startsWith('http')) { - const remoteUrl = new URL(config.url); - const headers = getGithubTokenHeaders(remoteUrl, githubToken); - const res = await fetch(config.url, headers ? { headers } : undefined); - if (Math.floor(res.status / 100) !== 2) { - throw new Error(`${res.status} - Failed to fetch spec. ${res.statusText}`); - } - return await res.text(); - } else { - // Load local file otherwise - const filePath = path.resolve(__dirname, config.url); - const spec = fs.readFileSync(filePath, 'utf8'); - return spec; - } +if (config.url.startsWith('http')) { + throw new Error( + `Remote spec URLs are not supported by this script. Got: ${config.url}. ` + + `Vendor the spec locally and reference it by relative path.` + ); +} + +const getFile = () => { + const filePath = path.resolve(__dirname, config.url); + return fs.readFileSync(filePath, 'utf8'); }; /** @@ -49,14 +39,18 @@ const getFile = async () => { const postProcessZodFiles = (zodPath: string) => { const zodDefaultFile = path.join(__dirname, '..', zodPath); - if (!fs.existsSync(zodDefaultFile)) { - console.log(`Zod file not found at ${zodDefaultFile}, skipping post-processing`); - return; + let content: string; + try { + content = fs.readFileSync(zodDefaultFile, 'utf8'); + } catch (err) { + if ((err as NodeJS.ErrnoException).code === 'ENOENT') { + console.log(`Zod file not found at ${zodDefaultFile}, skipping post-processing`); + return; + } + throw err; } console.log(`Post-processing Zod file: ${zodDefaultFile}`); - - const content = fs.readFileSync(zodDefaultFile, 'utf8'); const lines = content.split('\n'); let fixCount = 0; @@ -109,8 +103,8 @@ const postProcessZodFiles = (zodPath: string) => { const main = async () => { console.log(`Generating types for: ${service}.`); const spec = await getFile(); - const tempFile = path.join(os.tmpdir(), `openapi-spec-${config.path}.yaml`); - const clientVar = client ? `ORVAL_CLIENT=${client}` : ''; + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'openapi-spec-')); + const tempFile = path.join(tempDir, `${config.path}.yaml`); const target = client === 'zod' ? `./generated/${config.path}/zod/index.ts` @@ -123,11 +117,21 @@ const main = async () => { } try { - execSync( - `ORVAL_SERVICE=${service} ORVAL_INPUT=${tempFile} ${clientVar} ORVAL_TARGET=${target} ORVAL_SCHEMAS=./generated/${config.path}/schema pnpm exec orval`, - { - stdio: 'inherit', - } + const orvalEnv: NodeJS.ProcessEnv = { + ...process.env, + ORVAL_SERVICE: service, + ORVAL_INPUT: tempFile, + ORVAL_TARGET: target, + ORVAL_SCHEMAS: `./generated/${config.path}/schema`, + }; + if (client) { + orvalEnv.ORVAL_CLIENT = client; + } + const isWindows = process.platform === 'win32'; + execFileSync( + isWindows ? 'cmd.exe' : 'pnpm', + isWindows ? ['/c', 'pnpm', 'exec', 'orval'] : ['exec', 'orval'], + { stdio: 'inherit', env: orvalEnv } ); // Post-process Zod files if generating with zod client @@ -135,7 +139,7 @@ const main = async () => { postProcessZodFiles(`./generated/${config.path}/zod/default.ts`); } } finally { - fs.unlinkSync(tempFile); + fs.rmSync(tempDir, { recursive: true, force: true }); } }; diff --git a/web/packages/studio/e2e-tests/api/customizations.ts b/web/packages/studio/e2e-tests/api/customizations.ts index d40807d82a9..6f77a1ba39f 100644 --- a/web/packages/studio/e2e-tests/api/customizations.ts +++ b/web/packages/studio/e2e-tests/api/customizations.ts @@ -10,9 +10,7 @@ import { import { APIRequestContext } from '@playwright/test'; export class CustomizationsAPI { - constructor(private request: APIRequestContext) { - this.request = request; - } + constructor(private request: APIRequestContext) {} async createCustomizationJob(data: CustomizationJobInput) { const response = await this.request.post(`${NMP_BASE_URL}/v1/customization/jobs`, { diff --git a/web/packages/studio/e2e-tests/api/datasets.ts b/web/packages/studio/e2e-tests/api/datasets.ts index cf6169ce80b..e5db8195cbb 100644 --- a/web/packages/studio/e2e-tests/api/datasets.ts +++ b/web/packages/studio/e2e-tests/api/datasets.ts @@ -17,9 +17,7 @@ interface Dataset { } export class DatasetsAPI { - constructor(private request: APIRequestContext) { - this.request = request; - } + constructor(private request: APIRequestContext) {} private getFileNameFromPath(filePath: string) { const fileNameParts = filePath.split('/'); diff --git a/web/packages/studio/e2e-tests/api/evaluations.ts b/web/packages/studio/e2e-tests/api/evaluations.ts index 4b80a2bf16b..3737b5a22b4 100644 --- a/web/packages/studio/e2e-tests/api/evaluations.ts +++ b/web/packages/studio/e2e-tests/api/evaluations.ts @@ -10,9 +10,7 @@ type EvaluationConfig = Record; type EvaluationConfigInput = Record; export class EvaluationsAPI { - constructor(private request: APIRequestContext) { - this.request = request; - } + constructor(private request: APIRequestContext) {} async createEvaluationConfig(data: EvaluationConfigInput) { const response = await this.request.post(`${NMP_BASE_URL}/v1/evaluation/configs`, { diff --git a/web/packages/studio/e2e-tests/api/models.ts b/web/packages/studio/e2e-tests/api/models.ts index 635dac2ff5d..df87d6e4a20 100644 --- a/web/packages/studio/e2e-tests/api/models.ts +++ b/web/packages/studio/e2e-tests/api/models.ts @@ -6,9 +6,7 @@ import { CreateModelEntityRequest, ModelEntity } from '@nemo/sdk/generated/platf import { APIRequestContext } from '@playwright/test'; export class ModelsAPI { - constructor(private request: APIRequestContext) { - this.request = request; - } + constructor(private request: APIRequestContext) {} async createModel(workspace: string, data: CreateModelEntityRequest) { const response = await this.request.post(`${NMP_BASE_URL}/v2/workspaces/${workspace}/models`, { diff --git a/web/packages/studio/e2e-tests/api/projects.ts b/web/packages/studio/e2e-tests/api/projects.ts index 9c883e18e3c..17579b29b72 100644 --- a/web/packages/studio/e2e-tests/api/projects.ts +++ b/web/packages/studio/e2e-tests/api/projects.ts @@ -6,9 +6,7 @@ import { ProjectInput, Project, ProjectsPage } from '@nemo/sdk/generated/platfor import { APIRequestContext } from '@playwright/test'; export class ProjectsAPI { - constructor(private request: APIRequestContext) { - this.request = request; - } + constructor(private request: APIRequestContext) {} async createProject(workspace: string, data: ProjectInput) { const response = await this.request.post( diff --git a/web/packages/studio/e2e-tests/pages/project-customizations.ts b/web/packages/studio/e2e-tests/pages/project-customizations.ts index 4019077281e..e0f36d41e00 100644 --- a/web/packages/studio/e2e-tests/pages/project-customizations.ts +++ b/web/packages/studio/e2e-tests/pages/project-customizations.ts @@ -5,9 +5,7 @@ import { CustomizationJob as CustomizationJobOutput } from '@nemo/sdk/vendored/c import { type Page } from '@playwright/test'; export class ProjectCustomizationsPage { - constructor(public readonly page: Page) { - this.page = page; - } + constructor(public readonly page: Page) {} async goto( projectNamespace: string, diff --git a/web/packages/studio/e2e-tests/pages/project-datasets.ts b/web/packages/studio/e2e-tests/pages/project-datasets.ts index a75a1c5743f..5a7d779b77e 100644 --- a/web/packages/studio/e2e-tests/pages/project-datasets.ts +++ b/web/packages/studio/e2e-tests/pages/project-datasets.ts @@ -10,9 +10,7 @@ import path from 'path'; /** Dataset shape for e2e page (files_url, name, etc.). */ type Dataset = { files_url?: string; name?: string; [key: string]: unknown }; export class ProjectDatasetsPage { - constructor(public readonly page: Page) { - this.page = page; - } + constructor(public readonly page: Page) {} private async openQuickActionsMenu(name: string, actionName: string) { const fileRow = await getRowByName(this.page, name); diff --git a/web/packages/studio/e2e-tests/pages/project-evaluations.ts b/web/packages/studio/e2e-tests/pages/project-evaluations.ts index d98aee49230..8d27b8adc02 100644 --- a/web/packages/studio/e2e-tests/pages/project-evaluations.ts +++ b/web/packages/studio/e2e-tests/pages/project-evaluations.ts @@ -5,9 +5,7 @@ import { waitForLongOperation } from '@e2e-tests/utils/pageUtils'; import { expect, type Page } from '@playwright/test'; export class ProjectEvaluationsPage { - constructor(public readonly page: Page) { - this.page = page; - } + constructor(public readonly page: Page) {} async gotoEvaluations(projectNamespace: string, projectName: string) { await this.page.goto(`projects/${projectNamespace}/${projectName}/evaluation/jobs`); diff --git a/web/packages/studio/e2e-tests/pages/project-models.ts b/web/packages/studio/e2e-tests/pages/project-models.ts index a384982c539..f8b3cf2d704 100644 --- a/web/packages/studio/e2e-tests/pages/project-models.ts +++ b/web/packages/studio/e2e-tests/pages/project-models.ts @@ -7,9 +7,7 @@ import { getRowByName } from '@e2e-tests/utils/tables'; import { expect, type Page } from '@playwright/test'; export class ProjectModelsPage { - constructor(public readonly page: Page) { - this.page = page; - } + constructor(public readonly page: Page) {} private async openQuickActionsMenu(modelName: string, actionName: string) { const modelRow = await getRowByName(this.page, modelName); diff --git a/web/packages/studio/e2e-tests/pages/project-safe-synthesizer.ts b/web/packages/studio/e2e-tests/pages/project-safe-synthesizer.ts index ed11b5a1ddc..9b0a0b6f93f 100644 --- a/web/packages/studio/e2e-tests/pages/project-safe-synthesizer.ts +++ b/web/packages/studio/e2e-tests/pages/project-safe-synthesizer.ts @@ -4,9 +4,7 @@ import { type Page } from '@playwright/test'; export class ProjectSafeSynthesizerPage { - constructor(public readonly page: Page) { - this.page = page; - } + constructor(public readonly page: Page) {} async goto(projectNamespace: string, projectName: string) { await this.page.goto(`projects/${projectNamespace}/${projectName}/safe-synthesizer`); diff --git a/web/packages/studio/e2e-tests/pages/projects.ts b/web/packages/studio/e2e-tests/pages/projects.ts index d728c07eb97..e9ad57f31ad 100644 --- a/web/packages/studio/e2e-tests/pages/projects.ts +++ b/web/packages/studio/e2e-tests/pages/projects.ts @@ -8,9 +8,7 @@ import { expect, type Page } from '@playwright/test'; const PROJECTS_PAGE_URL = `projects?sort_by=created_at&order=desc`; export class ProjectsPage { - constructor(public readonly page: Page) { - this.page = page; - } + constructor(public readonly page: Page) {} private async openQuickActionsMenu(projectName: string, actionName: string) { const projectRow = await getRowByName(this.page, projectName); diff --git a/web/packages/studio/scripts/fetch-styles.ts b/web/packages/studio/scripts/fetch-styles.ts index eb5a032c075..ee2c0679298 100644 --- a/web/packages/studio/scripts/fetch-styles.ts +++ b/web/packages/studio/scripts/fetch-styles.ts @@ -14,6 +14,7 @@ import { parse } from 'yaml'; const PKG = '@nvidia/foundations-react-core'; const CDN = 'https://webassets.nvidia.com/kaizen-ui-foundations'; +const CDN_URL = new URL(CDN); const FILES = ['base-external.css', 'components.css']; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -69,7 +70,10 @@ async function readCachedVersion() { } async function fetchCss(version: string, file: string) { - const url = `${CDN}/${version}/${file}`; + const url = new URL(`${encodeURIComponent(version)}/${encodeURIComponent(file)}`, `${CDN}/`); + if (url.protocol !== 'https:' || url.host !== CDN_URL.host) { + throw new Error(`Refusing to fetch from disallowed origin: ${url.origin}`); + } const res = await fetch(url); if (!res.ok) { throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`); diff --git a/web/packages/studio/src/api/intake/utils.ts b/web/packages/studio/src/api/intake/utils.ts index 31eecfdd29c..0793408a42d 100644 --- a/web/packages/studio/src/api/intake/utils.ts +++ b/web/packages/studio/src/api/intake/utils.ts @@ -32,7 +32,7 @@ const processFilterObject = (obj: EntryFilter, params: URLSearchParams, prefix = }); } // Handle nested objects recursively - else if (typeof value === 'object' && value !== null) { + else if (typeof value === 'object') { processFilterObject(value, params, paramKey); } // Handle primitive values (string, number, boolean) diff --git a/web/packages/studio/src/components/PromptTuningForm/InContextLearningSection/hooks/useSubmitICLsFile.ts b/web/packages/studio/src/components/PromptTuningForm/InContextLearningSection/hooks/useSubmitICLsFile.ts index e670c355905..0d1d9272726 100644 --- a/web/packages/studio/src/components/PromptTuningForm/InContextLearningSection/hooks/useSubmitICLsFile.ts +++ b/web/packages/studio/src/components/PromptTuningForm/InContextLearningSection/hooks/useSubmitICLsFile.ts @@ -36,12 +36,9 @@ export const useSubmitICLsFile = ( fileName, }; const hasFileAlready = currentICLs.some((icl) => icl.fileName === fileName); - let combinedICLs = currentICLs; - if (hasFileAlready) { - combinedICLs = currentICLs.map((icl) => (icl.fileName === fileName ? newICL : icl)); - } else { - combinedICLs = [...currentICLs, newICL]; - } + const combinedICLs = hasFileAlready + ? currentICLs.map((icl) => (icl.fileName === fileName ? newICL : icl)) + : [...currentICLs, newICL]; const iclFewShotExamples = combinedICLs.map((icl) => icl.content).join('\n'); const { prompt: compiledSystemPrompt, promptTemplate: newSystemPromptTemplate } = compileSystemPrompt({ diff --git a/web/packages/studio/src/components/ReportTraceModal/utils.ts b/web/packages/studio/src/components/ReportTraceModal/utils.ts index dc2c35c2b8e..173c76a24fb 100644 --- a/web/packages/studio/src/components/ReportTraceModal/utils.ts +++ b/web/packages/studio/src/components/ReportTraceModal/utils.ts @@ -62,7 +62,7 @@ Thanks!`; }; export const formatTags = (tags: TraceData['spans'][number]['tags']) => { - if (tags == null || tags === undefined) { + if (tags == null) { return 'empty'; } diff --git a/web/packages/studio/src/components/evaluation/Configurations/ActionMenu.tsx b/web/packages/studio/src/components/evaluation/Configurations/ActionMenu.tsx index af26541fe48..62c0ad9b231 100644 --- a/web/packages/studio/src/components/evaluation/Configurations/ActionMenu.tsx +++ b/web/packages/studio/src/components/evaluation/Configurations/ActionMenu.tsx @@ -42,7 +42,7 @@ export const ActionMenu: FC = ({ actions, slotTrigger }) => { {actions.map((action, key) => ( - {action.slotIcon && action.slotIcon} + {action.slotIcon} {action.slotLabel} diff --git a/web/packages/studio/src/components/sidePanels/BenchmarkDetailsPanel/index.tsx b/web/packages/studio/src/components/sidePanels/BenchmarkDetailsPanel/index.tsx index 315cca667fd..f0512f5f20e 100644 --- a/web/packages/studio/src/components/sidePanels/BenchmarkDetailsPanel/index.tsx +++ b/web/packages/studio/src/components/sidePanels/BenchmarkDetailsPanel/index.tsx @@ -33,7 +33,7 @@ export const BenchmarkDetailsPanel: FC = ({ const { metrics } = benchmark; if (typeof metrics[0] === 'string') return metrics.join(', '); return metrics - .map((m) => (m && typeof m === 'object' && m !== null && 'name' in m ? String(m.name) : '')) + .map((m) => (m && typeof m === 'object' && 'name' in m ? String(m.name) : '')) .filter(Boolean) .join(', '); })(); diff --git a/web/packages/studio/src/routes/SafeSynthesizerJobReportRoute/util.ts b/web/packages/studio/src/routes/SafeSynthesizerJobReportRoute/util.ts index f4cc7e8fe66..a88de1037f3 100644 --- a/web/packages/studio/src/routes/SafeSynthesizerJobReportRoute/util.ts +++ b/web/packages/studio/src/routes/SafeSynthesizerJobReportRoute/util.ts @@ -12,33 +12,21 @@ export const GRADE_VALUES = { }; export function getDataPrivacyGradeLabel(score: number): string { - if (score < 2) { - return GRADE_VALUES.POOR; - } else if (score < 4) { - return GRADE_VALUES.MODERATE; - } else if (score < 6) { - return GRADE_VALUES.GOOD; - } else if (score < 8) { - return GRADE_VALUES.VERY_GOOD; - } else if (score >= 8) { - return GRADE_VALUES.EXCELLENT; - } - return GRADE_VALUES.UNAVAILABLE; + if (Number.isNaN(score)) return GRADE_VALUES.UNAVAILABLE; + if (score < 2) return GRADE_VALUES.POOR; + if (score < 4) return GRADE_VALUES.MODERATE; + if (score < 6) return GRADE_VALUES.GOOD; + if (score < 8) return GRADE_VALUES.VERY_GOOD; + return GRADE_VALUES.EXCELLENT; } export function getSyntheticQualityGradeLabel(score: number): string { - if (score < 2) { - return GRADE_VALUES.VERY_POOR; - } else if (score < 4) { - return GRADE_VALUES.POOR; - } else if (score < 6) { - return GRADE_VALUES.MODERATE; - } else if (score < 8) { - return GRADE_VALUES.GOOD; - } else if (score >= 8) { - return GRADE_VALUES.EXCELLENT; - } - return GRADE_VALUES.UNAVAILABLE; + if (Number.isNaN(score)) return GRADE_VALUES.UNAVAILABLE; + if (score < 2) return GRADE_VALUES.VERY_POOR; + if (score < 4) return GRADE_VALUES.POOR; + if (score < 6) return GRADE_VALUES.MODERATE; + if (score < 8) return GRADE_VALUES.GOOD; + return GRADE_VALUES.EXCELLENT; } export const GRADE_ORDER = [ diff --git a/web/packages/studio/src/routes/WorkspaceDashboardRoute/index.tsx b/web/packages/studio/src/routes/WorkspaceDashboardRoute/index.tsx index 80147c672f5..57dd3585f87 100644 --- a/web/packages/studio/src/routes/WorkspaceDashboardRoute/index.tsx +++ b/web/packages/studio/src/routes/WorkspaceDashboardRoute/index.tsx @@ -22,11 +22,7 @@ import { import { ROUTES } from '@studio/constants/routes'; import { useWorkspaceFromPath } from '@studio/hooks/useWorkspaceFromPath'; import { useBreadcrumbs } from '@studio/providers/breadcrumbs/useBreadcrumbs'; -import { - getEvaluationMetricsRunRoute, - getModelCompareRoute, - getWorkspaceBaseModelsRoute, -} from '@studio/routes/utils'; +import { getEvaluationMetricsRunRoute, getModelCompareRoute } from '@studio/routes/utils'; import { DashboardCard } from '@studio/routes/WorkspaceDashboardRoute/DashboardCard'; import { ResourcesSection } from '@studio/routes/WorkspaceDashboardRoute/ResourcesSection'; import { Sliders, Boxes } from 'lucide-react'; @@ -67,11 +63,7 @@ export const WorkspaceDashboardRoute: FC = () => { title="Chat with a Model" description="Chat with base models and explore capabilities." actionLabel="Chat" - actionHref={ - MODEL_COMPARE_ENABLED - ? getModelCompareRoute(workspace) - : getWorkspaceBaseModelsRoute(workspace) - } + actionHref={getModelCompareRoute(workspace)} /> )} {/* Fine-tune a Model */} diff --git a/web/packages/studio/src/workers/LargeFileWorker.ts b/web/packages/studio/src/workers/LargeFileWorker.ts index e4a44586654..70f3a678efd 100644 --- a/web/packages/studio/src/workers/LargeFileWorker.ts +++ b/web/packages/studio/src/workers/LargeFileWorker.ts @@ -2,93 +2,45 @@ // SPDX-License-Identifier: Apache-2.0 import { DEFAULT_WORKSPACE } from '@nemo/common/src/models/constants'; -import { filesDownloadFile, filesUploadFile } from '@nemo/sdk/generated/platform/api'; +import { filesDownloadFile } from '@nemo/sdk/generated/platform/api'; import axios from 'axios'; export interface LargeFileWorkerMessage { dataset: string; workspace?: string; - file?: File; - action: 'download' | 'downloadAsFile' | 'upload'; - path?: string; - url?: string; + action: 'downloadAsFile'; + path: string; /** Access token passed from the main thread (localStorage is unavailable in workers). */ accessToken?: string; } /** - * This worker is used to download/upload large files from the server. - * It sends progress updates to the main thread. + * Downloads a file from the NeMo Files service as an ArrayBuffer. + * Goes through the SDK so the request URL is bound to the configured API base, + * not whatever URL the caller passes in. */ self.onmessage = async function (e: MessageEvent) { - const { dataset, workspace, file, action, url, path, accessToken } = e.data; + const { dataset, workspace, action, path, accessToken } = e.data; if (accessToken) { axios.defaults.headers.common['Authorization'] = `Bearer ${accessToken}`; } - switch (action) { - case 'download': { - if (!url) { - self.postMessage({ done: true, error: 'URL is required' }); - break; - } - try { - const response = await fetch(url); - const reader = response.body?.getReader(); - const contentLength = +response.headers.get('Content-Length')!; - let receivedLength = 0; - const chunks = []; - - while (true) { - const { done, value } = await reader!.read(); - if (done) break; - - chunks.push(value); - receivedLength += value.length; + if (action !== 'downloadAsFile') { + self.postMessage({ done: true, error: `Invalid action: ${action}` }); + return; + } - const progress = Math.floor((receivedLength / contentLength) * 100); - self.postMessage({ progress }); - } + if (!path) { + self.postMessage({ done: true, error: 'Path is required' }); + return; + } - const text = chunks.map((chunk: Uint8Array) => new TextDecoder().decode(chunk)).join(''); - self.postMessage({ done: true, text }); - } catch (error) { - self.postMessage({ done: true, error: String(error) }); - } - break; - } - case 'downloadAsFile': { - if (!path) { - self.postMessage({ done: true, error: 'Path is required' }); - break; - } - try { - const response = await filesDownloadFile(workspace || DEFAULT_WORKSPACE, dataset, path); - const arrayBuffer = await response.arrayBuffer(); - self.postMessage({ done: true, arrayBuffer }, { transfer: [arrayBuffer] }); - } catch (error) { - self.postMessage({ done: true, error: String(error) }); - } - break; - } - case 'upload': { - if (!file) { - self.postMessage({ done: true, error: 'File is required' }); - break; - } - try { - const blob = new Blob([await file.arrayBuffer()], { - type: file.type || 'application/octet-stream', - }); - await filesUploadFile(workspace || DEFAULT_WORKSPACE, dataset, file.name, blob); - self.postMessage({ done: true }); - } catch (error) { - self.postMessage({ done: true, error: String(error) }); - } - break; - } - default: - self.postMessage({ done: true, error: `Invalid action: ${action}` }); + try { + const response = await filesDownloadFile(workspace || DEFAULT_WORKSPACE, dataset, path); + const arrayBuffer = await response.arrayBuffer(); + self.postMessage({ done: true, arrayBuffer }, { transfer: [arrayBuffer] }); + } catch (error) { + self.postMessage({ done: true, error: String(error) }); } };