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
2 changes: 2 additions & 0 deletions i18n/english.js
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,8 @@ const ui = {
preset_empty_no_license: "All packages have a license",
preset_empty_deprecated: "No deprecated packages",
preset_empty_large: "No package exceeds 100kb",
section_ignore_flags: "Ignore flags",
section_ignore_warnings: "Ignore warnings",
nav_navigate: "navigate",
nav_select: "select",
nav_remove: "remove filter",
Expand Down
2 changes: 2 additions & 0 deletions i18n/french.js
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,8 @@ const ui = {
preset_empty_no_license: "Tous les packages ont une licence",
preset_empty_deprecated: "Aucun package déprécié",
preset_empty_large: "Aucun package ne dépasse 100ko",
section_ignore_flags: "Ignorer les flags",
section_ignore_warnings: "Ignorer les avertissements",
nav_navigate: "naviguer",
nav_select: "sélectionner",
nav_remove: "supprimer le filtre",
Expand Down
17 changes: 17 additions & 0 deletions public/common/flags.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Import Third-party Dependencies
import { getManifest } from "@nodesecure/flags/web";

// CONSTANTS
export const IGNORABLE_FLAGS = new Set([
"hasManyPublishers",
"hasIndirectDependencies",
"hasMissingOrUnusedDependency",
"isDead",
"isOutdated",
"hasDuplicate"
]);
export const FLAG_IGNORE_ITEMS = Object.values(getManifest())
.filter(({ title }) => IGNORABLE_FLAGS.has(title))
.map(({ title, emoji }) => {
return { value: title, label: title, emoji };
});
27 changes: 27 additions & 0 deletions public/components/command-palette/command-palette-panels.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,33 @@ export function renderActions({ actions, onExecute }) {
`;
}

/**
* @param {{ title: string, items: Array<{value: string, label: string, emoji?: string}>, ignored: Set<string>, onToggle: Function }} props
*/
export function renderIgnorePanel({ title, items, ignored, onToggle }) {
return html`
<div class="section">
<div class="section-title">${title}</div>
<div class="flag-grid">
${repeat(items, (item) => item.value, (item) => {
const isIgnored = ignored.has(item.value);

return html`
<div
class=${classMap({ "flag-chip": true, "flag-active": isIgnored })}
title=${item.value}
@click=${() => onToggle(item.value)}
>
${item.emoji ? html`<span class="flag-emoji">${item.emoji}</span>` : nothing}
<span class="flag-name">${item.label}</span>
</div>
`;
})}
</div>
</div>
`;
}

/**
* @param {{ results: Array, selectedIndex: number, helperCount: number, onFocus: Function }} props
*/
Expand Down
62 changes: 62 additions & 0 deletions public/components/command-palette/command-palette.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Import Third-party Dependencies
import { LitElement, html, nothing } from "lit";
import { repeat } from "lit/directives/repeat.js";
import { warnings } from "@nodesecure/js-x-ray/warnings";

// Import Internal Dependencies
import { currentLang, vec2Distance } from "../../common/utils.js";
Expand All @@ -13,6 +14,7 @@ import {
computeMatches,
getHelperValues
} from "./filters.js";
import { FLAG_IGNORE_ITEMS } from "../../common/flags.js";
import { commandPaletteStyles } from "./command-palette-styles.js";
import {
renderFlagPanel,
Expand All @@ -21,6 +23,7 @@ import {
renderFilterList,
renderPresets,
renderActions,
renderIgnorePanel,
renderResults
} from "./command-palette-panels.js";
import "./search-chip.js";
Expand All @@ -29,6 +32,10 @@ import "./search-chip.js";
const kActions = [
{ id: "toggle_theme", shortcut: "t" }
];
const kWarningItems = Object.keys(warnings)
.map((id) => {
return { value: id, label: id.replaceAll("-", " ") };
});

function resolveKbd(shortcut) {
if (!shortcut) {
Expand Down Expand Up @@ -107,15 +114,23 @@ class CommandPalette extends LitElement {
this.results = [];
}

#onSettingsSaved = () => {
if (this.open) {
this.requestUpdate();
}
};

connectedCallback() {
super.connectedCallback();
document.addEventListener("keydown", this.#handleKeydown);
window.addEventListener(EVENTS.COMMAND_PALETTE_INIT, this.#init);
window.addEventListener(EVENTS.SETTINGS_SAVED, this.#onSettingsSaved);
}

disconnectedCallback() {
document.removeEventListener("keydown", this.#handleKeydown);
window.removeEventListener(EVENTS.COMMAND_PALETTE_INIT, this.#init);
window.removeEventListener(EVENTS.SETTINGS_SAVED, this.#onSettingsSaved);
super.disconnectedCallback();
}

Expand Down Expand Up @@ -360,6 +375,41 @@ class CommandPalette extends LitElement {
this.#close();
}

#toggleIgnore(type, value) {
const ignoreSet = window.settings.config.ignore[type];
if (ignoreSet.has(value)) {
ignoreSet.delete(value);
}
else {
ignoreSet.add(value);
}

const config = window.settings.config;
fetch("/config", {
method: "put",
body: JSON.stringify({
...config,
ignore: {
warnings: [...config.ignore.warnings],
flags: [...config.ignore.flags]
}
}),
headers: { "content-type": "application/json" }
}).catch(console.error);

window.dispatchEvent(new CustomEvent(EVENTS.SETTINGS_SAVED, {
detail: {
...config,
ignore: {
warnings: config.ignore.warnings,
flags: config.ignore.flags
}
}
}));

this.requestUpdate();
}

#getEmptyQueryMessage() {
const i18n = window.i18n[currentLang()].search_command;
if (this.queries.length === 1) {
Expand Down Expand Up @@ -538,6 +588,18 @@ class CommandPalette extends LitElement {
actions: this.#resolveActions(),
onExecute: (action) => this.#executeAction(action)
}) : nothing}
${showRichPlaceholder ? renderIgnorePanel({
title: i18n.section_ignore_flags,
items: FLAG_IGNORE_ITEMS,
ignored: window.settings?.config?.ignore?.flags ?? new Set(),
onToggle: (value) => this.#toggleIgnore("flags", value)
}) : nothing}
${showRichPlaceholder ? renderIgnorePanel({
title: i18n.section_ignore_warnings,
items: kWarningItems,
ignored: window.settings?.config?.ignore?.warnings ?? new Set(),
onToggle: (value) => this.#toggleIgnore("warnings", value)
}) : nothing}
${renderResults({
results: this.results,
selectedIndex: this.selectedIndex,
Expand Down
32 changes: 17 additions & 15 deletions public/components/views/settings/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
import { LitElement, html, css, nothing } from "lit";
import { getJSON } from "@nodesecure/vis-network";
import { warnings } from "@nodesecure/js-x-ray/warnings";
import { getManifest } from "@nodesecure/flags/web";

// Import Internal Dependencies
import { EVENTS } from "../../../core/events.js";
import { currentLang } from "../../../common/utils.js";
import { FLAG_IGNORE_ITEMS } from "../../../common/flags.js";

// CONSTANTS
const kAllowedHotKeys = new Set([
Expand All @@ -25,19 +25,6 @@ const kDefaultHotKeys = {
warnings: "A"
};
const kShortcutInputTargetIds = new Set(Object.keys(kDefaultHotKeys));
const kIgnorableFlags = new Set([
"hasManyPublishers",
"hasIndirectDependencies",
"hasMissingOrUnusedDependency",
"isDead",
"isOutdated",
"hasDuplicate"
]);
const kFlags = Object.values(getManifest())
.filter(({ title }) => kIgnorableFlags.has(title))
.map(({ title, emoji }) => {
return { value: title, emoji };
});
const kShortcuts = [
{ id: "home", labelKey: "goto", viewKey: "home" },
{ id: "network", labelKey: "goto", viewKey: "network" },
Expand Down Expand Up @@ -304,6 +291,21 @@ export class SettingsView extends LitElement {
}
}

#onSettingsSaved = (event) => {
this.setNewConfig(event.detail);
this._saveEnabled = false;
};

connectedCallback() {
super.connectedCallback();
window.addEventListener(EVENTS.SETTINGS_SAVED, this.#onSettingsSaved);
}

disconnectedCallback() {
window.removeEventListener(EVENTS.SETTINGS_SAVED, this.#onSettingsSaved);
super.disconnectedCallback();
}

firstUpdated() {
this.updateNavigationHotKey(this._hotkeys);
}
Expand Down Expand Up @@ -464,7 +466,7 @@ export class SettingsView extends LitElement {
}

#renderFlagCheckboxes() {
return kFlags.map(({ value, emoji }) => html`
return FLAG_IGNORE_ITEMS.map(({ value, emoji }) => html`
<div>
<input
type="checkbox"
Expand Down
113 changes: 110 additions & 3 deletions test/e2e/command-palette.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,20 +45,20 @@ test.describe("[command-palette] presets and actions", () => {
test("shows contextual empty message for has-vulnerabilities preset", async({ page }) => {
await page.locator(".range-preset").filter({ hasText: i18n.preset_has_vulnerabilities }).click();

await expect(page.locator(".empty-state")).toHaveText(i18n.preset_empty_has_vulnerabilities);
await expect(page.locator(".dialog .empty-state")).toHaveText(i18n.preset_empty_has_vulnerabilities);
});

test("shows contextual empty message for deprecated preset", async({ page }) => {
await page.locator(".range-preset").filter({ hasText: i18n.preset_deprecated }).click();

await expect(page.locator(".empty-state")).toHaveText(i18n.preset_empty_deprecated);
await expect(page.locator(".dialog .empty-state")).toHaveText(i18n.preset_empty_deprecated);
});

test("shows generic empty message when a manual filter yields no results", async({ page }) => {
await page.locator("#cmd-input").fill("flag:hasBannedFile");
await page.keyboard.press("Enter");

await expect(page.locator(".empty-state")).toHaveText(i18n.empty_after_filter);
await expect(page.locator(".dialog .empty-state")).toHaveText(i18n.empty_after_filter);
});

test("clicking the theme action closes the palette and toggles the theme", async({ page }) => {
Expand Down Expand Up @@ -90,3 +90,110 @@ test.describe("[command-palette] presets and actions", () => {
await expect(page.locator(".backdrop")).not.toBeVisible();
});
});

test.describe("[command-palette] ignore flags and warnings", () => {
let i18n;

test.beforeEach(async({ page }) => {
await page.goto("/");
await page.waitForSelector(`[data-menu="network--view"].active`);

i18n = await page.evaluate(() => {
const lang = document.getElementById("lang").dataset.lang;
const activeLang = lang in window.i18n ? lang : "english";

return window.i18n[activeLang].search_command;
});

await page.locator("body").click();
await page.keyboard.press("Control+k");

await expect(page.locator(".backdrop")).toBeVisible();
});

test("renders the ignore flags section", async({ page }) => {
await expect(page.locator(".section").filter({ hasText: i18n.section_ignore_flags })).toBeVisible();
});

test("renders the ignore warnings section", async({ page }) => {
await expect(page.locator(".section").filter({ hasText: i18n.section_ignore_warnings })).toBeVisible();
});

test("renders all six ignorable flag chips", async({ page }) => {
const ignoreFlagsSection = page.locator(".section").filter({ hasText: i18n.section_ignore_flags });

await expect(ignoreFlagsSection.locator(".flag-chip")).toHaveCount(6);
});

test("renders at least one warning chip", async({ page }) => {
const ignoreWarningsSection = page.locator(".section").filter({ hasText: i18n.section_ignore_warnings });

expect(await ignoreWarningsSection.locator(".flag-chip").count()).toBeGreaterThan(0);
});

test("clicking a flag chip marks it as ignored", async({ page }) => {
const ignoreFlagsSection = page.locator(".section").filter({ hasText: i18n.section_ignore_flags });
const chip = ignoreFlagsSection.locator(".flag-chip[title='isOutdated']");

const isInitiallyIgnored = await page.evaluate(
() => window.settings.config.ignore.flags.has("isOutdated")
);
if (isInitiallyIgnored) {
await chip.click();
await expect(chip).not.toContainClass("flag-active");
}

await chip.click();

await expect(chip).toContainClass("flag-active");
const ignoredFlags = await page.evaluate(() => [...window.settings.config.ignore.flags]);
expect(ignoredFlags).toContain("isOutdated");
});

test("clicking an active flag chip removes it from ignored", async({ page }) => {
const ignoreFlagsSection = page.locator(".section").filter({ hasText: i18n.section_ignore_flags });
const chip = ignoreFlagsSection.locator(".flag-chip[title='isOutdated']");

const isInitiallyIgnored = await page.evaluate(
() => window.settings.config.ignore.flags.has("isOutdated")
);
if (!isInitiallyIgnored) {
await chip.click();
await expect(chip).toContainClass("flag-active");
}

await chip.click();

await expect(chip).not.toContainClass("flag-active");
const ignoredFlags = await page.evaluate(() => [...window.settings.config.ignore.flags]);
expect(ignoredFlags).not.toContain("isOutdated");
});

test("clicking a warning chip marks it as ignored", async({ page }) => {
const ignoreWarningsSection = page.locator(".section").filter({ hasText: i18n.section_ignore_warnings });
const chip = ignoreWarningsSection.locator(".flag-chip").first();
const warningValue = await chip.getAttribute("title");

const isInitiallyIgnored = await page.evaluate(
(id) => window.settings.config.ignore.warnings.has(id),
warningValue
);
if (isInitiallyIgnored) {
await chip.click();
await expect(chip).not.toContainClass("flag-active");
}

await chip.click();

await expect(chip).toContainClass("flag-active");
const ignoredWarnings = await page.evaluate(() => [...window.settings.config.ignore.warnings]);
expect(ignoredWarnings).toContain(warningValue);
});

test("ignore sections are hidden when a filter is active", async({ page }) => {
await page.locator("#cmd-input").fill("flag:");

await expect(page.locator(".section").filter({ hasText: i18n.section_ignore_flags })).not.toBeVisible();
await expect(page.locator(".section").filter({ hasText: i18n.section_ignore_warnings })).not.toBeVisible();
});
});
Loading