diff --git a/src/constants.ts b/src/constants.ts index 8672b1a12..bc0f8ee02 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -263,3 +263,6 @@ export const PAGINATION_OFFSET = 0; export const PAGINATION_LIMIT = 10; export const PAGINATION_COUNT = 0; export const SEARCH_INPUT = ''; + +export const BLUEPRINTS_DIR = + '.local/share/cockpit/image-builder-frontend/blueprints'; diff --git a/src/mocks/cockpit.ts b/src/mocks/cockpit.ts new file mode 100644 index 000000000..289f8a61f --- /dev/null +++ b/src/mocks/cockpit.ts @@ -0,0 +1,44 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ + +// TODO: maybe we should pull in the cockpit types here +// and keep them in the project. Not ideal because it may +// diverge, so we need to think about it. +export interface UserInfo { + home: string; +} + +export default { + user: (): Promise => { + return new Promise((resolve) => { + resolve({ + home: '', + }); + }); + }, + file: (path: string) => { + return { + read: (): Promise => { + return new Promise((resolve) => { + resolve(''); + }); + }, + close: () => {}, + }; + }, +}; + +export interface FileInfo { + entries?: Record; +} + +export const fsinfo = ( + path: string, + attributes: (keyof FileInfo)[], + options: object +): Promise => { + return new Promise((resolve) => { + resolve({ + entries: {}, + }); + }); +}; diff --git a/src/store/cockpitApi.ts b/src/store/cockpitApi.ts index 6691c54eb..347839cbe 100644 --- a/src/store/cockpitApi.ts +++ b/src/store/cockpitApi.ts @@ -1,18 +1,32 @@ +import TOML from '@ltd/j-toml'; import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; +import cockpit from 'cockpit'; +import { fsinfo } from 'fsinfo'; import { GetArchitecturesApiResponse, GetArchitecturesApiArg, GetBlueprintsApiArg, GetBlueprintsApiResponse, + BlueprintItem, } from './imageBuilderApi'; +import { BLUEPRINTS_DIR } from '../constants'; + const emptyCockpitApi = createApi({ reducerPath: 'cockpitApi', baseQuery: fetchBaseQuery({ baseUrl: '/api' }), endpoints: () => ({}), }); +const getBlueprintsPath = async () => { + const user = await cockpit.user(); + + // we will use the user's `.local` directory + // to save blueprints used for on-prem + return `${user.home}/${BLUEPRINTS_DIR}`; +}; + export const cockpitApi = emptyCockpitApi.injectEndpoints({ endpoints: (builder) => { return { @@ -28,22 +42,50 @@ export const cockpitApi = emptyCockpitApi.injectEndpoints({ GetBlueprintsApiResponse, GetBlueprintsApiArg >({ - queryFn: () => { - // TODO: Add cockpit file api support for reading in blueprints. - // For now we're just hardcoding a dummy response - // so we can render an empty table. - return new Promise((resolve) => { - resolve({ + queryFn: async () => { + try { + const path = await getBlueprintsPath(); + + // we probably don't need any more information other + // than the entries from the directory + const info = await fsinfo(path, ['entries'], { + superuser: 'try', + }); + + const entries = Object.entries(info?.entries || {}); + const blueprints: BlueprintItem[] = await Promise.all( + entries.map(async ([filename]) => { + const file = cockpit.file(`${path}/${filename}`); + + const contents = await file.read(); + const parsed = TOML.parse(contents); + file.close(); + + // TODO: add other blueprint options + return { + name: parsed.name as string, + id: filename as string, + version: parsed.version as number, + description: parsed.description as string, + last_modified_at: Date.now().toString(), + }; + }) + ); + + return { data: { - meta: { count: 0 }, + meta: { count: blueprints.length }, links: { + // TODO: figure out the pagination first: '', last: '', }, - data: [], + data: blueprints, }, - }); - }); + }; + } catch (error) { + return { error }; + } }, }), }; diff --git a/src/test/setup.ts b/src/test/setup.ts index cb4d00fa6..bb74ebf8a 100644 --- a/src/test/setup.ts +++ b/src/test/setup.ts @@ -81,6 +81,28 @@ vi.mock('@unleash/proxy-client-react', () => ({ }), })); +vi.mock(import('cockpit'), async () => { + return { + user: () => { + new Promise((resolve) => { + resolve({ + home: '/home/osbuild', + }); + }); + }, + }; +}); + +vi.mock(import('fsinfo'), async () => { + return { + fsinfo: () => { + new Promise((resolve) => { + resolve({}); + }); + }, + }; +}); + // Remove DOM dump from the testing-library output configure({ getElementError: (message: string) => { diff --git a/tsconfig.json b/tsconfig.json index 2d745d33b..71b024f5e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,6 +19,14 @@ "paths": { "*": [ "./pkg/lib/*" + ], + "cockpit": [ + "./src/mocks/cockpit.ts", + "./pkg/lib/cockpit.ts" + ], + "fsinfo": [ + "./src/mocks/cockpit.ts", + "./pkg/lib/cockpit/fsinfo.ts" ] } } diff --git a/vitest.config.ts b/vitest.config.ts index 99ebe807d..c6bbf65a3 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,4 +1,5 @@ import react from '@vitejs/plugin-react'; +import path from 'path'; const config = { plugins: [react()], @@ -24,6 +25,10 @@ const config = { }, resolve: { mainFields: ['module'], + alias: { + cockpit: path.resolve(__dirname, 'src/mocks/cockpit'), + fsinfo: path.resolve(__dirname, 'src/mocks/cockpit'), + }, }, esbuild: { loader: 'tsx',