Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@ import { ConfigFileEntityType } from '@/src/types/config-file-entity';
import { getUserToken } from '@/src/utils/auth/auth-request';
import { getIsEnableAuthToggle } from '@/src/utils/env/get-auth-toggle';
import { PLATFORM_ROOT_FOLDER } from '@/src/utils/files/root-folder';
import { getApp, getApps, getPlatformApplication } from '../actions';
import { getApp, getApps, getConfigFileApplication, getPlatformApplication } from '../actions';

export const dynamic = 'force-dynamic';

export default async function Page(params: {
params: Promise<{ id: string }>;
searchParams: Promise<{ path?: string }>;
searchParams: Promise<{ path?: string; configFile?: string }>;
}) {
const token = await getUserToken(getIsEnableAuthToggle(), headers(), cookies());

Expand All @@ -52,18 +52,25 @@ export default async function Page(params: {

// A `path` query param means this is a public-bucket (versioned, folder-nested) application; its
// absence means a platform-bucket one — flat, identified by name alone (design.md D3/D5).
const rawPath = (await params.searchParams).path;
const searchParams = await params.searchParams;
const rawPath = searchParams.path;
const isConfigFileMode = searchParams.configFile === 'true';
const isPlatformBucket = !rawPath;
const name = decodeURIComponent((await params.params).id);

try {
if (isPlatformBucket) {
const path = `${PLATFORM_ROOT_FOLDER}/${name}`;

app = await getPlatformApplication(path, etag).then((res) => {
etag = res?.etag || DEFAULT_ETAG;
return (res?.response as unknown as AssetApp) || null;
});
if (isConfigFileMode) {
const result = await getConfigFileApplication(name);
app = result.success ? (result.data as unknown as AssetApp) : null;
} else {
const path = `${PLATFORM_ROOT_FOLDER}/${name}`;

app = await getPlatformApplication(path, etag).then((res) => {
etag = res?.etag || DEFAULT_ETAG;
return (res?.response as unknown as AssetApp) || null;
});
}
} else {
const path = decodeURIComponent(rawPath as string);

Expand All @@ -77,11 +84,16 @@ export default async function Page(params: {
) || []) as AssetApp[];
}

models = await getModelsList();
assetRunners = await getAllRunners();
translators = (await getTranslators('')) || [];
applications = await applicationsApi.getApplicationsList(token);
applicationSchemes = await applicationRunnersApi.getApplicationSchemesList(token);

// Admin-backend enrichment only: without DIAL_ADMIN_API_URL there is no host to call, so
// `applications`/`applicationSchemes` stay empty and the page renders on Core-direct data alone.
if (process.env.DIAL_ADMIN_API_URL) {
models = await getModelsList();
applications = await applicationsApi.getApplicationsList(token);
applicationSchemes = await applicationRunnersApi.getApplicationSchemesList(token);
}
} catch (e) {
errorObjLog(e, 'Failed to fetch app view data');
}
Expand All @@ -94,8 +106,8 @@ export default async function Page(params: {
// buckets (not just the platform-bucket Roles tab that uses `roles`) to match the existing
// interceptors read here, which is likewise unconditional.
[roles, interceptors, globalInterceptors] = await Promise.all([
readConfigEntities<DialRole>(token, ConfigFileEntityType.Roles, optionWarnings),
readConfigEntities<DialInterceptor>(token, ConfigFileEntityType.Interceptors, optionWarnings),
readConfigEntities<DialRole>(token, ConfigFileEntityType.Roles, optionWarnings, false),
readConfigEntities<DialInterceptor>(token, ConfigFileEntityType.Interceptors, optionWarnings, false),
readGlobalInterceptors(token, optionWarnings),
]);

Expand All @@ -117,6 +129,7 @@ export default async function Page(params: {
globalInterceptors={globalInterceptors}
translators={translators}
optionWarnings={optionWarnings}
isConfigFileSource={isConfigFileMode}
/>
) : (
<AppView
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';

import { assetApi, externalServiceConsentApi, externalServiceOpsApi, toolsetOpsApi } from '@/src/app/api/api';
import {
assetApi,
configFileApi,
externalServiceConsentApi,
externalServiceOpsApi,
toolsetOpsApi,
} from '@/src/app/api/api';
import * as eximModule from '@/src/server/applications/exim';
import * as zipEximModule from '@/src/server/applications/zip-exim';
import { ConfigFileEntityType } from '@/src/types/config-file-entity';
import { getUserToken } from '@/src/utils/auth/auth-request';
import { getIsEnableAuthToggle } from '@/src/utils/env/get-auth-toggle';
import { RESPONSE_MOCK, TOKEN_MOCK } from '@/src/utils/tests/mock/api.mock';
Expand All @@ -17,6 +24,8 @@ import {
importApps,
exportApps,
getAssetTools,
getConfigFileApplication,
getConfigFileApplications,
signInExternalService,
signOutExternalService,
grantExternalServiceConsent,
Expand Down Expand Up @@ -403,4 +412,24 @@ describe('Assets application :: server actions', () => {
expect(externalServiceConsentApi.withdraw).toHaveBeenCalledWith(TOKEN_MOCK, 'public/als code apps/my app', 'dial');
expect(result).toBe(RESPONSE_MOCK);
});

test('Should call getConfigFileApplications action', async () => {
(configFileApi.listNames as any).mockResolvedValue(RESPONSE_MOCK);

const result = await getConfigFileApplications();

expect(getUserToken).toHaveBeenCalled();
expect(configFileApi.listNames).toHaveBeenCalledWith(TOKEN_MOCK, ConfigFileEntityType.Applications);
expect(result).toBe(RESPONSE_MOCK);
});

test('Should call getConfigFileApplication action', async () => {
(configFileApi.getEntity as any).mockResolvedValue(RESPONSE_MOCK);

const result = await getConfigFileApplication('my-app');

expect(getUserToken).toHaveBeenCalled();
expect(configFileApi.getEntity).toHaveBeenCalledWith(TOKEN_MOCK, ConfigFileEntityType.Applications, 'my-app');
expect(result).toBe(RESPONSE_MOCK);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@

import { cookies, headers } from 'next/headers';

import { assetApi, externalServiceConsentApi, externalServiceOpsApi, toolsetOpsApi } from '@/src/app/api/api';
import {
assetApi,
configFileApi,
externalServiceConsentApi,
externalServiceOpsApi,
toolsetOpsApi,
} from '@/src/app/api/api';
import { ROOT_FOLDER } from '@/src/constants/file';
import {
DialApplicationResource,
Expand All @@ -19,6 +25,7 @@ import { runAssetExportAction, runAssetImportAction } from '@/src/server/assets/
import { moveAssets } from '@/src/server/assets/move';
import { validateApplicationResourceFields } from '@/src/server/core/asset-validation';
import { encodeCorePath, getVersionedName } from '@/src/server/publications/path';
import { ConfigFileEntityType } from '@/src/types/config-file-entity';
import { ImportFileType } from '@/src/types/import';
import { ResourceType } from '@/src/types/resource-type';
import { getUserToken } from '@/src/utils/auth/auth-request';
Expand Down Expand Up @@ -263,3 +270,15 @@ export async function signOutExternalService(appPath: string, serviceId: string,
authenticationType: authType,
});
}

/** `config-file-entity-views`: the application names Core's config file declares. */
export async function getConfigFileApplications() {
const token = await getUserToken(getIsEnableAuthToggle(), headers(), cookies());
return configFileApi.listNames(token, ConfigFileEntityType.Applications);
}

/** `config-file-entity-views`: reads an application by name from Core's config-file population directly. */
export async function getConfigFileApplication(name: string) {
const token = await getUserToken(getIsEnableAuthToggle(), headers(), cookies());
return configFileApi.getEntity<DialApplication>(token, ConfigFileEntityType.Applications, name);
}
15 changes: 10 additions & 5 deletions apps/ai-dial-admin/src/app/[lang]/assets-applications/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { cookies, headers } from 'next/headers';

import { applicationRunnersApi } from '@/src/app/api/api';
import { getAllRunners } from '@/src/app/[lang]/platform-app-runners/actions';
import AppsList from '@/src/components/Assets/Apps/List';
import AppsList from '@/src/components/Assets/Apps/PageList';
import { buildAppRunnerOptions } from '@/src/components/SourceField/Application/utils';
import { AppRunnerOption } from '@/src/components/SourceField/Application/models';
import { SaveValidationContextProvider } from '@/src/context/SaveValidationContext';
Expand All @@ -20,10 +20,15 @@ export default async function Page() {
let runners: DialApplicationScheme[] | null = [];
let assetRunners: ResourceInfo[] = [];

try {
runners = await applicationRunnersApi.getApplicationSchemesList(token);
} catch (e) {
errorObjLog(e, 'Failed to fetch applications data');
// Admin-backend enrichment only: this page is otherwise Core-direct (assetRunners below), so when
// DIAL_ADMIN_API_URL is unset there is no host to call and `runners` stays empty rather than
// attempting a request that would fail.
if (process.env.DIAL_ADMIN_API_URL) {
try {
runners = await applicationRunnersApi.getApplicationSchemesList(token);
} catch (e) {
errorObjLog(e, 'Failed to fetch applications data');
}
}

try {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';

vi.mock('next/headers', () => ({ headers: vi.fn(), cookies: vi.fn() }));
vi.mock('@/src/utils/auth/auth-request', () => ({ getUserToken: vi.fn().mockResolvedValue('token') }));
vi.mock('@/src/utils/env/get-auth-toggle', () => ({ getIsEnableAuthToggle: vi.fn().mockReturnValue(false) }));
vi.mock('@/src/server/logger', () => ({ errorObjLog: vi.fn() }));

vi.mock('@/src/app/api/api', () => ({
applicationRunnersApi: { getApplicationSchemesList: vi.fn().mockResolvedValue([]) },
applicationsApi: { getApplicationsList: vi.fn().mockResolvedValue([]) },
}));
vi.mock('@/src/app/[lang]/platform-app-runners/actions', () => ({ getAllRunners: vi.fn().mockResolvedValue([]) }));
vi.mock('@/src/components/Assets/Apps/PageList', () => ({ __esModule: true, default: () => null }));

vi.mock('@/src/app/[lang]/assets-applications/actions', () => ({
getApp: vi.fn().mockResolvedValue({ etag: 'e', response: { name: 'my-app', folderId: 'f' } }),
getApps: vi.fn().mockResolvedValue([]),
getPlatformApplication: vi.fn().mockResolvedValue({ etag: 'e', response: { name: 'my-app' } }),
}));
vi.mock('@/src/app/[lang]/models/actions', () => ({ getModelsList: vi.fn().mockResolvedValue([]) }));
vi.mock('@/src/app/[lang]/platform-translators/actions', () => ({ getTranslators: vi.fn().mockResolvedValue([]) }));
vi.mock('@/src/server/config-entities/read-page-options', () => ({
readConfigEntities: vi.fn().mockResolvedValue([]),
readGlobalInterceptors: vi.fn().mockResolvedValue([]),
}));
vi.mock('@/src/components/Assets/Apps/View', () => ({ __esModule: true, default: () => null }));
vi.mock('@/src/components/Assets/Platform/Applications/View', () => ({ __esModule: true, default: () => null }));

import { applicationRunnersApi, applicationsApi } from '@/src/app/api/api';
import ListPage from '@/src/app/[lang]/assets-applications/page';
import DetailPage from '@/src/app/[lang]/assets-applications/[id]/page';

describe('Assets > Applications :: admin API gating', () => {
beforeEach(() => {
vi.clearAllMocks();
});

afterEach(() => {
vi.unstubAllEnvs();
});

describe('list page', () => {
test('skips the admin-backend runner-schemes call when DIAL_ADMIN_API_URL is unset', async () => {
vi.stubEnv('DIAL_ADMIN_API_URL', undefined);

await ListPage();

expect(applicationRunnersApi.getApplicationSchemesList).not.toHaveBeenCalled();
});

test('calls the admin-backend runner-schemes call when DIAL_ADMIN_API_URL is set', async () => {
vi.stubEnv('DIAL_ADMIN_API_URL', 'https://admin-be.example.com');

await ListPage();

expect(applicationRunnersApi.getApplicationSchemesList).toHaveBeenCalledOnce();
});
});

describe('detail page', () => {
const renderDetailPage = () =>
DetailPage({
params: Promise.resolve({ id: 'my-app' }),
searchParams: Promise.resolve({}),
});

test('skips both admin-backend calls when DIAL_ADMIN_API_URL is unset', async () => {
vi.stubEnv('DIAL_ADMIN_API_URL', undefined);

await renderDetailPage();

expect(applicationRunnersApi.getApplicationSchemesList).not.toHaveBeenCalled();
expect(applicationsApi.getApplicationsList).not.toHaveBeenCalled();
});

test('calls both admin-backend calls when DIAL_ADMIN_API_URL is set', async () => {
vi.stubEnv('DIAL_ADMIN_API_URL', 'https://admin-be.example.com');

await renderDetailPage();

expect(applicationRunnersApi.getApplicationSchemesList).toHaveBeenCalledOnce();
expect(applicationsApi.getApplicationsList).toHaveBeenCalledOnce();
});
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';

import { AppRunnerOption, AppRunnerOrigin } from '@/src/components/SourceField/Application/models';

Expand All @@ -14,7 +14,7 @@ vi.mock('@/src/app/[lang]/platform-app-runners/actions', () => ({

vi.mock('@/src/utils/auth/auth-request', () => ({ getUserToken: vi.fn().mockResolvedValue('token') }));
vi.mock('@/src/utils/env/get-auth-toggle', () => ({ getIsEnableAuthToggle: () => false }));
vi.mock('@/src/components/Assets/Apps/List', () => ({ __esModule: true, default: () => null }));
vi.mock('@/src/components/Assets/Apps/PageList', () => ({ __esModule: true, default: () => null }));

import { getAllRunners } from '@/src/app/[lang]/platform-app-runners/actions';
import { applicationRunnersApi } from '@/src/app/api/api';
Expand All @@ -29,6 +29,13 @@ const runnersPassedToList = (tree: any): AppRunnerOption[] => tree.props.childre
describe('Assets > Applications :: runner sources', () => {
beforeEach(() => {
vi.clearAllMocks();
// These cases exercise the admin-BE runner-schemes read itself, so the admin API is on here;
// the on/off gating of that read is covered separately in admin-api-gating.spec.tsx.
vi.stubEnv('DIAL_ADMIN_API_URL', 'https://admin-be.example.com');
});

afterEach(() => {
vi.unstubAllEnvs();
});

test('merges both populations into the picker options', async () => {
Expand Down
23 changes: 15 additions & 8 deletions apps/ai-dial-admin/src/app/[lang]/assets-toolsets/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ import { ConfigFileEntityType } from '@/src/types/config-file-entity';
import { getUserToken } from '@/src/utils/auth/auth-request';
import { getIsEnableAuthToggle } from '@/src/utils/env/get-auth-toggle';
import { PLATFORM_ROOT_FOLDER } from '@/src/utils/files/root-folder';
import { getPlatformToolset, getToolset, getToolsets } from '../actions';
import { getConfigFileToolset, getPlatformToolset, getToolset, getToolsets } from '../actions';

export const dynamic = 'force-dynamic';

export default async function Page(params: {
params: Promise<{ id: string }>;
searchParams: Promise<{ path?: string; code?: string }>;
searchParams: Promise<{ path?: string; code?: string; configFile?: string }>;
}) {
const token = await getUserToken(getIsEnableAuthToggle(), headers(), cookies());

Expand All @@ -38,17 +38,23 @@ export default async function Page(params: {
const searchParams = await params.searchParams;
oAuthCode = searchParams.code;
const rawPath = searchParams.path;
const isConfigFileMode = searchParams.configFile === 'true';
const isPlatformBucket = !rawPath;
const name = decodeURIComponent((await params.params).id);

try {
if (isPlatformBucket) {
const path = `${PLATFORM_ROOT_FOLDER}/${name}`;
if (isConfigFileMode) {
const result = await getConfigFileToolset(name);
toolset = result.success ? (result.data as unknown as AssetToolset) : null;
} else {
const path = `${PLATFORM_ROOT_FOLDER}/${name}`;

toolset = await getPlatformToolset(path, etag).then((res) => {
etag = res?.etag || DEFAULT_ETAG;
return (res?.response as unknown as AssetToolset) || null;
});
toolset = await getPlatformToolset(path, etag).then((res) => {
etag = res?.etag || DEFAULT_ETAG;
return (res?.response as unknown as AssetToolset) || null;
});
}
} else {
const path = decodeURIComponent(rawPath as string);

Expand All @@ -69,7 +75,7 @@ export default async function Page(params: {
// interceptors read on the sibling assets-applications page — an option-list problem must not
// prevent the toolset from loading. Core-direct (`readConfigEntities`), not the admin-BE role list,
// which cannot see a role declared only in Core's configuration file.
roles = await readConfigEntities<DialRole>(token, ConfigFileEntityType.Roles, optionWarnings);
roles = await readConfigEntities<DialRole>(token, ConfigFileEntityType.Roles, optionWarnings, false);

if (toolset == null) {
notFound();
Expand All @@ -84,6 +90,7 @@ export default async function Page(params: {
originalToolset={toolset}
roles={roles}
optionWarnings={optionWarnings}
isConfigFileSource={isConfigFileMode}
/>
) : (
<ToolsetView oAuthCode={oAuthCode} etag={etag} originalToolset={toolset} toolsets={toolsets || []} />
Expand Down
Loading
Loading