diff --git a/resources/output_schema.json b/resources/output_schema.json index f7debae..1b2e2b3 100644 --- a/resources/output_schema.json +++ b/resources/output_schema.json @@ -176,6 +176,9 @@ "defaultBranch": { "type": "string" }, + "defaultNcs" : { + "type": "string" + }, "lastUpdate": { "type": "string", "format": "date-time" diff --git a/scripts/generate-index-json.ts b/scripts/generate-index-json.ts index 0a4ce3b..3cf8d86 100644 --- a/scripts/generate-index-json.ts +++ b/scripts/generate-index-json.ts @@ -11,6 +11,8 @@ import path from 'path'; import { Octokit } from '@octokit/rest'; import fetch from 'node-fetch'; import colours from 'ansi-colors'; +import * as yaml from "yaml"; +import WestEnv from './west'; import type { OrgIndex, @@ -24,6 +26,7 @@ import { execSync } from 'child_process'; const nordicOrgs: string[] = ['nrfconnect', 'nordic', 'nordicplayground']; const partnerOrgs: string[] = ['golioth']; +const tempAppDir: string = 'apps'; function notUndefined(value: T | undefined): value is T { return value !== undefined; @@ -122,6 +125,32 @@ async function fetchRepoData( repo: app.name, }); + const repoUrl = `https://github.com/${orgId}/${app.name}`; + + const westEnv = new WestEnv(repoUrl, repoData.default_branch, `${tempAppDir}/${app.name}`); + + const ncsVersions: Map = new Map(); + + try { + await westEnv.init(); + const nrfRevision = await westEnv.getModuleRev("nrf"); + ncsVersions.set(repoData.default_branch, nrfRevision ?? ""); + console.log(`nrfRevision: ${nrfRevision}`); + } catch (e) { + console.log(`West env failed: ${e}`); + } + + const revs = [...releases.data.map((release) => release.tag_name)]; + for (const rev of revs) { + try { + await westEnv.checkout(rev); + const ncsVer = await westEnv.getModuleRev("nrf") ?? ""; + ncsVersions.set(rev, ncsVer); + } catch (e) { + console.log(`Checkout failed: ${e}`); + } + } + console.log(colours.green(`Fetched data for ${orgId}/${app.name}`)); return { @@ -132,6 +161,7 @@ async function fetchRepoData( name: app.name, title: app.title, defaultBranch: repoData.default_branch, + defaultNcs: ncsVersions.get(repoData.default_branch) ?? "", isTemplate: repoData.is_template ?? false, kind: app.kind, lastUpdate: repoData.updated_at, @@ -144,6 +174,7 @@ async function fetchRepoData( date: release.created_at, name: release.name ?? release.tag_name, tag: release.tag_name, + ncs: ncsVersions.get(release.tag_name) ?? "" })), tags: app.tags, }; @@ -152,12 +183,22 @@ async function fetchRepoData( } } +async function cleanup() { + try { + await fs.access(tempAppDir); + await fs.rm(tempAppDir, { recursive: true}); + } catch { + // No need to remove the directory + } +} + async function run() { const orgIndices = await readOrgIndexFiles(); const appIndex = await generateIndex(orgIndices); const stringified = JSON.stringify(appIndex, undefined, 2); const indexPath = path.join(__dirname, '..', 'resources', 'index.json'); await fs.writeFile(indexPath, stringified); + await cleanup(); console.log(`\nWritten app index to ${indexPath}`); } diff --git a/scripts/west.ts b/scripts/west.ts new file mode 100644 index 0000000..b754a89 --- /dev/null +++ b/scripts/west.ts @@ -0,0 +1,63 @@ +/* Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import { exec } from 'child_process'; +import fs from 'fs/promises'; +import { promisify } from 'util'; + +const execAsync = promisify(exec); + +async function exists(path: string): Promise { + try { + await fs.access(path); + return true; + } catch { + return false; + } +} + +class WestEnv { + + constructor(private repoUrl: string, private revision: string, private appPath: string) {} + + async init(): Promise { + try { + const dirExists = await exists(this.appPath); + if (!dirExists) { + await fs.mkdir(this.appPath, { recursive: true }); + } + await execAsync( + `cd ${this.appPath} && west init -m ${this.repoUrl} --mr ${this.revision} && west update` + ) + } catch(err) { + Promise.reject(`west init failed with err: ${err}`); + } + } + + async checkout(revision: string): Promise { + + const {stdout} = await execAsync(`cd ${this.appPath} && west config manifest.path`); + const manifestRepoPath = `${this.appPath}/${stdout.trim()}`; + + await execAsync(`cd ${manifestRepoPath} && git checkout ${revision} && west update`) + + this.revision = revision; + } + + get manifestRev(): string { + return this.revision; + } + + async getModuleRev(module: string): Promise { + + let {stdout} = await execAsync( + `cd ${this.appPath} && west list --all --format "{name};{revision}" | grep "${module};"` + ); + + return stdout.split(";")[1]?.trim(); + } +} + +export default WestEnv; diff --git a/site/src/app/Header.tsx b/site/src/app/Header.tsx index 7e0467c..33525b4 100644 --- a/site/src/app/Header.tsx +++ b/site/src/app/Header.tsx @@ -39,8 +39,8 @@ interface Props { } function Header(props: Props): JSX.Element { - function handleTextSearchChange(e: ChangeEvent) { - props.dispatchFilters({ type: 'textSearch', payload: e.target.value }); + function handleSearch(type: 'appSearch' | 'ncsSearch') { + return (e: ChangeEvent) => { props.dispatchFilters({ type: type, payload: e.target.value }); } } const aboutIcon = ( @@ -94,25 +94,44 @@ function Header(props: Props): JSX.Element {
- +
+ + +
- +
+ + +
); } diff --git a/site/src/app/Root.tsx b/site/src/app/Root.tsx index 5d3ef0a..a85c4aa 100644 --- a/site/src/app/Root.tsx +++ b/site/src/app/Root.tsx @@ -51,10 +51,15 @@ function Root(props: Props) { useEffect(() => { const searchParams = new URLSearchParams(window.location.search); - const appFilter = searchParams.get("app"); + const app = searchParams.get("app"); + const ncs = searchParams.get("ncs"); - if (appFilter) { - dispatchFilters({ type: 'textSearch', payload: appFilter }); + if (app) { + dispatchFilters({ type: 'appSearch', payload: app }); + } + + if (ncs) { + dispatchFilters({ type: 'ncsSearch', payload: ncs }); } }, []); diff --git a/site/src/app/filters.ts b/site/src/app/filters.ts index 22ccc38..a8ebaff 100644 --- a/site/src/app/filters.ts +++ b/site/src/app/filters.ts @@ -7,39 +7,60 @@ import Fuse from 'fuse.js'; import { NormalisedApp } from '../schema'; -export interface Filters { - textSearch: string; + +function filterAppName(apps: NormalisedApp[], search: string): NormalisedApp[] { + const fuse = new Fuse(apps, { keys: ['owner.name', 'title', 'description', 'name', 'tags'] }); + const results = fuse.search(search); + return results.map((result) => result.item); +} + +function filterNcsVersion(apps: NormalisedApp[], search: string): NormalisedApp[] { + const exactMatches: NormalisedApp[] = []; + + for (const app of apps) { + for (const release of app.releases) { + if (release.ncs.includes(search)) { + exactMatches.push(app); + break; + } + } + } + + return exactMatches; } -export const initialFilters: Filters = { - textSearch: '', +export interface Filters { + appSearch: string; + ncsSearch: string; }; +export const initialFilters: Filters = { appSearch: "", ncsSearch: "" }; + interface TextSearchAction { - type: 'textSearch'; + type: 'ncsSearch' | 'appSearch'; payload: string; } export type FilterAction = TextSearchAction; -export function filterReducer(state: Filters, action: FilterAction): Filters { +export function filterReducer(filters: Filters, action: FilterAction): Filters { switch (action.type) { - case 'textSearch': - return { ...state, textSearch: action.payload }; + case 'appSearch': + return { ...filters, appSearch: action.payload}; + case 'ncsSearch': + return { ...filters, ncsSearch: action.payload}; } } export function filterApps(apps: NormalisedApp[], filters: Filters): NormalisedApp[] { - if (!filters.textSearch) { - return apps; + + if (filters.appSearch !== '') { + apps = filterAppName(apps, filters.appSearch); } - filters = normaliseFilters(filters); - const fuse = new Fuse(apps, { keys: ['owner.name', 'title', 'description', 'name', 'tags'] }); - const results = fuse.search(filters.textSearch); - return results.map((result) => result.item); -} + if (filters.ncsSearch !== '') { + apps = filterNcsVersion(apps, filters.ncsSearch); + } -function normaliseFilters(filters: Filters): Filters { - return { ...filters, textSearch: filters.textSearch.toLowerCase() }; + return apps; } diff --git a/site/src/sampleData.ts b/site/src/sampleData.ts index 764ce9e..a543a17 100644 --- a/site/src/sampleData.ts +++ b/site/src/sampleData.ts @@ -51,6 +51,7 @@ function createFakeApp(): AppIndex['apps'][number] { name: faker.system.semver(), date: faker.date.recent().toString(), tag: faker.git.branch(), + ncs: faker.git.branch(), }), { count: { min: 1, max: 5 } }, ), diff --git a/site/src/schema.ts b/site/src/schema.ts index 1ce3a28..79e3aa6 100644 --- a/site/src/schema.ts +++ b/site/src/schema.ts @@ -170,8 +170,9 @@ export const appSchema = { tag: { type: 'string' }, name: { type: 'string' }, date: { type: 'string', format: 'date' }, + ncs: { type: 'string'}, }, - required: ['tag', 'name', 'date'], + required: ['tag', 'name', 'date', 'ncs'], additionalProperties: false, }, minItems: 1, @@ -180,6 +181,7 @@ export const appSchema = { stars: { type: 'integer' }, forks: { type: 'integer' }, defaultBranch: { type: 'string' }, + defaultNcs: { type: 'string' }, lastUpdate: { type: 'string', format: 'date-time' }, apps: { type: 'string' }, },