From a248b53d231c5fe0c5a484154693708dacdea00d Mon Sep 17 00:00:00 2001 From: Dave Shanley Date: Tue, 13 Jun 2023 18:08:54 -0400 Subject: [PATCH] working through filters UI and experience. Signed-off-by: Dave Shanley --- ui/package.json | 2 +- .../controls/controls.component.css.ts | 19 -- .../components/controls/controls.component.ts | 61 ++++--- .../controls/filters.component.css.ts | 79 +++++++++ .../components/controls/filters.component.ts | 164 ++++++++++++++++++ .../controls/settings.component.css.ts | 22 +++ .../components/controls/settings.component.ts | 45 +++++ ui/src/components/shared.css.ts | 21 +++ .../transaction-container.component.ts | 56 +++++- .../transaction-item.component.css.ts | 17 +- .../transaction/transaction-item.component.ts | 29 +--- .../components/wiretap-header/header.css.ts | 2 +- ui/src/index.ts | 14 +- ui/src/model/constants.ts | 8 + ui/src/model/controls.ts | 35 ++++ ui/src/model/events.ts | 4 + ui/src/model/exchange_method.ts | 16 ++ ui/src/model/http_transaction.ts | 43 +++-- ui/src/wiretap.ts | 60 ++++--- 19 files changed, 573 insertions(+), 124 deletions(-) create mode 100644 ui/src/components/controls/filters.component.css.ts create mode 100644 ui/src/components/controls/filters.component.ts create mode 100644 ui/src/components/controls/settings.component.css.ts create mode 100644 ui/src/components/controls/settings.component.ts create mode 100644 ui/src/model/exchange_method.ts diff --git a/ui/package.json b/ui/package.json index dab9e92..ddebdd7 100644 --- a/ui/package.json +++ b/ui/package.json @@ -17,7 +17,7 @@ "vite-tsconfig-paths": "^4.2.0" }, "dependencies": { - "@pb33f/ranch": "^0.3.3", + "@pb33f/ranch": "^0.3.4", "@pb33f/saddlebag": "^0.1.0", "@shoelace-style/shoelace": "^2.4.0", "@stomp/stompjs": "^7.0.0", diff --git a/ui/src/components/controls/controls.component.css.ts b/ui/src/components/controls/controls.component.css.ts index c3e64d5..c5da7fb 100644 --- a/ui/src/components/controls/controls.component.css.ts +++ b/ui/src/components/controls/controls.component.css.ts @@ -6,25 +6,10 @@ export default css` margin:5px auto; } - sl-button.report { - - } - sl-icon.gear { font-size: 1.7rem; padding-top:6px; } - - sl-icon.report { - font-size: 1.4rem; - display: inline-block; - //padding-top:6px; - } - - label { - display: block; - padding-bottom: 10px; - } sl-drawer::part(panel) { @@ -45,9 +30,5 @@ export default css` font-family: var(--font-stack); } - hr { - margin-top: 30px; - margin-bottom: 30px; - } ` \ No newline at end of file diff --git a/ui/src/components/controls/controls.component.ts b/ui/src/components/controls/controls.component.ts index f41aea6..a044c50 100644 --- a/ui/src/components/controls/controls.component.ts +++ b/ui/src/components/controls/controls.component.ts @@ -1,7 +1,7 @@ import {customElement, query, state} from "lit/decorators.js"; import {LitElement} from "lit"; import {html} from "lit"; -import {ControlsResponse, ReportResponse, WiretapConfig, WiretapControls} from "@/model/controls"; +import {ControlsResponse, ReportResponse, WiretapConfig, WiretapControls, WiretapFilters} from "@/model/controls"; import localforage from "localforage"; import {Bus, BusCallback, Channel, CommandResponse, GetBus, Message, Subscription} from "@pb33f/ranch"; import controlsComponentCss from "./controls.component.css"; @@ -13,7 +13,7 @@ import sharedCss from "@/components/shared.css"; import { ChangeDelayCommand, RequestReportCommand, WiretapControlsChannel, WiretapControlsKey, - WiretapControlsStore, + WiretapControlsStore, WiretapFiltersStore, WiretapHttpTransactionStore, WiretapReportChannel } from "@/model/constants"; @@ -27,8 +27,12 @@ export class WiretapControlsComponent extends LitElement { private readonly _bus: Bus; - @query('sl-drawer') - drawer: SlDrawer; + @query('#controls-drawer') + controlsDrawer: SlDrawer; + + @query('#filters-drawer') + filtersDrawer: SlDrawer; + @query("#global-delay") delayInput: SlInput; @@ -45,6 +49,10 @@ export class WiretapControlsComponent extends LitElement { private readonly _wiretapReportChannel: Channel; private readonly _storeManager: BagManager; private readonly _controlsStore: Bag; + private readonly _filtersStore: Bag; + + @state() + private filters: WiretapFilters; constructor() { super(); @@ -53,12 +61,12 @@ export class WiretapControlsComponent extends LitElement { this._bus = GetBus(); this._storeManager = GetBagManager(); this._controlsStore = this._storeManager.getBag(WiretapControlsStore); + this._filtersStore = this._storeManager.getBag(WiretapFiltersStore); this._wiretapControlsChannel = this._bus.getChannel(WiretapControlsChannel); this._wiretapReportChannel = this._bus.getChannel(WiretapReportChannel); this._wiretapControlsSubscription = this._wiretapControlsChannel.subscribe(this.controlUpdateHandler()); this._wiretapReportSubscription = this._wiretapReportChannel.subscribe(this.reportHandler()); - this.loadControlStateFromStorage().then((controls: WiretapControls) => { if (!controls) { this._controls = { @@ -70,12 +78,18 @@ export class WiretapControlsComponent extends LitElement { // get the delay from the backend. this.changeGlobalDelay(-1) // -1 won't update anything, but will return the current delay }); + + } async loadControlStateFromStorage(): Promise { return localforage.getItem(WiretapControlsStore); } + + + + controlUpdateHandler(): BusCallback { return (msg: Message>) => { const delay = msg.payload.payload.config.globalAPIDelay; @@ -121,8 +135,12 @@ export class WiretapControlsComponent extends LitElement { }); } - openControls() { - this.drawer.show(); + openSettings() { + this.controlsDrawer.show(); + } + + openFilters() { + this.filtersDrawer.show(); } sendReportRequest() { @@ -139,7 +157,8 @@ export class WiretapControlsComponent extends LitElement { } closeControls() { - this.drawer.hide() + this.controlsDrawer.hide() + this.filtersDrawer.hide() } handleGlobalDelayChange(event) { @@ -158,23 +177,25 @@ export class WiretapControlsComponent extends LitElement { render() { return html` - + + + + - - - - - -
- Reset State -
- - Download Session Data + + Close + + + + Close + ` } } \ No newline at end of file diff --git a/ui/src/components/controls/filters.component.css.ts b/ui/src/components/controls/filters.component.css.ts new file mode 100644 index 0000000..253f11b --- /dev/null +++ b/ui/src/components/controls/filters.component.css.ts @@ -0,0 +1,79 @@ + +import {css} from "lit"; + +export default css` + + .label-on-left { + --label-width: 5rem; + --gap-width: 1rem; + } + + .label-on-left + .label-on-left { + margin-top: var(--sl-spacing-medium); + } + + .label-on-left::part(form-control) { + display: grid; + grid: auto / var(--label-width) 1fr; + gap: var(--sl-spacing-3x-small) var(--gap-width); + align-items: center; + font-family: var(--font-stack); + } + + .label-on-left::part(form-control-label) { + text-align: right; + } + + .label-on-left::part(form-control-help-text) { + grid-column-start: 2; + } + + hr { + margin-bottom: 20px; + margin-top: 20px; + } + + .keywords { + border: 1px dashed var(--secondary-color-dimmer); + padding: 5px; + min-height: 40px; + margin-top: 20px; + } + + .chains { + border: 1px dashed var(--primary-color-lowalpha); + padding: 5px; + min-height: 40px; + margin-top: 20px; + } + + .keyword { + transition: var(--sl-transition-fast) opacity; + } + + .keyword::part(base) { + font-family: var(--font-stack); + border: 1px dashed var(--secondary-color); + margin-bottom: 5px; + } + + .chain { + transition: var(--sl-transition-fast) opacity; + } + + .chain::part(base) { + font-family: var(--font-stack); + border: 1px dashed var(--primary-color); + margin-bottom: 5px; + } + + p { + margin-top: 20px; + margin-bottom: 20px; + } + + .chain-input::part(base) { + --sl-input-border-color: var(--primary-color); + } + +` \ No newline at end of file diff --git a/ui/src/components/controls/filters.component.ts b/ui/src/components/controls/filters.component.ts new file mode 100644 index 0000000..4802fc0 --- /dev/null +++ b/ui/src/components/controls/filters.component.ts @@ -0,0 +1,164 @@ +import {customElement, state, query, property} from "lit/decorators.js"; +import {html, LitElement} from "lit"; +import sharedCss from "@/components/shared.css"; +import filtersComponentCss from "./filters.component.css"; +import {ExchangeMethod} from "@/model/exchange_method"; +import {WiretapFilters} from "@/model/controls"; +import {GlobalDelayChangedEvent} from "@/model/events"; +import {SlInput} from "@shoelace-style/shoelace"; +import {Bag, BagManager, GetBagManager} from "@pb33f/saddlebag"; +import {WiretapFiltersKey, WiretapFiltersStore} from "@/model/constants"; +import localforage from "localforage"; +import {RanchUtils} from "@pb33f/ranch"; + +@customElement('wiretap-controls-filters') +export class WiretapControlsFiltersComponent extends LitElement { + + static styles = [sharedCss, filtersComponentCss] + + private _methods: string[] = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD', 'TRACE']; + private readonly _filtersStore: Bag; + private readonly _storeManager: BagManager; + + @query('#keyword-input') + keywordInput: SlInput; + + @query('#chain-input') + chainInput: SlInput; + + @state() + private filters: WiretapFilters; + + constructor() { + super(); + this._storeManager = GetBagManager(); + this._filtersStore = this._storeManager.getBag(WiretapFiltersStore); + + this.loadFiltersFromStorage().then((filters: WiretapFilters) => { + if (!filters) { + this.filters = new WiretapFilters(); + } else { + this.filters = filters; + } + this._filtersStore.set(WiretapFiltersKey, this.filters) + }); + + } + + async loadFiltersFromStorage(): Promise { + return localforage.getItem(WiretapFiltersStore); + } + + handleKeywordInput() { + const keyword = this.keywordInput.value; + this.filters.filterKeywords.push({keyword: keyword, id: RanchUtils.genShortId(5)}) + this.keywordInput.value = '' + this._filtersStore.set(WiretapFiltersKey, this.filters) + this.saveFiltersToStorage() + this.requestUpdate(); + } + + handleChainInput() { + const param = this.chainInput.value; + this.filters.filterChain.push({keyword: param, id: RanchUtils.genShortId(5)}) + this.chainInput.value = '' + this._filtersStore.set(WiretapFiltersKey, this.filters) + this.saveFiltersToStorage() + this.requestUpdate(); + } + + private saveFiltersToStorage() { + localforage.setItem(WiretapFiltersStore, this.filters); + } + + private removeKeyword(id: string) { + this.filters.filterKeywords = this.filters.filterKeywords.filter((filter) => { + return filter.id !== id; + }) + this._filtersStore.set(WiretapFiltersKey, this.filters) + this.saveFiltersToStorage() + this.requestUpdate(); + } + + private removeChain(id: string) { + this.filters.filterChain = this.filters.filterChain.filter((filter) => { + return filter.id !== id; + }) + this._filtersStore.set(WiretapFiltersKey, this.filters) + this.saveFiltersToStorage() + this.requestUpdate(); + } + + private methodFilterChanged(value: string) { + this.filters.filterMethod.keyword = value + this._filtersStore.set(WiretapFiltersKey, this.filters) + this.saveFiltersToStorage() + this.requestUpdate(); + } + + + render() { + + return html` + { this.methodFilterChanged(change.target.value)} } clearable> + ${this._methods.map((method) => { + return html` + + ${method} + ` + })} + + +
+

