diff --git a/os-checks/components/DropDownWithCount.vue b/os-checks/components/DropDownWithCount.vue new file mode 100644 index 0000000..642b29a --- /dev/null +++ b/os-checks/components/DropDownWithCount.vue @@ -0,0 +1,57 @@ + + + + + {{ tag }}: + + + + {{ counts.counts[option] }} + {{ option }} + + + + + + + + + + + + diff --git a/os-checks/components/FileTree.vue b/os-checks/components/FileTree.vue index af84638..24a4340 100644 --- a/os-checks/components/FileTree.vue +++ b/os-checks/components/FileTree.vue @@ -2,6 +2,7 @@ import type { FetchError } from 'ofetch'; import type { TreeNode } from 'primevue/treenode'; import type { FileTree, Kinds } from '~/shared/file-tree'; +import { getEmpty } from '../shared/file-tree/utils'; type Props = { fetch_path: (target: string) => string }; const props = defineProps(); @@ -10,7 +11,7 @@ highlightRust(); const tabs = ref([]); const selectedTab = ref(""); -const fileTree = ref({ kinds_order: [], data: [] }); +const fileTree = ref(getEmpty().fileTree); const basic = useBasicStore(); @@ -46,7 +47,7 @@ basic.init_with_and_subscribe_to_current((target: string) => { lang: "rust", severity: Severity.Info, disabled: false }]; selectedTab.value = "All good! 🥳"; - fileTree.value = { kinds_order: [], data: [] }; + fileTree.value = getEmpty().fileTree; // tabs.value = [{ // kind: "Not Exists!", raw: ["该目标架构下,无原始报告数据。"], diff --git a/os-checks/components/FileTree2.vue b/os-checks/components/FileTree2.vue new file mode 100644 index 0000000..0c7dfb0 --- /dev/null +++ b/os-checks/components/FileTree2.vue @@ -0,0 +1,185 @@ + + + + + + + + + + displayFileTree = !displayFileTree" /> + + + displayFilters = !displayFilters" /> + + + + Total Count: + {{ count }} + + + + + + + + + + + + + {{ tab.kind }} + + + + + + + + + + + + + + + + + + + diff --git a/os-checks/components/Print.vue b/os-checks/components/Print.vue new file mode 100644 index 0000000..c7cbb14 --- /dev/null +++ b/os-checks/components/Print.vue @@ -0,0 +1,31 @@ + + + + {{ props.tmp }} + + fileTree.data.length: {{ props.fileTree.data.length }} + + + tabs: {{ props.tabs }} + + + selectedTab: {{ props.selectedTab }} + + + {{ props.get }} + diff --git a/os-checks/components/TargetDropDown.vue b/os-checks/components/TargetDropDown.vue index 9d75db2..954ab8a 100644 --- a/os-checks/components/TargetDropDown.vue +++ b/os-checks/components/TargetDropDown.vue @@ -43,17 +43,14 @@ function fetch() { }); } -function change(path: string, params: any) { - // console.log("path =", path); - const excludes = ["/", "/repos", "/charts", "/target", "/workflows", "/testcases"]; - if (excludes.findIndex(p => p === path) !== -1) { - visible.value = false; - return; - } else if (params) { - // console.log("path =", path); +function change(path: string, _params: any) { + const includes = ["/diagnostics"]; + if (includes.findIndex(p => p === path) !== -1) { + visible.value = true; fetch(); + } else { + visible.value = false; } - visible.value = true; } diff --git a/os-checks/package-lock.json b/os-checks/package-lock.json index 76713c3..33555a4 100644 --- a/os-checks/package-lock.json +++ b/os-checks/package-lock.json @@ -12,6 +12,7 @@ "chart.js": "^4.4.5", "chartjs-plugin-datalabels": "^2.2.0", "dompurify": "^3.1.7", + "es-toolkit": "^1.32.0", "highlight.js": "^11.10.0", "nuxt": "^3.13.2", "ofetch": "^1.4.1", @@ -4336,6 +4337,16 @@ "integrity": "sha512-fZmsRiDNv07K6s2KkKFTiD2aIvECa7++PKyD5NC32tpRw46qZA3sOz+aM+/V9V0GDHxVTKLziveV4JhzBHDp9Q==", "license": "MIT" }, + "node_modules/es-toolkit": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.32.0.tgz", + "integrity": "sha512-ZfSfHP1l6ubgW/B/FRtqb9bYdMvI6jizbOSfbwwJNcOQ1QE6TFsC3jpQkZ900uUPSR3t3SU5Ds7UWKnYz+uP8Q==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/esbuild": { "version": "0.23.1", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", diff --git a/os-checks/package.json b/os-checks/package.json index 1a58283..44210f7 100644 --- a/os-checks/package.json +++ b/os-checks/package.json @@ -15,6 +15,7 @@ "chart.js": "^4.4.5", "chartjs-plugin-datalabels": "^2.2.0", "dompurify": "^3.1.7", + "es-toolkit": "^1.32.0", "highlight.js": "^11.10.0", "nuxt": "^3.13.2", "ofetch": "^1.4.1", diff --git a/os-checks/pages/file-tree.vue b/os-checks/pages/file-tree.vue index 82cceb1..f008f15 100644 --- a/os-checks/pages/file-tree.vue +++ b/os-checks/pages/file-tree.vue @@ -1,9 +1,265 @@ - + + + + + + + + + + + User: + + + + + Repo: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/os-checks/shared/file-tree.ts b/os-checks/shared/file-tree.ts index f65d3ed..55d3199 100644 --- a/os-checks/shared/file-tree.ts +++ b/os-checks/shared/file-tree.ts @@ -1,5 +1,5 @@ export type Kinds = { [key: string]: string[] }; -export type RawReport = { file: string, count: number, kinds: Kinds }; +export type RawReport = { file: string, features: string, count: number, kinds: Kinds }; export type Datum = { user: string, repo: string, @@ -11,6 +11,24 @@ export type Datum = { export type FileTree = { // 诊断类别数组,越往前的越优先展示 kinds_order: string[], - data: Datum[] + data: Datum[], + repo: { + user: string, + repo: string, + } } +export type CheckerResult = { + kind: string, + raw: string[], + lang: string, + severity: Severity, + disabled: boolean, // 对于空数组,禁用选项卡 +}; + +export enum Severity { + Danger = "danger", + Warn = "warn", + Info = "info", + Disabled = "secondary", +} diff --git a/os-checks/shared/file-tree/dropdown.ts b/os-checks/shared/file-tree/dropdown.ts new file mode 100644 index 0000000..9195296 --- /dev/null +++ b/os-checks/shared/file-tree/dropdown.ts @@ -0,0 +1,186 @@ +import { ALL_CHECKERS, ALL_FEATURES_SETS, ALL_KINDS, ALL_PKGS, counts_to_options, emptyOptions, type Counts, type DropDownOptions } from "./types"; +import type { Get } from "./utils"; +import type { Kinds } from "../file-tree"; +import type { Basic, Targets } from "../types"; + +export class Dropdown { + pkgs: DropDownOptions; + kinds: DropDownOptions; + map: KindCheckerMap; + checkers: DropDownOptions; + features: DropDownOptions; + + /** called when a new fileTree is created */ + constructor(g: Get, map: KindCheckerMap) { + this.pkgs = gen_pkgs(g); + this.kinds = gen_kinds(g); + this.map = map; // map is supposed to never change + this.checkers = gen_checkers(this.kinds, map); + this.features = gen_features(g); + } + + /** called when a fitler is changed */ + filter(g: Get): Dropdown { + let options = new Dropdown(g, this.map); + // replace All* with full counts + options.pkgs.counts[ALL_PKGS] = this.pkgs.counts[ALL_PKGS]; + options.kinds.counts[ALL_KINDS] = this.kinds.counts[ALL_KINDS]; + options.checkers.counts[ALL_CHECKERS] = this.checkers.counts[ALL_CHECKERS]; + options.features.counts[ALL_FEATURES_SETS] = this.features.counts[ALL_FEATURES_SETS]; + return options; + } + + static empty(): Dropdown { + const options = emptyOptions(); + const obj = Object.create(Dropdown.prototype); + obj.pkgs = options; obj.kinds = options; obj.checkers = options; obj.map = {}; + return obj; + } + + static update_by_pkg(pkg: string | null, g: Get) { + if (pkg && pkg !== ALL_PKGS) update_by_pkg(pkg, g); + } + + static update_by_features(feat: string | null, g: Get) { + if (feat !== null && feat !== ALL_FEATURES_SETS) update_by_features(feat, g); + } + + static update_by_kind(kind: string | null, g: Get) { + if (kind && kind !== ALL_KINDS) update_by_kind(kind, g); + } + + static update_by_checker(kinds: string[], g: Get) { + update_by_checker(kinds, g); + } + + static find_kind(kind: string | null, g: Get): string | null { + if (!kind) return null; + for (const data of g.fileTree.data) { + for (const reports of data.raw_reports) { + const ele = reports.kinds[kind]; + if (ele && ele.length) return kind; + } + } + return null; + } +} + +// generate filter options + +function gen_pkgs(g: Get): DropDownOptions { + let counts: Counts = {}; + for (const data of g.fileTree.data) { + const pkg = data.pkg; + const len = data.raw_reports.reduce((acc, reports) => acc + reports.count, 0); + // usually if can't be true due to impossible duplicated pkg name + if (counts[pkg]) counts[pkg] += len; + else counts[pkg] = len; + } + return counts_to_options(counts, ALL_PKGS); +} + +function gen_kinds(g: Get): DropDownOptions { + let counts: Counts = {}; + for (const ft of g.fileTree.data) { + for (const report of ft.raw_reports) { + for (const [kind, arr] of Object.entries(report.kinds)) { + let len = arr.length; + if (counts[kind]) counts[kind] += len; + else counts[kind] = len; + } + } + } + return counts_to_options(counts, ALL_KINDS); +} + +type KindCheckerMap = { [key: string]: string }; +export function gen_map(data: Basic) { + // {"checker": ["kind1", "kind2"]} => {"kind1": "checker", "kind2": "checker"} + let kind_checker_map: KindCheckerMap = {}; + for (const [ck, kinds] of Object.entries(data.kinds.mapping)) { + for (const kind of kinds) { + kind_checker_map[kind] = ck; + } + } + return kind_checker_map; +} + +function gen_checkers(kinds: DropDownOptions, map: KindCheckerMap): DropDownOptions { + let counts: Counts = {}; + for (const [kind, count] of Object.entries(kinds.counts)) { + const ck = map[kind]; + if (!ck) continue; + if (counts[ck]) counts[ck] += count; + else counts[ck] = count; + } + return counts_to_options(counts, ALL_CHECKERS); +} + +export function gen_targets(targets: Targets): DropDownOptions { + let counts: Counts = {}; + for (const { triple, count } of targets) { + counts[triple] = count; + } + return counts_to_options(counts); +} + +function gen_features(g: Get): DropDownOptions { + let counts: Counts = {}; + for (const data of g.fileTree.data) { + for (const reports of data.raw_reports) { + const feat = reports.features; + const len = reports.count; + if (counts[feat]) counts[feat] += len; + else counts[feat] = len; + } + } + return counts_to_options(counts, ALL_FEATURES_SETS); +} + +// filters update Get: got2 is updated in place; got is deep cloned + +export function update_by_pkg(pkg: string, g: Get) { + g.fileTree.data = g.fileTree.data.filter(val => val.pkg === pkg); +} + +export function update_by_features(feat: string, g: Get) { + for (const data of g.fileTree.data) { + // only keep feat + data.raw_reports = data.raw_reports.filter(r => r.features === feat); + data.count = data.raw_reports.reduce((acc, r) => acc + r.count, 0); + } + // only keep non-zero nodes + g.fileTree.data = g.fileTree.data.filter(d => d.count !== 0); +} + +export function update_by_kind(kind: string, g: Get) { + update_by_checker([kind], g); +} + +/** for update_by_kind, pass [kind]; for update_by_checker, pass kinds */ +export function update_by_checker(kinds: string[], g: Get) { + const kinds_set = new Set(kinds); + + // deep copy due to got shouldn't be mutated + for (const data of g.fileTree.data) { + const reports = data.raw_reports; + for (const r of reports) { + let kinds_new: Kinds = {}; + let len = 0; + for (const [kind, arr] of Object.entries(r.kinds)) { + if (kinds_set.has(kind)) { + kinds_new[kind] = arr; + len += arr.length; + } + } + // filter ck kinds only + r.kinds = kinds_new; + r.count = len; + } + // remove count==0 items and sort + data.raw_reports = reports.filter(r => r.count !== 0).sort((a, b) => (b.count - a.count)); + data.count = data.raw_reports.reduce((acc, r) => acc + r.count, 0); + } + g.fileTree.data = g.fileTree.data.filter(d => d.count !== 0); +} + diff --git a/os-checks/shared/file-tree/types.ts b/os-checks/shared/file-tree/types.ts new file mode 100644 index 0000000..dd16d0d --- /dev/null +++ b/os-checks/shared/file-tree/types.ts @@ -0,0 +1,27 @@ +export type DropDownOptions = { counts: Counts, names: string[] }; +export type Counts = { [key: string]: number }; + +export const ALL_TARGETS = "All-Targets"; +export const ALL_PKGS = "All-Pkgs"; +export const ALL_CHECKERS = "All-Checkers"; +export const ALL_KINDS = "All-Kinds"; +export const ALL_FEATURES_SETS = "All-Features-Sets"; + +export function emptyOptions(): DropDownOptions { + return { counts: {}, names: [] }; +} + +/// * extract keys from Counts but descending sort by the number */ +export function counts_to_options(counts: Counts, all?: string): DropDownOptions { + // insert ALL key + if (all) counts[all] = Object.values(counts).reduce((acc, c) => acc + c, 0); + // descending sort by count and then name + const names = Object.entries(counts) + .sort((a, b) => { + const cmp_count = b[1] - a[1]; + if (cmp_count === 0) return a[0].localeCompare(b[0]); + return cmp_count + }) + .map(ele => ele[0]); + return { counts, names }; +} diff --git a/os-checks/shared/file-tree/utils.ts b/os-checks/shared/file-tree/utils.ts new file mode 100644 index 0000000..07f041c --- /dev/null +++ b/os-checks/shared/file-tree/utils.ts @@ -0,0 +1,144 @@ +import type { FetchError } from 'ofetch'; +import type { TreeNode } from 'primevue/treenode'; +import type { CheckerResult, FileTree, Kinds } from '~/shared/file-tree'; +import { Severity } from '~/shared/file-tree'; + +export function mergeObjectsWithArrayConcat(result: Kinds, obj: Kinds) { + for (const [key, value] of Object.entries(obj)) { + if (result.hasOwnProperty(key)) { + // 如果键已经存在,则合并数组 + result[key] = result[key].concat(value); + } else { + // 否则,添加新的键值对 + result[key] = value; + } + } +} + +export type CheckerResult_SelectedTab = { + results: CheckerResult[], + selectedTab: string, +} +// Kinds 可能不包含全部诊断类别,因此这里填充空数组,并按照顺序排列 +export function checkerResult(kinds: Kinds, kinds_order: string[]): CheckerResult_SelectedTab { + let results = kinds_order.map(kind => { + return { kind, raw: [], lang: "rust", severity: Severity.Disabled, disabled: true }; + }); + for (const [kind, raw] of Object.entries(kinds)) { + let lang = "rust"; + let severity = Severity.Info; + switch (kind) { + case "Cargo": severity = Severity.Danger; break; + case "Clippy(Error)": severity = Severity.Danger; break; + case "Lockbud(Probably)": severity = Severity.Danger; break; + case "Clippy(Warn)": severity = Severity.Warn; break; + case "Unformatted": lang = "diff"; break; + default: ; + } + const pos = results.findIndex(r => r.kind === kind); + if (pos !== -1) { + // JSON 提供的诊断信息一定不是空数组 + results[pos] = { kind, raw, lang, severity, disabled: false }; + } + } + results = results.filter(res => res.raw.length !== 0); + // selectedTab.value = results.find(r => !r.disabled)?.kind ?? ""; + const selectedTab = results.find(r => !r.disabled)?.kind ?? ""; + return { results, selectedTab }; +} + +export type Get = { tabs: CheckerResult[], selectedTab: string, fileTree: FileTree }; +export function getEmpty(): Get { + return { tabs: [], selectedTab: "", fileTree: { kinds_order: [], data: [], repo: { user: "", repo: "" } } }; +} +export function get(path: string): Get { + let got = getEmpty(); + + // basic.init_with_and_subscribe_to_current((target: string) => { + githubFetch({ path }) + .then((file_tree) => { + // const file_tree: FileTree = JSON.parse(data as string); + + // 首次打开页面加载数据后,从所有 packags 的原始输出填充到所有选项卡 + let kinds = {}; + for (const datum of file_tree.data) { + for (const report of datum.raw_reports) { + // for (const kind of Object.keys(report.kinds)) { + // 对原始输出中的所有特殊符号转义,以后就不需要转义了 + // report.kinds[kind] = report.kinds[kind].map(domSanitize); + // } + mergeObjectsWithArrayConcat(kinds, report.kinds); + } + } + got.tabs = checkerResult(kinds, file_tree.kinds_order).results; + got.selectedTab = got.tabs[0]?.kind ?? ""; + got.fileTree = file_tree; + }).catch((_: FetchError) => { + // 不存在该文件:意味着该目标架构下的所有仓库没有检查出错误 + // 注意,由于使用 parseResponse,这个错误码并不为 404,而是 undefined, + // 且错误原因为 SyntaxError: Unexpected non-whitespace character after JSON at position 3。 + // 这里 ofetch 没有正确处理错误(貌似也没人报告?),所以暂且认为出现任何网络或解析错误都视为无错误。 + // console.log(err, err.data, err.statusCode); + + got.tabs = [{ + kind: "All good! 🥳", raw: ["该目标架构下的所有仓库没有检查出错误 🥳🥳🥳"], + lang: "rust", severity: Severity.Info, disabled: false + }]; + got.selectedTab = "All good! 🥳"; + got.fileTree = getEmpty().fileTree; + + // tabs.value = [{ + // kind: "Not Exists!", raw: ["该目标架构下,无原始报告数据。"], + // lang: "rust", severity: Severity.Danger, disabled: false + // }]; + // selectedTab.value = "Not Exists!"; + // fileTree.value = { kinds_order: [], data: [] }; + }); + + console.log("utils got", got); + return got; +} + +export function updateSelectedKey(val: {}, nodes: TreeNode[], fileTree: FileTree): undefined | CheckerResult_SelectedTab { + const key = Object.keys(val)[0]; + if (!key) { return; } + const idx = parseInt(key); + // console.log(idx, node); + for (const node of nodes.slice().reverse()) { + const nd = node.data; + if (!(nd && nd.user && nd.repo && nd.pkg)) { return; } + + // 查找是否点击了 package + if (node.key === key) { + // 更新 tabs 展示的数据 + const found_pkg = fileTree.data.find(datum => { + return datum.user === nd.user && datum.repo === nd.repo && datum.pkg === nd.pkg; + }); + let kinds = {}; + for (const report of found_pkg?.raw_reports ?? []) { + mergeObjectsWithArrayConcat(kinds, report.kinds); + } + const tabs = checkerResult(kinds, fileTree.kinds_order); + return tabs; + } else { + // 由于 key 是升序的,现在只要找第一个小于目标 key 的 package,那么这个文件就在那里 + if (idx > parseInt(node.key)) { + for (const file of node.children ?? []) { + if (file.key === key) { + const filename = file.data; + if (!filename) { return { results: [], selectedTab: "" }; } + const package_ = fileTree.data.find(datum => { + return datum.user === nd.user && datum.repo === nd.repo && datum.pkg === nd.pkg; + }); + const found_file = package_?.raw_reports.find(item => item.file === filename); + if (found_file) { + const tabs = checkerResult(found_file.kinds, fileTree.kinds_order); + return tabs; + } + } + } + } + } + } +} + diff --git a/os-checks/shared/types.ts b/os-checks/shared/types.ts index 3e88a5b..f3da362 100644 --- a/os-checks/shared/types.ts +++ b/os-checks/shared/types.ts @@ -6,11 +6,13 @@ export type Targets = Target[]; export type Column = { field: string, header: string }; export type Columns = Column[]; export type Basic = { + pkgs: { pkg: string, count: number }[], + checkers: { checker: string, count: number }[], targets: Targets, kinds: { - // order: string[], - // mapping: { [key: string]: string[] }, - columns: Columns + order: string[], + mapping: { [key: string]: string[] }, + // columns: Columns } }; export type TargetOption = { target: string }; diff --git a/os-checks/stores/basic.ts b/os-checks/stores/basic.ts index bbe23ae..e0df3dc 100644 --- a/os-checks/stores/basic.ts +++ b/os-checks/stores/basic.ts @@ -12,7 +12,21 @@ export const useBasicStore = defineStore('targets', { }, columns(): Columns { - return this.basic?.kinds.columns ?? []; + const order = this.basic?.kinds.order; + if (!order) return []; + return order.map(kind => { + let val = kind; + if (kind === "Clippy(Warn)") { + val = "Clippy (Warn)"; + } else if (kind === "Clippy(Error)") { + val = "Clippy (Error)" + } else if (kind === "Lockbud(Possibly)") { + val = "Lockbud (Possibly)" + } else if (kind === "Lockbud(Probably)") { + val = "Lockbud (Probably)" + } + return { field: kind, header: val }; + }); } },