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
11 changes: 8 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- 🗂️ **Configurable table columns** (Call Tree, Analysis, Database). ([#298])
- 🗂️ **Column views**: switch preset column sets, show/hide columns from the **Columns** button or the header right-click menu, inline **reset** to restore defaults; choices persist per view.
- 🏷️ **New columns**: **Object** (queried/target SObject, with group-by) on SOQL/DML; **SOSL Count/Rows**, **Avg Self Time** and optional **Self** variants for every governor metric; and a SOQL **Query Plan** view (Relative Cost, Leading Operation, SObject Type, Cardinality).
- 🧰 **Filter bar** (Call Tree, Database): filters now live in one toolbar above each table.
- Filter by **Namespace**, **Object** or **Caller Namespace**, or by a **Row Count** / **Time Taken** min–max range; active filters are highlighted.
- Collapse behind a **Filter** button on narrow window. ([#873])
- 🔴 **Timeline exception markers**: exceptions show as red lines, with a **Throws** count in method tooltips. ([#828])
- 🧰 **Filter bar** (Call Tree, Database): filters now live in one toolbar above each table instead of in the column headers. Keep only the rows you care about — by **Namespace**, **Object** or **Caller Namespace** (multi-select, showing how many are picked), or by a **Row Count** / **Time Taken** min–max range; active filters are highlighted. On a narrow window the filters collapse behind a **Filter** button that opens them in a panel. ([#873])

### Changed

Expand All @@ -41,9 +43,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- 🏷️ **Call Tree names**: rows no longer carry a raw `EVENT_TYPE:` prefix in front of text that already identifies them, so `WF_CRITERIA_BEGIN: WF_CRITERIA : ON_ALL_CHANGES` reads as `WF_CRITERIA : ON_ALL_CHANGES`. Frames whose text can't stand alone keep the type, and the ones that needed naming now say what they are — `(code unit)`, `(constructor)`, `(managed package)`, `(flow)`. A **Type** column is available in every view from the **Columns** menu if you want the raw types back.
- 🗂️ **Call Tree + Database styling**: VS Code style tree icons, and rows indent under their group headings. ([#832]).
- 🎛️ **Modernised dropdowns**: searchable, compact controls that carry the field and value in one place (e.g. `Group: Namespace`, `Type: All`) ([#848]).
- ♻️ Replace `webview-ui-toolkit` with [vscode-elements](https://github.com/vscode-elements/elements) for all UI controls. ([#576]).
- 🗄️ **Database table columns** (DML, SOQL, SOSL): consolidated onto the shared Call Tree column/sort styling for a consistent look across all tables.
- 🧱 **Data grids**: a crisper header/content separator and tidied grid styling across all tables.
- 🎨 **Header bar**
- **Log problems** icon now shows the most severe problem found, with a count.
- **Log problems** and **Notifications** redesigned cards, show two lines of summary and message (click the message for the rest), and go to the Call Tree when clicked. An **Unsupported log event** card opens a prefilled bug report.
- **Help & documentation** and **Report an issue** move into a `•••` menu, which also holds the controls the header drops as the window narrows.
- ♻️ Replace `webview-ui-toolkit` with [vscode-elements](https://github.com/vscode-elements/elements) for all UI controls. ([#576]).

### Fixed

Expand Down
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export default {
...defaultConfig,
displayName: 'log-viewer',
rootDir: '<rootDir>/log-viewer',
setupFilesAfterEnv: ['<rootDir>/src/__tests__/setup.ts'],
moduleNameMapper: {
...defaultConfig.moduleNameMapper,
'^apex-log-parser$': '<rootDir>/../apex-log-parser/src/index.ts',
Expand Down
10 changes: 10 additions & 0 deletions lana/src/commands/LogView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,16 @@ export class LogView {
break;
}

case 'openUrl': {
// https only: a webview message must not be able to hand VS Code a
// `command:` or `file:` URI to execute.
const url = typeof payload === 'string' ? payload : '';
if (url && Uri.parse(url).scheme === 'https') {
commands.executeCommand('vscode.open', Uri.parse(url));
}
break;
}

case 'getConfig': {
const config = getConfig();
const overrides = getColumnOverrides(context.context.globalState);
Expand Down
18 changes: 18 additions & 0 deletions log-viewer/src/__tests__/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright (c) 2026 Certinia Inc. All rights reserved.
*/

/**
* jsdom implements no layout, so it ships no `ResizeObserver` either. Components that observe
* their own size construct one on connect, so without this every such suite throws before it can
* assert anything. A suite that needs to *drive* resizes replaces this with its own stub.
*/
class NoopResizeObserver implements ResizeObserver {
observe(): void {}
unobserve(): void {}
disconnect(): void {}
}

if (!('ResizeObserver' in globalThis)) {
(globalThis as unknown as Record<string, unknown>).ResizeObserver = NoopResizeObserver;
}
185 changes: 185 additions & 0 deletions log-viewer/src/components/AnchoredPopover.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
/*
* Copyright (c) 2026 Certinia Inc. All rights reserved.
*/
import { LitElement, css, html } from 'lit';
import { customElement, property } from 'lit/decorators.js';

// web components
import '#vscode-elements/vscode-icon.js';

// styles
import { globalStyles } from '../styles/global.styles.js';

/**
* Trigger + panel pair for the header's drop-downs: renders the slotted trigger and
* shows the `panel` slot in a native popover anchored to it.
*
* Native `popover` (rather than a `position: absolute` div) buys two things the
* hand-rolled panels didn't have: the panel lives in the top layer so no ancestor
* `overflow` can clip it, and light-dismiss (click-outside + `Escape`) comes for
* free — no `document` click listener to add, leak or get wrong.
*
* Opens on click only. These panels contain links and navigation buttons, so a
* hover-opened panel would be a trap.
*/
@customElement('anchored-popover')
export class AnchoredPopover extends LitElement {
/** Panel `aria-label`; rendered as a visible heading only with `show-heading`. */
@property()
heading = '';

/** Show `heading` at the top of the panel. Off for menus — VS Code's are untitled. */
@property({ attribute: 'show-heading', type: Boolean })
showHeading = false;

/** Which side the panel aligns to. */
@property()
align: 'start' | 'end' = 'end';

/** Shown in place of the `panel` slot when it has no content. */
@property({ attribute: 'empty-message' })
emptyMessage = '';

/** Queried live rather than cached: the slot's content changes without a re-render. */
private get _panelContent(): readonly Element[] {
const slot = this.shadowRoot?.querySelector<HTMLSlotElement>('slot[name="panel"]');
return slot?.assignedElements({ flatten: true }) ?? [];
}

static styles = [
globalStyles,
css`
:host {
display: inline-flex;
flex: 0 0 auto;
}

.trigger {
display: inline-flex;
/* The popover positions against this, so it must be the anchor rather than
:host — a display:contents/inline-flex host has no usable anchor box. */
anchor-name: --anchored-popover-trigger;
border: 0;
padding: 0;
margin: 0;
background: none;
color: inherit;
font: inherit;
cursor: pointer;
}

.panel {
position: fixed;
position-anchor: --anchored-popover-trigger;
/* Flip above / to the other side rather than running off-screen: the header
sits at the top of a panel that can be docked at either edge. */
position-try-fallbacks:
flip-block,
flip-inline,
flip-block flip-inline;
inset: auto;
margin: 6px 0 0 0;
box-sizing: border-box;
width: 320px;
max-width: min(92vw, 320px);
max-height: 540px;
overflow-y: auto;
padding: 6px;
/* Panel content sets its own alignment — never inherit one from the header row. */
text-align: start;
}

:host([align='end']) .panel {
position-area: bottom span-left;
}

:host([align='start']) .panel {
position-area: bottom span-right;
}

.panel__head {
padding: 2px 8px 6px;
font-weight: 600;
font-size: 12px;
color: var(--vscode-foreground);
}

.panel__empty {
display: flex;
align-items: center;
gap: 6px;
padding: 8px;
font-size: 12px;
color: var(--vscode-descriptionForeground);
}

/* Hidden until the slot reports content, so the empty message shows instead. */
.panel__items--empty {
display: none;
}
`,
];

render() {
// Emptiness is read from the slot, which doesn't exist on the first render — the
// firstUpdated/slotchange re-render settles it. No flash: the panel stays closed
// until the trigger is clicked.
const isEmpty = this._panelContent.length === 0;

return html`<button
part="trigger"
class="trigger"
popovertarget="anchored-popover-panel"
aria-haspopup="true"
aria-controls="anchored-popover-panel"
>
<slot name="trigger"></slot>
</button>
<div
part="panel"
class="panel filter-popover"
id="anchored-popover-panel"
popover
role="group"
aria-label=${this.heading}
>
${
this.showHeading && this.heading
? html`<div class="panel__head">${this.heading}</div>`
: ''
}
<div class=${isEmpty ? 'panel__items--empty' : ''}>
<slot name="panel" @slotchange=${this._onSlotChange}></slot>
</div>
${
isEmpty && this.emptyMessage
? html`<div class="panel__empty">
<vscode-icon name="pass" size="16"></vscode-icon>
<span>${this.emptyMessage}</span>
</div>`
: ''
}
</div>`;
}

/**
* Dismiss the panel. Light-dismiss covers clicks *outside*, so a command row inside
* the panel has to close it explicitly or it stays open over whatever it just did.
*/
close(): void {
const panel = this.shadowRoot?.querySelector<HTMLElement>('.panel');
// No-op under jsdom, which implements neither `hidePopover` nor `:popover-open`.
if (typeof panel?.hidePopover === 'function' && panel.matches(':popover-open')) {
panel.hidePopover();
}
}

/** The slot only reports its content once it exists, so settle the empty state here. */
override firstUpdated(): void {
this.requestUpdate();
}

private _onSlotChange(): void {
this.requestUpdate();
}
}
55 changes: 0 additions & 55 deletions log-viewer/src/components/BadgeBase.ts

This file was deleted.

Loading
Loading