Keyword Filters

+

+ Keywords are matched against the request and response body and + query parameters. +

+ + + +
+ ${this.filters?.filterKeywords?.map((filter) => { + return html` + { + const tag = event.target; + tag.style.opacity = '0'; + setTimeout(() => { + (tag.style.opacity = '1'); + this.removeKeyword(filter.id) + }, 200); + + }} class="keyword" size="small" removable>${filter.keyword} + ` + })} +
+ +
+

Request Chains

+

+ Link related requests together using query parameter keys. +

+ + + +
+ ${this.filters?.filterChain?.map((filter) => { + return html` + { + const tag = event.target; + tag.style.opacity = '0'; + setTimeout(() => { + (tag.style.opacity = '1'); + this.removeChain(filter.id) + }, 200); + + }} class="chain" size="small" removable>${filter.keyword} + ` + })} +
+ ` + } +} \ No newline at end of file diff --git a/ui/src/components/controls/settings.component.css.ts b/ui/src/components/controls/settings.component.css.ts new file mode 100644 index 0000000..3d8b6ae --- /dev/null +++ b/ui/src/components/controls/settings.component.css.ts @@ -0,0 +1,22 @@ + + +import {css} from "lit"; + +export default css` + + sl-icon.report { + font-size: 1.4rem; + display: inline-block; + } + + label { + display: block; + padding-bottom: 10px; + } + + hr { + margin-top: 30px; + margin-bottom: 30px; + } + +` \ No newline at end of file diff --git a/ui/src/components/controls/settings.component.ts b/ui/src/components/controls/settings.component.ts new file mode 100644 index 0000000..ad68f79 --- /dev/null +++ b/ui/src/components/controls/settings.component.ts @@ -0,0 +1,45 @@ +import {customElement, state, query} from "lit/decorators.js"; +import {html, LitElement} from "lit"; +import {GlobalDelayChangedEvent, RequestReportEvent, WipeDataEvent} from "@/model/events"; +import {property} from "lit/decorators.js"; +import sharedCss from "@/components/shared.css"; +import settingsComponentCss from "@/components/controls/settings.component.css"; + +@customElement('wiretap-controls-settings') +export class WiretapControlsSettingsComponent extends LitElement { + + static styles = [sharedCss, settingsComponentCss] + + @property({type: Number}) + globalDelay: number; + + handleGlobalDelayChange(event: CustomEvent) { + const delay = event.detail.value + this.dispatchEvent(new CustomEvent(GlobalDelayChangedEvent, {detail: delay})) + } + + wipeData() { + this.dispatchEvent(new CustomEvent(WipeDataEvent)) + } + + sendReportRequest() { + this.dispatchEvent(new CustomEvent(RequestReportEvent)) + } + + render() { + return html` + + + + +
+ Reset State +
+ + + Download Session Data + ` + + } +} diff --git a/ui/src/components/shared.css.ts b/ui/src/components/shared.css.ts index 8b8868a..16fd899 100644 --- a/ui/src/components/shared.css.ts +++ b/ui/src/components/shared.css.ts @@ -56,4 +56,25 @@ export default css` margin-bottom: 20px; color: var(--primary-color); } + + sl-tag.method { + width: 80px; + text-align: center; + } + + .method::part(base) { + background: var(--background-color); + border-radius: 0; + text-align: center; + font-family: var(--font-stack); + width:100%; + } + + .method::part(content) { + border-radius: 0; + text-align: center; + width: 100%; + display: inline-block; + } + ` \ No newline at end of file diff --git a/ui/src/components/transaction/transaction-container.component.ts b/ui/src/components/transaction/transaction-container.component.ts index 77d14c9..6ea6bc8 100644 --- a/ui/src/components/transaction/transaction-container.component.ts +++ b/ui/src/components/transaction/transaction-container.component.ts @@ -8,7 +8,8 @@ import transactionContainerComponentCss from "./transaction-container.component. import {HttpTransactionViewComponent} from "./transaction-view.component"; import {SpecEditor} from "@/components/editor/editor.component"; import {ViolationLocation} from "@/model/events"; -import {WiretapCurrentSpec, WiretapLocalStorage} from "@/model/constants"; +import {WiretapCurrentSpec, WiretapFiltersKey, WiretapLocalStorage} from "@/model/constants"; +import {AreFiltersActive, WiretapFilters} from "@/model/controls"; @customElement('http-transaction-container') export class HttpTransactionContainerComponent extends LitElement { @@ -19,6 +20,8 @@ export class HttpTransactionContainerComponent extends LitElement { private _selectedTransactionStore: Bag; private _specStore: Bag; private _transactionComponents: HttpTransactionItemComponent[] = []; + private _filteredTransactionComponents: HttpTransactionItemComponent[] = []; + private readonly _filtersStore: Bag; @state() private _mappedHttpTransactions: Map @@ -32,29 +35,41 @@ export class HttpTransactionContainerComponent extends LitElement { @query('spec-editor') private _specEditor: SpecEditor; + private _filters: WiretapFilters; + constructor(allTransactionStore: Bag, selectedTransactionStore: Bag, - specStore: Bag) { + specStore: Bag, + filtersStore: Bag) { super() this._allTransactionStore = allTransactionStore this._selectedTransactionStore = selectedTransactionStore this._specStore = specStore; this._mappedHttpTransactions = new Map() + this._filtersStore = filtersStore; + this._filters = new WiretapFilters(); + + // filters store & subscribe to filter changes. + this._filtersStore.subscribe(WiretapFiltersKey, this.filtersChanged.bind(this)) + + } + + filtersChanged(filters: WiretapFilters) { + this._filters = filters; + this.filterComponents() + this.requestUpdate(); } reset(): void { this._selectedTransactionStore.reset() } - connectedCallback() { super.connectedCallback(); // listen for changes to selected transaction. this._selectedTransactionStore.onAllChanges(this.handleSelectedTransactionChange.bind(this)) this._specStore.subscribe(WiretapCurrentSpec, this.handleSpecChange.bind(this)) - - this._allTransactionStore.onAllChanges(this.handleTransactionChange.bind(this)) this._allTransactionStore.onPopulated((storeData: Map) => { // rebuild our internal state @@ -71,6 +86,7 @@ export class HttpTransactionContainerComponent extends LitElement { // save our internal state. this._mappedHttpTransactions = savedTransactions + // extract state this._mappedHttpTransactions.forEach( (v: HttpTransactionContainer) => { @@ -78,6 +94,8 @@ export class HttpTransactionContainerComponent extends LitElement { this._transactionComponents.push(comp) } ); + + this.filterComponents() }); } @@ -133,7 +151,6 @@ export class HttpTransactionContainerComponent extends LitElement { this._mappedHttpTransactions.set(value.id, container) const comp: HttpTransactionItemComponent = new HttpTransactionItemComponent(value) this._transactionComponents.push(comp) - this.requestUpdate(); } } else { // remove it. @@ -148,13 +165,33 @@ export class HttpTransactionContainerComponent extends LitElement { }); const index = this._transactionComponents.indexOf(comp); this._transactionComponents.splice(index, 1); - this.requestUpdate(); } + if (this._filters) { + this.filterComponents() + } + this.requestUpdate(); } + filterComponents() { + this._filteredTransactionComponents = this._transactionComponents.filter( + (v: HttpTransactionItemComponent) => { + const filter = v.httpTransaction.matchesMethodFilter(this._filters); + if (filter == false) { + return false; + } + return true; + }); + this.requestUpdate(); + } render() { - const reversed = this._transactionComponents.sort( + + let components = this._transactionComponents; + if (this._filters && AreFiltersActive(this._filters)) { + components = this._filteredTransactionComponents; + } + + const reversed = components.sort( (a: HttpTransactionItemComponent, b: HttpTransactionItemComponent) => { return b.httpTransaction.timestamp - a.httpTransaction.timestamp }); @@ -171,7 +208,8 @@ export class HttpTransactionContainerComponent extends LitElement {
- +
diff --git a/ui/src/components/transaction/transaction-item.component.css.ts b/ui/src/components/transaction/transaction-item.component.css.ts index 271f104..6b4de8c 100644 --- a/ui/src/components/transaction/transaction-item.component.css.ts +++ b/ui/src/components/transaction/transaction-item.component.css.ts @@ -36,19 +36,7 @@ export default css` color: var(--primary-color); } - .method::part(base) { - background: var(--background-color); - border-radius: 0; - text-align: center; - width:100%; - } - .method::part(content) { - border-radius: 0; - text-align: center; - width: 100%; - display: inline-block; - } .tab::part(base) { font: var(--font-stack); @@ -80,10 +68,7 @@ export default css` margin-right: 12px; } - sl-tag { - width: 80px; - text-align: center; - } + .delay { width: 70px; diff --git a/ui/src/components/transaction/transaction-item.component.ts b/ui/src/components/transaction/transaction-item.component.ts index be9680a..2920706 100644 --- a/ui/src/components/transaction/transaction-item.component.ts +++ b/ui/src/components/transaction/transaction-item.component.ts @@ -2,15 +2,19 @@ import {customElement, state} from "lit/decorators.js"; import {html, LitElement, TemplateResult} from "lit"; import {HttpTransaction} from "@/model/http_transaction"; import transactionComponentCss from "@/components/transaction/transaction-item.component.css"; +import {ExchangeMethod} from "@/model/exchange_method"; import Prism from 'prismjs' -import 'prismjs/components/prism-javascript' // Language +import 'prismjs/components/prism-javascript' import 'prismjs/themes/prism-okaidia.css' -import {HttpTransactionSelectedEvent} from "@/model/events"; // Theme +import {HttpTransactionSelectedEvent} from "@/model/events"; +import sharedCss from "@/components/shared.css"; +import {Filter, WiretapFilters} from "@/model/controls"; + @customElement('http-transaction-item') export class HttpTransactionItemComponent extends LitElement { - static styles = transactionComponentCss + static styles = [sharedCss, transactionComponentCss] @state() _httpTransaction: HttpTransaction @@ -68,23 +72,6 @@ export class HttpTransactionItemComponent extends LitElement { const resp = this._httpTransaction?.httpResponse; this._processing = req && !resp; - const exchangeMethod = (method: string): string => { - switch (method) { - case 'GET': - return 'success' - case 'POST': - return 'primary' - case 'PUT': - return 'primary' - case 'DELETE': - return 'danger' - case 'PATCH': - return 'warning' - default: - return 'neutral' - } - } - let tClass = "transaction"; if (this._active) { tClass += " active"; @@ -114,7 +101,7 @@ export class HttpTransactionItemComponent extends LitElement { return html`
- ${req.method} + ${req.method} ${decodeURI(req.url)}
${delay} diff --git a/ui/src/components/wiretap-header/header.css.ts b/ui/src/components/wiretap-header/header.css.ts index 0359490..cfd657c 100644 --- a/ui/src/components/wiretap-header/header.css.ts +++ b/ui/src/components/wiretap-header/header.css.ts @@ -12,7 +12,7 @@ export default css` } wiretap-controls { - width: 50px; + width: 100px; height: 55px; position: absolute; right: 2px diff --git a/ui/src/index.ts b/ui/src/index.ts index f4fcf7a..fa0a28f 100644 --- a/ui/src/index.ts +++ b/ui/src/index.ts @@ -13,6 +13,13 @@ import '@shoelace-style/shoelace/dist/components/spinner/spinner.js'; import '@shoelace-style/shoelace/dist/components/drawer/drawer.js'; import '@shoelace-style/shoelace/dist/components/button/button.js'; import '@shoelace-style/shoelace/dist/components/input/input.js'; +import '@shoelace-style/shoelace/dist/components/dropdown/dropdown.js'; +import '@shoelace-style/shoelace/dist/components/menu/menu.js'; +import '@shoelace-style/shoelace/dist/components/menu-item/menu-item.js'; +import '@shoelace-style/shoelace/dist/components/divider/divider.js'; +import '@shoelace-style/shoelace/dist/components/select/select.js'; +import '@shoelace-style/shoelace/dist/components/option/option.js'; + import './css/variables.css' @@ -29,6 +36,9 @@ import './components/editor/editor.component'; import './components/wiretap-header/metrics.component'; import './components/wiretap-header/metric.component'; import './components/controls/controls.component'; +import './components/controls/settings.component'; +import './components/controls/filters.component'; + import './model/http_transaction'; import './wiretap'; @@ -38,5 +48,5 @@ import './wiretap'; import {setBasePath} from '@shoelace-style/shoelace/dist/utilities/base-path.js'; // Set the base path to the folder you copied Shoelace's assets to -setBasePath('/assets/shoelace'); - +//setBasePath('/assets/shoelace'); +setBasePath('/shoelace'); diff --git a/ui/src/model/constants.ts b/ui/src/model/constants.ts index 0c4503e..c2bdb5e 100644 --- a/ui/src/model/constants.ts +++ b/ui/src/model/constants.ts @@ -10,6 +10,12 @@ export const WiretapHttpTransactionStore = "http-transaction-store"; export const WiretapSelectedTransactionStore = "selected-transaction-store"; export const WiretapSpecStore = "wiretap-spec-store"; export const WiretapControlsStore = "wiretap-controls-store"; + +export const WiretapFiltersStore = "wiretap-filters-store"; + + +export const WiretapFiltersKey = "wiretap-filters"; + export const WiretapControlsKey = "wiretap-controls"; export const WiretapCurrentSpec = "current-spec"; export const GetCurrentSpecCommand = "get-current-spec"; @@ -19,5 +25,7 @@ export const RequestReportCommand = "generate-report-request"; export const WiretapLocalStorage = "wiretap-transactions"; + + export const TopicPrefix = "/topic/"; export const QueuePrefix = "/queue/"; diff --git a/ui/src/model/controls.ts b/ui/src/model/controls.ts index 8ae343e..29bbe7e 100644 --- a/ui/src/model/controls.ts +++ b/ui/src/model/controls.ts @@ -1,9 +1,44 @@ import {HttpTransaction} from "@/model/http_transaction"; +import {RanchUtils} from "@pb33f/ranch"; export class WiretapControls { globalDelay: number; } +export class WiretapFilters { + + constructor() { + this.filterMethod = { + id: RanchUtils.genShortId(5), + keyword: "", + } + this.filterKeywords = []; + this.filterChain = []; + } + filterMethod: Filter; + filterKeywords: Filter[]; + filterChain: Filter[]; + +} + +export function AreFiltersActive(filters: WiretapFilters): boolean { + if (filters.filterMethod.keyword.length > 0) { + return true; + } + if (filters.filterKeywords.length > 0) { + return true; + } + return filters.filterChain.length > 0; + +} + + +export interface Filter { + id?: string; + keyword: string; +} + + export interface ControlsResponse { config: WiretapConfig; } diff --git a/ui/src/model/events.ts b/ui/src/model/events.ts index 4df7c13..50b4527 100644 --- a/ui/src/model/events.ts +++ b/ui/src/model/events.ts @@ -1,5 +1,9 @@ export const HttpTransactionSelectedEvent = "httpTransactionSelected"; export const ViolationLocationSelectionEvent = "violationLocationSelected"; +export const GlobalDelayChangedEvent = "globalDelayChanged"; +export const RequestReportEvent = "requestReport"; +export const CloseSettingsEvent = "closeSettings"; + export interface ViolationLocation { line: number; column: number; diff --git a/ui/src/model/exchange_method.ts b/ui/src/model/exchange_method.ts new file mode 100644 index 0000000..e65a47f --- /dev/null +++ b/ui/src/model/exchange_method.ts @@ -0,0 +1,16 @@ +export function ExchangeMethod(method: string): string { + switch (method) { + case 'GET': + return 'success' + case 'POST': + return 'primary' + case 'PUT': + return 'primary' + case 'DELETE': + return 'danger' + case 'PATCH': + return 'warning' + default: + return 'neutral' + } +} \ No newline at end of file diff --git a/ui/src/model/http_transaction.ts b/ui/src/model/http_transaction.ts index 6109f4e..ea54292 100644 --- a/ui/src/model/http_transaction.ts +++ b/ui/src/model/http_transaction.ts @@ -1,4 +1,5 @@ import {ExtractQueryString} from "@/model/extract_query"; +import {Filter, WiretapFilters} from "@/model/controls"; export interface HttpCookie { value?: string; @@ -88,7 +89,7 @@ export class HttpResponse { } } -export interface HttpTransaction { +export class HttpTransaction { timestamp?: number; delay?: number; httpRequest?: HttpRequest; @@ -96,16 +97,38 @@ export interface HttpTransaction { httpResponse?: HttpResponse; responseValidation?: ValidationError[]; id?: string; + + constructor(timestamp?: number, + delay?: number, + httpRequest?: HttpRequest, + httpResponse?: HttpResponse, + id?: string, + requestValidation?: ValidationError[], + responseValidation?: ValidationError[]) { + this.timestamp = timestamp; + this.delay = delay; + this.httpRequest = httpRequest; + this.httpResponse = httpResponse; + this.id = id; + this.requestValidation = requestValidation; + this.responseValidation = responseValidation; + } + + matchesMethodFilter(filter: WiretapFilters): Filter | boolean { + if (filter?.filterMethod?.keyword.toLowerCase() === this.httpRequest.method.toLowerCase()) { + return filter.filterMethod; + } + return false; + } } export function BuildLiveTransactionFromState(httpTransaction: HttpTransaction): HttpTransaction { - return { - delay: httpTransaction.delay, - timestamp: httpTransaction.timestamp, - httpRequest: Object.assign(new HttpRequest(), httpTransaction.httpRequest), - httpResponse: Object.assign(new HttpResponse(), httpTransaction.httpResponse), - id: httpTransaction.id, - requestValidation: httpTransaction.requestValidation, - responseValidation: httpTransaction.responseValidation, - } + return new HttpTransaction( + httpTransaction.timestamp, + httpTransaction.delay, + Object.assign(new HttpRequest(), httpTransaction.httpRequest), + Object.assign(new HttpResponse(), httpTransaction.httpResponse), + httpTransaction.id, + httpTransaction.requestValidation, + httpTransaction.responseValidation); } \ No newline at end of file diff --git a/ui/src/wiretap.ts b/ui/src/wiretap.ts index 85f073a..c7254b9 100644 --- a/ui/src/wiretap.ts +++ b/ui/src/wiretap.ts @@ -6,13 +6,13 @@ import {Bus, BusCallback, Channel, CommandResponse, CreateBus, Subscription} fro import {HttpTransactionContainerComponent} from "./components/transaction/transaction-container.component"; import * as localforage from "localforage"; import {HeaderComponent} from "@/components/wiretap-header/header.component"; -import {WiretapControls} from "@/model/controls"; +import {WiretapControls, WiretapFilters} from "@/model/controls"; import { GetCurrentSpecCommand, QueuePrefix, SpecChannel, TopicPrefix, WiretapChannel, WiretapConfigurationChannel, WiretapControlsChannel, WiretapControlsKey, WiretapControlsStore, - WiretapCurrentSpec, + WiretapCurrentSpec, WiretapFiltersKey, WiretapFiltersStore, WiretapHttpTransactionStore, WiretapLocalStorage, WiretapReportChannel, WiretapSelectedTransactionStore, @@ -20,17 +20,19 @@ import { } from "@/model/constants"; declare global { - interface Window { wiretapPort: any; } + interface Window { + wiretapPort: any; + } } - @customElement('wiretap-application') export class WiretapComponent extends LitElement { private readonly _storeManager: BagManager; private readonly _httpTransactionStore: Bag; private readonly _selectedTransactionStore: Bag; + private readonly _filtersStore: Bag; private readonly _controlsStore: Bag; private readonly _specStore: Bag; private readonly _bus: Bus; @@ -63,6 +65,7 @@ export class WiretapComponent extends LitElement { @property({type: Number}) complianceLevel: number = 100.0; + constructor() { super(); //configure local storage @@ -93,6 +96,9 @@ export class WiretapComponent extends LitElement { // controls store this._controlsStore = this._storeManager.createBag(WiretapControlsStore); + // filters store & subscribe to filter changes. + this._filtersStore = this._storeManager.createBag(WiretapFiltersStore); + // set up wiretap channels this._wiretapChannel = this._bus.createChannel(WiretapChannel); this._wiretapSpecChannel = this._bus.createChannel(SpecChannel); @@ -146,20 +152,22 @@ export class WiretapComponent extends LitElement { let responses = 0; let violations = 0; let violated = 0.0 - previousTransactions.forEach((transaction: HttpTransaction) => { - requests++; - if (transaction.httpResponse) { - responses++; - } - if (transaction.requestValidation) { - violated += 0.5; - violations += transaction.requestValidation.length - } - if (transaction.responseValidation) { - violated += 0.5; - violations += transaction.responseValidation.length; - } - }); + if (previousTransactions) { + previousTransactions.forEach((transaction: HttpTransaction) => { + requests++; + if (transaction.httpResponse) { + responses++; + } + if (transaction.requestValidation) { + violated += 0.5; + violations += transaction.requestValidation.length + } + if (transaction.responseValidation) { + violated += 0.5; + violations += transaction.responseValidation.length; + } + }); + } this.requestCount = requests; this.responseCount = responses; this.violationsCount = violations; @@ -203,12 +211,13 @@ export class WiretapComponent extends LitElement { return (msg: CommandResponse) => { const wiretapMessage = msg.payload as HttpTransaction - const httpTransaction: HttpTransaction = { - httpRequest: Object.assign(new HttpRequest(), wiretapMessage.httpRequest), - id: wiretapMessage.id, - requestValidation: wiretapMessage.requestValidation, - responseValidation: wiretapMessage.responseValidation, - } + + const httpTransaction: HttpTransaction = new HttpTransaction(); + httpTransaction.httpRequest = Object.assign(new HttpRequest(), wiretapMessage.httpRequest); + httpTransaction.id = wiretapMessage.id; + httpTransaction.requestValidation = wiretapMessage.requestValidation; + httpTransaction.responseValidation = wiretapMessage.responseValidation; + // get global delay const controls = this._controlsStore.get(WiretapControlsKey) @@ -279,7 +288,8 @@ export class WiretapComponent extends LitElement { transaction = new HttpTransactionContainerComponent( this._httpTransactionStore, this._selectedTransactionStore, - this._specStore); + this._specStore, + this._filtersStore); this._transactionContainer = transaction; } return html`