1518 extensibility front add an application section in settings (#15056)

Protected by IS_APPLICATION_ENABLED featureFlag

Add `Application` section in settings

<img width="301" height="137" alt="image"
src="https://github.com/user-attachments/assets/ee53bdd2-36f6-45c6-8646-17b1e08abf00"
/>


A `settings/applications` route listing all installed applications

<img width="661" height="428" alt="image"
src="https://github.com/user-attachments/assets/69d534c4-4e9e-452a-a3d9-ded0223bb457"
/>

Introduce a new Tag for application managed items

<img width="885" height="759" alt="image"
src="https://github.com/user-attachments/assets/19767be5-61e5-4bd2-a51d-54ed9bfb1923"
/>



A `settings/applications/<application_id>` details setting page listing
all objects, serverlessFunctions and agents created by the application:

<img width="917" height="778" alt="image"
src="https://github.com/user-attachments/assets/7fc056a6-1d73-4242-b2eb-6f8955d8597d"
/>

A `settings/applications/<application_id>/<serverless_function_id>`

<img width="905" height="652" alt="image"
src="https://github.com/user-attachments/assets/56ca0021-26bf-42cb-9abf-34879f16050a"
/>

Add trigger tab in serverless function details (readonly for now)

<img width="899" height="724" alt="image"
src="https://github.com/user-attachments/assets/5eeefa35-f2a4-4fd8-a640-7b5c5891f226"
/>

Set object, serverless and agent setting detail pages readonly for
managed items
<img width="1075" height="859" alt="image"
src="https://github.com/user-attachments/assets/57c73d69-4980-47a2-b752-8dc5ab494530"
/>
<img width="648" height="582" alt="image"
src="https://github.com/user-attachments/assets/5ad5f3f7-3bc3-4e40-870a-4981c6492524"
/>
<img width="982" height="692" alt="image"
src="https://github.com/user-attachments/assets/7ad756c4-5d33-4a0a-9eb8-416c040362b9"
/>
<img width="1077" height="647" alt="image"
src="https://github.com/user-attachments/assets/e086b9f5-4062-4d10-82a9-4023de3cad3f"
/>
This commit is contained in:
martmull
2025-10-15 17:13:29 +02:00
committed by GitHub
parent c0ed246a03
commit b16ab1b7c9
109 changed files with 2464 additions and 1357 deletions
@@ -0,0 +1 @@
export type Sources = { [key: string]: string | Sources };
@@ -0,0 +1,146 @@
import { computeNewSources } from '@/serverless-functions/utils/computeNewSources';
describe('computeNewSources', () => {
it('should compute new code input root 0', () => {
const previousCodeInput = {
'index.ts': 'export const toto = () => {}',
};
const filePath = 'index.ts';
const value = 'export const totoUpdated = () => {}';
const expectedResult = {
'index.ts': 'export const totoUpdated = () => {}',
};
expect(
computeNewSources({ previousCode: previousCodeInput, filePath, value }),
).toEqual(expectedResult);
});
it('should compute new code input root 0 file changed', () => {
const previousCodeInput = {
'.env': 'ENV=env',
'index.ts': 'export const toto = () => {}',
};
const filePath = '.env';
const value = 'ENV=env\nENV2=env2';
const expectedResult = {
'.env': 'ENV=env\nENV2=env2',
'index.ts': 'export const toto = () => {}',
};
expect(
computeNewSources({ previousCode: previousCodeInput, filePath, value }),
).toEqual(expectedResult);
});
it('should compute new code input root 0 with multiple files', () => {
const previousCodeInput = {
'index.ts': 'export const toto = () => {}',
'.env': 'ENV',
};
const filePath = 'index.ts';
const value = 'export const totoUpdated = () => {}';
const expectedResult = {
'index.ts': 'export const totoUpdated = () => {}',
'.env': 'ENV',
};
expect(
computeNewSources({ previousCode: previousCodeInput, filePath, value }),
).toEqual(expectedResult);
});
it('should compute new code input root 1', () => {
const previousCodeInput = {
src: { 'index.ts': 'export const toto = () => {}' },
};
const filePath = 'src/index.ts';
const value = 'export const totoUpdated = () => {}';
const expectedResult = {
src: { 'index.ts': 'export const totoUpdated = () => {}' },
};
expect(
computeNewSources({ previousCode: previousCodeInput, filePath, value }),
).toEqual(expectedResult);
});
it('should compute new code input root 1 with multiple files', () => {
const previousCodeInput = {
src: {
'index.ts': 'export const toto = () => {}',
'index2.ts': 'export const toto2 = () => {}',
},
};
const filePath = 'src/index.ts';
const value = 'export const totoUpdated = () => {}';
const expectedResult = {
src: {
'index.ts': 'export const totoUpdated = () => {}',
'index2.ts': 'export const toto2 = () => {}',
},
};
expect(
computeNewSources({ previousCode: previousCodeInput, filePath, value }),
).toEqual(expectedResult);
});
it('should compute new code input root 1 with added files', () => {
const previousCodeInput = {
src: {
'index.ts': 'export const toto = () => {}',
},
};
const filePath = 'src/index2.ts';
const value = 'export const toto2 = () => {}';
const expectedResult = {
src: {
'index.ts': 'export const toto = () => {}',
'index2.ts': 'export const toto2 = () => {}',
},
};
expect(
computeNewSources({ previousCode: previousCodeInput, filePath, value }),
).toEqual(expectedResult);
});
it('should compute new code input multiple roots', () => {
const previousCodeInput = {
'.env': 'ENV=env',
src: { 'index.ts': 'export const toto = () => {}' },
};
const filePath = 'src/index.ts';
const value = 'export const totoUpdated = () => {}';
const expectedResult = {
src: { 'index.ts': 'export const totoUpdated = () => {}' },
'.env': 'ENV=env',
};
expect(
computeNewSources({ previousCode: previousCodeInput, filePath, value }),
).toEqual(expectedResult);
});
});
@@ -0,0 +1,58 @@
// IA Generated
import { type Sources } from '@/serverless-functions/types/sources.type';
import { flattenSources } from '@/serverless-functions/utils/flattenSources';
describe('flattenSources', () => {
it('flattens nested sources with root files', () => {
const input: Sources = {
'.env': 'KEY=VALUE',
src: {
'index.ts': 'export const a = 1',
lib: {
'util.ts': 'export const util = () => {}',
},
},
docs: {
'README.md': '# Hello',
},
};
const result = flattenSources(input);
expect(result).toEqual([
{ path: '.env', content: 'KEY=VALUE' },
{ path: 'docs/README.md', content: '# Hello' },
{ path: 'src/index.ts', content: 'export const a = 1' },
{ path: 'src/lib/util.ts', content: 'export const util = () => {}' },
]);
});
it('handles deep nesting and preserves file contents', () => {
const input: Sources = {
a: { b: { c: { d: { 'file.ts': 'content' } } } },
};
expect(flattenSources(input)).toEqual([
{ path: 'a/b/c/d/file.ts', content: 'content' },
]);
});
it('ignores empty folders and non-string leaves', () => {
const input: Sources = {
empty: {},
weird: {
oops: 42,
} as unknown as Sources,
file: 'ok',
};
const res = flattenSources(input);
expect(res).toEqual([{ path: 'file', content: 'ok' }]);
});
it('accepts a custom basePath prefix', () => {
const input: Sources = { src: { 'index.ts': 'x' } };
const res = flattenSources(input, 'pkg');
expect(res).toEqual([{ path: 'pkg/src/index.ts', content: 'x' }]);
});
});
@@ -0,0 +1,48 @@
import { type Sources } from '@/serverless-functions/types/sources.type';
export const computeNewSources = ({
previousCode,
filePath,
value,
}: {
previousCode: Sources;
filePath: string;
value: string;
}): Sources => {
const result = { ...previousCode };
const parts = filePath.split('/').filter(Boolean);
if (parts.length === 0) {
return result;
}
if (parts.length === 1) {
result[filePath] = value;
return result;
}
const [root, ...rest] = parts;
const newFilePath = rest.join('/');
if (
typeof result?.[root] === 'string' ||
typeof previousCode[root] === 'string'
) {
throw Error('Cannot compute new code input');
}
return {
...previousCode,
[root]: {
...previousCode[root],
...computeNewSources({
previousCode: result?.[root] ?? {},
filePath: newFilePath,
value,
}),
},
};
};
@@ -0,0 +1,28 @@
// IA Generated
import { type Sources } from '@/serverless-functions/types/sources.type';
type FlatSource = { path: string; content: string };
export const flattenSources = (
sources: Sources,
basePath = '',
): FlatSource[] => {
const out: FlatSource[] = [];
const join = (a: string, b: string) => (a ? `${a}/${b}` : b);
const walk = (node: Sources, prefix: string) => {
for (const [name, value] of Object.entries(node)) {
if (typeof value === 'string') {
out.push({ path: join(prefix, name), content: value });
} else if (value && typeof value === 'object') {
walk(value as Sources, join(prefix, name));
}
}
};
walk(sources, basePath);
out.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
return out;
};