Merge remote-tracking branch 'origin/main' into feature/user-section-v2

This commit is contained in:
Jesper Møller Jensen
2022-11-22 09:06:35 +01:00
19 changed files with 1233 additions and 43181 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -48,7 +48,7 @@
"@umbraco-ui/uui-modal-dialog": "file:umbraco-ui-uui-modal-dialog-0.0.0.tgz",
"@umbraco-ui/uui-modal-sidebar": "file:umbraco-ui-uui-modal-sidebar-0.0.0.tgz",
"element-internals-polyfill": "^1.1.16",
"lit": "^2.4.0",
"lit": "^2.4.1",
"lodash": "^4.17.21",
"openapi-typescript-fetch": "^1.1.3",
"router-slot": "^1.5.5",
@@ -59,7 +59,7 @@
"@babel/core": "^7.20.2",
"@mdx-js/react": "^2.1.5",
"@open-wc/testing": "^3.1.7",
"@playwright/test": "^1.27.1",
"@playwright/test": "^1.28.0",
"@storybook/addon-a11y": "^6.5.13",
"@storybook/addon-actions": "^6.5.13",
"@storybook/addon-essentials": "^6.5.13",
@@ -69,7 +69,7 @@
"@storybook/web-components": "^6.5.13",
"@types/chai": "^4.3.4",
"@types/lodash-es": "^4.17.6",
"@types/mocha": "^9.1.1",
"@types/mocha": "^10.0.0",
"@types/uuid": "^8.3.4",
"@typescript-eslint/eslint-plugin": "^5.43.0",
"@typescript-eslint/parser": "^5.43.0",
@@ -83,19 +83,19 @@
"eslint-import-resolver-typescript": "^3.5.2",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-lit": "^1.6.1",
"eslint-plugin-lit-a11y": "^2.2.3",
"eslint-plugin-lit-a11y": "^2.3.0",
"eslint-plugin-local-rules": "^1.3.2",
"eslint-plugin-storybook": "^0.6.7",
"lit-html": "^2.4.0",
"msw": "^0.47.4",
"msw": "^0.48.3",
"msw-storybook-addon": "^1.6.3",
"openapi-typescript-codegen": "^0.23.0",
"playwright-msw": "^2.0.1",
"plop": "^3.1.1",
"prettier": "2.7.1",
"tiny-glob": "^0.2.9",
"typescript": "^4.8.4",
"vite": "^3.2.3",
"typescript": "^4.9.3",
"vite": "^3.2.4",
"vite-plugin-static-copy": "^0.12.0",
"vite-tsconfig-paths": "^3.5.2",
"web-component-analyzer": "^2.0.0-next.4"

View File

@@ -2,13 +2,13 @@
/* tslint:disable */
/**
* Mock Service Worker (0.47.4).
* Mock Service Worker (0.48.3).
* @see https://github.com/mswjs/msw
* - Please do NOT modify this file.
* - Please do NOT serve this file on production.
*/
const INTEGRITY_CHECKSUM = 'b3066ef78c2f9090b4ce87e874965995'
const INTEGRITY_CHECKSUM = '3d6b9f06410d179a7f7404d4bf4c3c70'
const activeClientIds = new Set()
self.addEventListener('install', function () {
@@ -174,7 +174,7 @@ async function handleRequest(event, requestId) {
async function resolveMainClient(event) {
const client = await self.clients.get(event.clientId)
if (client.frameType === 'top-level') {
if (client?.frameType === 'top-level') {
return client
}

View File

@@ -1,17 +1,66 @@
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
import { html, LitElement, css, nothing } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { IRoute, IRoutingInfo, path } from 'router-slot';
import { UmbDashboardExamineIndexElement } from './views/section-view-examine-indexers';
import { UmbDashboardExamineSearcherElement } from './views/section-view-examine-searchers';
import { UmbContextConsumerMixin } from '@umbraco-cms/context-api';
@customElement('umb-dashboard-examine-management')
export class UmbDashboardExamineManagementElement extends LitElement {
static styles = [UUITextStyles, css``];
export class UmbDashboardExamineManagementElement extends UmbContextConsumerMixin(LitElement) {
static styles = [
css`
a {
color: var(--uui-color-text);
background: transparent;
border: none;
text-decoration: underline;
cursor: pointer;
display: inline-block;
}
`,
];
@state()
private _routes: IRoute[] = [
{
path: `/index/:indexerName`,
component: () => import('./views/section-view-examine-indexers'),
setup: (component: HTMLElement, info: IRoutingInfo) => {
const element = component as UmbDashboardExamineIndexElement;
element.indexName = info.match.params.indexerName;
},
},
{
path: `/searcher/:searcherName`,
component: () => import('./views/section-view-examine-searchers'),
setup: (component: HTMLElement, info: IRoutingInfo) => {
const element = component as UmbDashboardExamineSearcherElement;
element.searcherName = info.match.params.searcherName;
},
},
{
path: ``,
component: () => import('./views/section-view-examine-overview'),
},
];
@state()
private _currentPath?: string;
private _onRouteChange() {
this._currentPath = path();
}
private get backbutton(): boolean {
return this._currentPath != '/section/settings/dashboard/examine-management/' || !this._currentPath ? true : false;
}
render() {
return html`
<uui-box>
<h1>Examine Management</h1>
</uui-box>
`;
return html` ${this.backbutton
? html` <a href="/section/settings/dashboard/examine-management/"> &larr; Back to overview </a> `
: nothing}
<router-slot @changestate="${this._onRouteChange}" .routes=${this._routes}></router-slot>`;
}
}

View File

@@ -4,12 +4,27 @@ import { html } from 'lit-html';
import type { UmbDashboardExamineManagementElement } from './dashboard-examine-management.element';
import './dashboard-examine-management.element';
import type { UmbDashboardExamineOverviewElement } from './views/section-view-examine-overview';
import './views/section-view-examine-overview';
import type { UmbDashboardExamineIndexElement } from './views/section-view-examine-indexers';
import './views/section-view-examine-indexers';
import type { UmbDashboardExamineSearcherElement } from './views/section-view-examine-searchers';
import './views/section-view-examine-searchers';
export default {
title: 'Dashboards/Examine Management',
component: 'umb-dashboard-examine-management',
id: 'umb-dashboard-examine-management',
} as Meta;
export const AAAOverview: Story<UmbDashboardExamineManagementElement> = () =>
html` <umb-dashboard-examine-management></umb-dashboard-examine-management>`;
export const AAAOverview: Story<UmbDashboardExamineOverviewElement> = () =>
html` <umb-dashboard-examine-overview></umb-dashboard-examine-overview>`;
AAAOverview.storyName = 'Overview';
export const Index: Story<UmbDashboardExamineIndexElement> = () =>
html` <umb-dashboard-examine-index indexName="InternalIndex"></umb-dashboard-examine-index>`;
export const Searcher: Story<UmbDashboardExamineSearcherElement> = () =>
html` <umb-dashboard-examine-searcher searcherName="InternalSearcher"></umb-dashboard-examine-searcher>`;

View File

@@ -0,0 +1,38 @@
export interface SearcherModel {
name: string;
providerProperties: unknown; //TODO
}
export interface IndexModel {
name: string;
canRebuild: boolean;
healthStatus: string;
isHealthy: boolean;
providerProperties: ProviderPropertiesModel;
}
export interface ProviderPropertiesModel {
CommitCount: number;
DefaultAnalyzer: string;
DocumentCount: number;
FieldCount: number;
LuceneDirectory: string;
LuceneIndexFolder: string;
DirectoryFactory: string;
EnableDefaultEventHandler: boolean;
PublishedValuesOnly: boolean;
SupportProtectedContent: boolean;
IncludeFields?: string[];
}
export interface FieldViewModel {
name: string;
values: string[];
}
export interface SearchResultsModel {
id: number;
name: string;
fields: FieldViewModel[];
score: number;
}

View File

@@ -0,0 +1,97 @@
import { html, css } from 'lit';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement, state } from 'lit/decorators.js';
import { UmbModalLayoutElement } from '@umbraco-cms/services';
export interface UmbModalFieldsSettingsData {
name: string;
exposed: boolean;
}
@customElement('umb-modal-layout-fields-settings')
export class UmbModalLayoutFieldsSettingsElement extends UmbModalLayoutElement<UmbModalFieldsSettingsData> {
static styles = [
UUITextStyles,
css`
:host {
display: relative;
}
uui-dialog-layout {
display: flex;
flex-direction: column;
height: 100%;
background-color: white;
box-shadow: var(--uui-shadow-depth-1, 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24));
border-radius: var(--uui-border-radius);
padding: var(--uui-size-space-5);
box-sizing: border-box;
}
uui-scroll-container {
overflow-y: scroll;
max-height: 100%;
min-height: 0;
flex: 1;
}
div {
margin-top: var(--uui-size-space-5);
display: flex;
flex-direction: row-reverse;
}
`,
];
@state()
private _fields?: UmbModalFieldsSettingsData[];
private _handleClose() {
this.modalHandler?.close({ fields: this._fields });
}
disconnectedCallback(): void {
super.disconnectedCallback();
this._handleClose();
}
firstUpdated() {
this.data
? (this._fields = Object.values(this.data).map((field) => {
return { name: field.name, exposed: field.exposed };
}))
: '';
}
render() {
if (this._fields) {
return html`
<uui-dialog-layout headline="Show fields">
<uui-scroll-container id="field-settings">
<span>
${Object.values(this._fields).map((field, index) => {
return html`<uui-toggle
name="${field.name}"
label="${field.name}"
.checked="${field.exposed}"
@change="${() => {
this._fields ? (this._fields[index].exposed = !field.exposed) : '';
}}"></uui-toggle>
<br />`;
})}
</span>
</uui-scroll-container>
<div>
<uui-button look="primary" label="Close sidebar" @click="${this._handleClose}">Close</uui-button>
</div>
</uui-dialog-layout>
`;
} else return html``;
}
}
declare global {
interface HTMLElementTagNameMap {
'umb-modal-layout-fields-settings': UmbModalLayoutFieldsSettingsElement;
}
}

View File

@@ -0,0 +1,82 @@
import { html, css } from 'lit';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement } from 'lit/decorators.js';
import { UmbModalLayoutElement } from '@umbraco-cms/services';
import { SearchResultsModel } from 'src/backoffice/dashboards/examine-management/examine-extension';
@customElement('umb-modal-layout-fields-viewer')
export class UmbModalLayoutFieldsViewerElement extends UmbModalLayoutElement<SearchResultsModel> {
static styles = [
UUITextStyles,
css`
:host {
display: relative;
}
uui-dialog-layout {
display: flex;
flex-direction: column;
height: 100%;
background-color: white;
box-shadow: var(--uui-shadow-depth-1, 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24));
border-radius: var(--uui-border-radius);
padding: var(--uui-size-space-5);
box-sizing: border-box;
}
span {
display: block;
padding-right: var(--uui-size-space-5);
}
uui-scroll-container {
line-height: 0;
overflow-y: scroll;
max-height: 100%;
min-height: 0;
}
div {
margin-top: var(--uui-size-space-5);
display: flex;
flex-direction: row-reverse;
}
`,
];
private _handleClose() {
this.modalHandler?.close();
}
render() {
if (this.data) {
return html`
<uui-dialog-layout class="uui-text" headline="${this.data.name}">
<uui-scroll-container id="field-viewer">
<span>
<uui-table>
<uui-table-head>
<uui-table-head-cell> Field </uui-table-head-cell>
<uui-table-head-cell> Value </uui-table-head-cell>
</uui-table-head>
${Object.values(this.data.fields).map((cell) => {
return html`<uui-table-row>
<uui-table-cell> ${cell.name} </uui-table-cell>
<uui-table-cell> ${cell.values.join(', ')} </uui-table-cell>
</uui-table-row>`;
})}
</uui-table>
</span>
</uui-scroll-container>
<div>
<uui-button look="primary" @click="${this._handleClose}">Close</uui-button>
</div>
</uui-dialog-layout>
`;
} else return html``;
}
}
declare global {
interface HTMLElementTagNameMap {
'umb-modal-layout-fields-viewer': UmbModalLayoutFieldsViewerElement;
}
}

View File

@@ -0,0 +1,221 @@
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, LitElement } from 'lit';
import { customElement, state, property } from 'lit/decorators.js';
import { UUIButtonState } from '@umbraco-ui/uui-button';
import { UmbModalService, UmbNotificationService, UmbNotificationDefaultData } from '@umbraco-cms/services';
import { UmbContextConsumerMixin } from '@umbraco-cms/context-api';
import './section-view-examine-searchers';
import { ApiError, ProblemDetails, Index, SearchResource } from '@umbraco-cms/backend-api';
@customElement('umb-dashboard-examine-index')
export class UmbDashboardExamineIndexElement extends UmbContextConsumerMixin(LitElement) {
static styles = [
UUITextStyles,
css`
:host {
display: block;
}
uui-box,
umb-dashboard-examine-searcher {
margin-top: var(--uui-size-space-5);
}
uui-box p {
margin-top: 0;
}
div.flex {
display: flex;
}
div.flex > uui-button {
padding-left: var(--uui-size-space-4);
height: 0;
}
uui-input {
width: 100%;
margin-bottom: var(--uui-size-space-5);
}
uui-table.info uui-table-row:first-child uui-table-cell {
border-top: none;
}
uui-table-head-cell {
text-transform: capitalize;
}
uui-table-row:last-child uui-table-cell {
padding-bottom: 0;
}
uui-icon {
vertical-align: top;
padding-right: var(--uui-size-space-5);
}
.positive {
color: var(--uui-color-positive);
}
.danger {
color: var(--uui-color-danger);
}
button {
background: none;
border: none;
text-decoration: underline;
cursor: pointer;
}
button.bright {
font-style: italic;
color: var(--uui-color-positive-emphasis);
}
`,
];
@property()
indexName!: string;
@state()
private _buttonState?: UUIButtonState = undefined;
@state()
private _indexData!: Index;
private _notificationService?: UmbNotificationService;
private _modalService?: UmbModalService;
private async _getIndexData() {
try {
const index = await SearchResource.getSearchIndexByIndexName({ indexName: this.indexName });
this._indexData = index;
} catch (e) {
if (e instanceof ApiError) {
const error = e as ProblemDetails;
const data: UmbNotificationDefaultData = { message: error.message ?? 'Could not fetch index' };
this._notificationService?.peek('danger', { data });
}
}
}
constructor() {
super();
this.consumeAllContexts(['umbNotificationService', 'umbModalService'], (instances) => {
this._notificationService = instances['umbNotificationService'];
this._modalService = instances['umbModalService'];
});
}
connectedCallback(): void {
super.connectedCallback();
this._getIndexData();
}
private async _onRebuildHandler() {
const modalHandler = this._modalService?.confirm({
headline: `Rebuild ${this.indexName}`,
content: html`
This will cause the index to be rebuilt.<br />
Depending on how much content there is in your site this could take a while.<br />
It is not recommended to rebuild an index during times of high website traffic or when editors are editing
content.
`,
color: 'danger',
confirmLabel: 'Rebuild',
});
modalHandler?.onClose().then(({ confirmed }) => {
if (confirmed) this._rebuild();
});
}
private async _rebuild() {
this._buttonState = 'waiting';
if (this._indexData.name)
try {
await SearchResource.postSearchIndexByIndexNameRebuild({ indexName: this._indexData.name });
this._buttonState = 'success';
} catch (e) {
this._buttonState = 'failed';
if (e instanceof ApiError) {
const error = e as ProblemDetails;
const data: UmbNotificationDefaultData = { message: error.message ?? 'Rebuild error' };
this._notificationService?.peek('danger', { data });
}
}
}
render() {
if (this._indexData) {
return html` <uui-box headline="${this.indexName}">
<p>
<strong>Health Status</strong><br />
The health status of the ${this._indexData.name} and if it can be read
</p>
<div>
<uui-icon-essentials>
<uui-icon
name=${this._indexData.isHealthy ? `check` : `wrong`}
class=${this._indexData.isHealthy ? 'positive' : 'danger'}>
</uui-icon>
</uui-icon-essentials>
${this._indexData.healthStatus}
</div>
</uui-box>
<umb-dashboard-examine-searcher searcherName="${this.indexName}"></umb-dashboard-examine-searcher>
${this.renderPropertyList()} ${this.renderTools()}`;
} else return html``;
}
private renderPropertyList() {
return html`<uui-box headline="Index info">
<p>Lists the properties of the ${this._indexData.name}</p>
<uui-table class="info">
<uui-table-row>
<uui-table-cell style="width:0px; font-weight: bold;"> documentCount </uui-table-cell>
<uui-table-cell>${this._indexData.documentCount} </uui-table-cell>
</uui-table-row>
<uui-table-row>
<uui-table-cell style="width:0px; font-weight: bold;"> fieldCount </uui-table-cell>
<uui-table-cell>${this._indexData.fieldCount} </uui-table-cell>
</uui-table-row>
${this._indexData.providerProperties
? Object.entries(this._indexData.providerProperties).map((entry) => {
return html`<uui-table-row>
<uui-table-cell style="width:0px; font-weight: bold;"> ${entry[0]} </uui-table-cell>
<uui-table-cell clip-text> ${entry[1]} </uui-table-cell>
</uui-table-row>`;
})
: ''}
</uui-table>
</uui-box>`;
}
private renderTools() {
return html` <uui-box headline="Tools">
<p>Tools to manage the ${this._indexData.name}</p>
<uui-button
color="danger"
look="primary"
.state="${this._buttonState}"
@click="${this._onRebuildHandler}"
.disabled="${!this._indexData?.canRebuild}"
label="Rebuild index">
Rebuild
</uui-button>
</uui-box>`;
}
}
export default UmbDashboardExamineIndexElement;
declare global {
interface HTMLElementTagNameMap {
'umb-dashboard-examine-index': UmbDashboardExamineIndexElement;
}
}

View File

@@ -0,0 +1,173 @@
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, LitElement } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { UmbModalService, UmbNotificationService, UmbNotificationDefaultData } from '@umbraco-cms/services';
import { UmbContextConsumerMixin } from '@umbraco-cms/context-api';
import { ApiError, ProblemDetails, Searcher, Index, SearchResource } from '@umbraco-cms/backend-api';
@customElement('umb-dashboard-examine-overview')
export class UmbDashboardExamineOverviewElement extends UmbContextConsumerMixin(LitElement) {
static styles = [
UUITextStyles,
css`
uui-box + uui-box {
margin-top: var(--uui-size-space-5);
}
uui-box p {
margin-top: 0;
}
a {
color: var(--uui-color-text);
background: transparent;
border: none;
text-decoration: underline;
cursor: pointer;
}
uui-table-cell {
line-height: 0;
vertical-align: middle;
}
uui-table-row:last-child uui-table-cell {
padding-bottom: 0;
}
.positive {
color: var(--uui-color-positive);
}
.danger {
color: var(--uui-color-danger);
}
.not-found-message {
font-style: italic;
color: var(--uui-color-text-alt);
}
`,
];
@state()
private _indexers?: Index[];
@state()
private _searchers?: Searcher[];
private _notificationService?: UmbNotificationService;
private _modalService?: UmbModalService;
private async _getIndexers() {
try {
const indexers = await SearchResource.getSearchIndex({ take: 9999, skip: 0 });
this._indexers = indexers.items;
} catch (e) {
if (e instanceof ApiError) {
const error = e as ProblemDetails;
const data: UmbNotificationDefaultData = { message: error.message ?? 'Could not fetch indexers' };
this._notificationService?.peek('danger', { data });
}
}
}
private async _getSearchers() {
try {
const searchers = await SearchResource.getSearchSearcher({ take: 9999, skip: 0 });
this._searchers = searchers.items;
} catch (e) {
if (e instanceof ApiError) {
const error = e as ProblemDetails;
const data: UmbNotificationDefaultData = { message: error.message ?? 'Could not fetch searchers' };
this._notificationService?.peek('danger', { data });
}
}
}
constructor() {
super();
this._getIndexers();
this._getSearchers();
this.consumeAllContexts(['umbNotificationService', 'umbModalService'], (instances) => {
this._notificationService = instances['umbNotificationService'];
this._modalService = instances['umbModalService'];
});
}
render() {
return html`
<uui-box headline="Indexers" class="overview">
<p>
<strong>Manage Examine's indexes</strong><br />
Allows you to view the details of each index and provides some tools for managing the indexes
</p>
${this.renderIndexersList()}
</uui-box>
<uui-box headline="Searchers">
<p>
<strong>Configured Searchers</strong><br />
Shows properties and tools for any configured Searcher (i.e. such as a multi-index searcher)
</p>
${this.renderSearchersList()}
</uui-box>
`;
}
private renderIndexersList() {
if (!this._indexers) return;
return html` <uui-table class="overview">
${this._indexers.map((index) => {
return html`
<uui-table-row>
<uui-table-cell style="width:0px">
<uui-icon-essentials>
<uui-icon
style="vertical-align: top"
name=${index.isHealthy ? `check` : `wrong`}
class=${index.isHealthy ? 'positive' : 'danger'}>
</uui-icon>
</uui-icon-essentials>
</uui-table-cell>
<uui-table-cell>
<a href="${window.location.href.replace(/\/+$/, '')}/index/${index.name}">${index.name}</a>
</uui-table-cell>
</uui-table-row>
`;
})}
</uui-table>`;
}
private renderSearchersList() {
if (!this._searchers) return html`<span class="not-found-message">No searchers were found</span>`;
return html`
<uui-table class="overview2">
${this._searchers.map((searcher) => {
return html`<uui-table-row>
<uui-table-cell style="width:0px">
<uui-icon-essentials>
<uui-icon
style="vertical-align: top"
name="info"></uui-icon>
</uui-icon>
</uui-icon-essentials>
</uui-table-cell>
<uui-table-cell><a href="${window.location.href.replace(/\/+$/, '')}/searcher/${searcher.name}">${searcher.name}</a>
</uui-table-cell>
</uui-table-row>`;
})}
</uui-table>
`;
}
}
export default UmbDashboardExamineOverviewElement;
declare global {
interface HTMLElementTagNameMap {
'umb-dashboard-examine-overview': UmbDashboardExamineOverviewElement;
}
}

View File

@@ -0,0 +1,302 @@
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, LitElement } from 'lit';
import { customElement, state, query, property } from 'lit/decorators.js';
import { UmbModalService, UmbNotificationService, UmbNotificationDefaultData } from '@umbraco-cms/services';
import { UmbContextConsumerMixin } from '@umbraco-cms/context-api';
import { ApiError, ProblemDetails, SearchResult, SearchResource, Field } from '@umbraco-cms/backend-api';
import './modal-views/fields-viewer.element';
import './modal-views/fields-settings.element';
interface ExposedSearchResultField {
name?: string | null;
exposed: boolean;
}
@customElement('umb-dashboard-examine-searcher')
export class UmbDashboardExamineSearcherElement extends UmbContextConsumerMixin(LitElement) {
static styles = [
UUITextStyles,
css`
:host {
display: block;
}
uui-box {
margin-top: var(--uui-size-space-5);
}
uui-box p {
margin-top: 0;
}
div.flex {
display: flex;
}
div.flex > uui-button {
padding-left: var(--uui-size-space-4);
height: 0;
}
uui-input {
width: 100%;
margin-bottom: var(--uui-size-space-5);
}
uui-table-head-cell {
text-transform: capitalize;
}
uui-table-row:last-child uui-table-cell {
padding-bottom: 0;
}
uui-table-cell {
min-width: 100px;
}
button.bright {
font-style: italic;
color: var(--uui-color-positive-emphasis);
}
.field-adder {
line-height: 0;
cursor: pointer;
vertical-align: top;
background: transparent;
border: none;
}
.table-container uui-scroll-container {
padding-bottom: var(--uui-size-space-4);
max-width: 100%;
overflow-x: scroll;
flex: 1;
}
.table-container {
display: flex;
align-items: flex-start;
}
uui-tag {
margin-block: var(--uui-size-5, 15px);
}
.exposed-field uui-button {
align-items: center;
font-weight: normal;
font-size: 10px;
height: 10px;
width: 10px;
margin-top: -5px;
}
.exposed-field-container {
display: flex;
justify-content: space-between;
}
`,
];
private _notificationService?: UmbNotificationService;
private _modalService?: UmbModalService;
@property()
searcherName!: string;
@state()
private _searchResults?: SearchResult[];
@state()
private _exposedFields?: ExposedSearchResultField[];
@query('#search-input')
private _searchInput!: HTMLInputElement;
constructor() {
super();
this.consumeAllContexts(['umbNotificationService', 'umbModalService'], (instances) => {
this._notificationService = instances['umbNotificationService'];
this._modalService = instances['umbModalService'];
});
}
private _onNameClick() {
const data: UmbNotificationDefaultData = { message: 'TODO: Open editor for this' }; // TODO
this._notificationService?.peek('warning', { data });
}
private _onKeyPress(e: KeyboardEvent) {
e.key == 'Enter' ? this._onSearch() : undefined;
}
private async _onSearch() {
if (!this._searchInput.value.length) return;
try {
const res = await SearchResource.getSearchSearcherBySearcherNameSearch({
searcherName: this.searcherName,
query: this._searchInput.value,
take: 9999,
skip: 0,
});
this._searchResults = res.items;
this._updateFieldFilter();
} catch (e) {
if (e instanceof ApiError) {
const error = e as ProblemDetails;
const data: UmbNotificationDefaultData = { message: error.message ?? 'Could not fetch search results' };
this._notificationService?.peek('danger', { data });
}
}
}
private _updateFieldFilter() {
this._searchResults?.map((doc) => {
const document = doc.fields?.filter((field) => {
return field.name?.toUpperCase() !== 'NODENAME';
});
if (document) {
const newFieldNames = document.map((field) => {
return field.name;
});
this._exposedFields = this._exposedFields
? this._exposedFields.filter((field) => {
return { name: field.name, exposed: field.exposed };
})
: newFieldNames?.map((name) => {
return { name, exposed: false };
});
}
});
}
private _onFieldFilterClick() {
const modalHandler = this._modalService?.open('umb-modal-layout-fields-settings', {
type: 'sidebar',
size: 'small',
data: { ...this._exposedFields },
});
modalHandler?.onClose().then(({ fields } = {}) => {
if (!fields) return;
this._exposedFields = fields;
});
}
render() {
return html`
<uui-box headline="Search">
<p>Search the ${this.searcherName} and view the results</p>
<div class="flex">
<uui-input
id="search-input"
placeholder="Type to filter..."
label="Type to filter"
@keypress=${this._onKeyPress}>
</uui-input>
<uui-button color="positive" look="primary" label="Search" @click="${this._onSearch}"> Search </uui-button>
</div>
${this.renderSearchResults()}
</uui-box>
`;
}
private renderSearchResults() {
if (this._searchResults?.length) {
return html`<div class="table-container">
<uui-scroll-container>
<uui-table class="search">
<uui-table-head>
<uui-table-head-cell style="width:0">Score</uui-table-head-cell>
<uui-table-head-cell style="width:0">Id</uui-table-head-cell>
<uui-table-head-cell>Name</uui-table-head-cell>
<uui-table-head-cell>Fields</uui-table-head-cell>
${this.renderHeadCells()}
</uui-table-head>
${this._searchResults?.map((rowData) => {
return html`<uui-table-row>
<uui-table-cell> ${rowData.score} </uui-table-cell>
<uui-table-cell> ${rowData.id} </uui-table-cell>
<uui-table-cell>
<uui-button look="secondary" label="Open editor for this document" @click="${this._onNameClick}">
${rowData.fields?.find((field) => {
if (field.name?.toUpperCase() === 'NODENAME') return field.values;
else return;
})?.values}
</uui-button>
</uui-table-cell>
<uui-table-cell>
<uui-button
class="bright"
look="secondary"
label="Open sidebar to see all fields"
@click="${() =>
this._modalService?.open('umb-modal-layout-fields-viewer', {
type: 'sidebar',
size: 'medium',
data: { ...rowData },
})}">
${rowData.fields ? Object.keys(rowData.fields).length : ''} fields
</uui-button>
</uui-table-cell>
${rowData.fields ? this.renderBodyCells(rowData.fields) : ''}
</uui-table-row>`;
})}
</uui-table>
</uui-scroll-container>
<button class="field-adder" @click="${this._onFieldFilterClick}">
<uui-icon-registry-essential>
<uui-tag look="secondary">
<uui-icon name="add"></uui-icon>
</uui-tag>
</uui-icon-registry-essential>
</button>
</div>`;
}
return;
}
renderHeadCells() {
return html`${this._exposedFields?.map((field) => {
return field.exposed
? html`<uui-table-head-cell class="exposed-field">
<div class="exposed-field-container">
<span>${field.name}</span>
<uui-button
look="secondary"
label="Close field ${field.name}"
compact
@click="${() => {
this._exposedFields = this._exposedFields?.map((f) => {
return f.name == field.name ? { name: f.name, exposed: false } : f;
});
}}"
>x</uui-button
>
</div>
</uui-table-head-cell>`
: html``;
})}`;
}
renderBodyCells(cellData: Field[]) {
return html`${this._exposedFields?.map((slot) => {
return cellData.map((field) => {
return slot.exposed && field.name == slot.name
? html`<uui-table-cell clip-text>${field.values}</uui-table-cell>`
: html``;
});
})}`;
}
}
export default UmbDashboardExamineSearcherElement;
declare global {
interface HTMLElementTagNameMap {
'umb-dashboard-examine-searcher': UmbDashboardExamineSearcherElement;
}
}

View File

@@ -28,6 +28,7 @@ export class UmbSectionDashboardsElement extends UmbContextConsumerMixin(UmbObse
}
#scroll-container {
width: calc(-300px + 100vw);
height: calc(100vh - 70px - 60px); // TODO: This is a temporary fix to get scrolling to work
// changed it so the height is correct but the fix is still not ideal. the 70px and 60px are the height of the blue top bar and the dashboard menu. Need a better solution still.
}

View File

@@ -11,6 +11,7 @@ import { handlers as userHandlers } from './domains/user.handlers';
import { handlers as telemetryHandlers } from './domains/telemetry.handlers';
import { handlers as usersHandlers } from './domains/users.handlers';
import { handlers as userGroupsHandlers } from './domains/user-groups.handlers';
import { handlers as examineManagementHandlers } from './domains/examine-management.handlers';
const handlers = [
serverHandlers.serverVersionHandler,
@@ -26,6 +27,7 @@ const handlers = [
...publishedStatusHandlers,
...usersHandlers,
...userGroupsHandlers,
...examineManagementHandlers,
];
switch (import.meta.env.VITE_UMBRACO_INSTALL_STATUS) {

View File

@@ -0,0 +1,145 @@
import { Index, PagedIndex, SearchResult } from '@umbraco-cms/backend-api';
export function getIndexByName(indexName: string) {
return Indexers.find((index) => {
if (index.name) return index.name.toLocaleLowerCase() == indexName.toLocaleLowerCase();
else return undefined;
});
}
export function getSearchResultsMockData(): SearchResult[] {
return searchResultMockData;
}
export const Indexers: Index[] = [
{
name: 'ExternalIndex',
canRebuild: true,
healthStatus: 'Healthy',
isHealthy: true,
documentCount: 0,
fieldCount: 0,
providerProperties: {
CommitCount: 0,
DefaultAnalyzer: 'StandardAnalyzer',
LuceneDirectory: 'SimpleFSDirectory',
LuceneIndexFolder: '/ /umbraco /data /temp /examineindexes /externalindex',
DirectoryFactory:
'Umbraco.Cms.Infrastructure.Examine.ConfigurationEnabledDirectoryFactory, Umbraco.Examine.Lucene, Version=10.2.0.0, Culture=neutral, PublicKeyToken=null',
EnableDefaultEventHandler: true,
PublishedValuesOnly: true,
SupportProtectedContent: false,
},
},
{
name: 'InternalIndex',
canRebuild: true,
healthStatus: 'Healthy',
isHealthy: true,
documentCount: 0,
fieldCount: 0,
providerProperties: {
CommitCount: 0,
DefaultAnalyzer: 'CultureInvariantWhitespaceAnalyzer',
LuceneDirectory: 'SimpleFSDirectory',
LuceneIndexFolder: '/ /umbraco /data /temp /examineindexes /internalindex',
DirectoryFactory:
'Umbraco.Cms.Infrastructure.Examine.ConfigurationEnabledDirectoryFactory, Umbraco.Examine.Lucene, Version=10.2.0.0, Culture=neutral, PublicKeyToken=null',
EnableDefaultEventHandler: true,
PublishedValuesOnly: false,
SupportProtectedContent: true,
IncludeFields: ['id', 'nodeName', 'updateDate', 'loginName', 'email', '__Key'],
},
},
{
name: 'MemberIndex',
canRebuild: true,
healthStatus: 'Healthy',
isHealthy: true,
fieldCount: 0,
documentCount: 0,
providerProperties: {
CommitCount: 0,
DefaultAnalyzer: 'CultureInvariantWhitespaceAnalyzer',
DirectoryFactory:
'Umbraco.Cms.Infrastructure.Examine.ConfigurationEnabledDirectoryFactory, Umbraco.Examine.Lucene, Version=10.2.0.0, Culture=neutral, PublicKeyToken=null',
EnableDefaultEventHandler: true,
IncludeFields: ['id', 'nodeName', 'updateDate', 'loginName', 'email', '__Key'],
LuceneDirectory: 'SimpleFSDirectory',
LuceneIndexFolder: '/ /umbraco /data /temp /examineindexes /membersindex',
PublishedValuesOnly: false,
SupportProtectedContent: false,
},
},
];
export const PagedIndexers: PagedIndex = {
items: Indexers,
total: 0,
};
export const searchResultMockData: SearchResult[] = [
{
id: '1',
score: 1,
fields: [
{ name: '__Icon', values: ['icon-document'] },
{ name: '__IndexType', values: ['content'] },
{ name: '__Key', values: ['903wyrqwjf-33wrefef-wefwef3-erw'] },
{ name: '__NodeId', values: ['1059'] },
{ name: '__NodeTypeAlias', values: ['Home'] },
{ name: '__Path', values: ['-1.345'] },
{ name: '__Published', values: ['y'] },
{ name: '__VariesByCulture', values: ['n'] },
{ name: 'createDate', values: ['30752539'] },
{ name: 'creatorId', values: ['-1'] },
{ name: 'creatorName', values: ['Lone'] },
{ name: 'icon', values: ['icon-document'] },
{ name: 'id', values: ['1059'] },
{ name: 'image', values: ['34343-3wdsw-sd35-3s', 'afewr-q5-23rd-3red'] },
{ name: 'level', values: ['1'] },
{ name: 'nodeName', values: ['Just a picture'] },
{ name: 'nodeType', values: ['1056'] },
{ name: 'parentID', values: ['-1'] },
{ name: 'path', values: ['-3.325'] },
{ name: 'sortOrder', values: ['1'] },
{ name: 'templateID', values: ['1055'] },
{ name: 'updateDate', values: ['9573024532945'] },
{ name: 'urlName', values: ['just-a-picture'] },
{ name: 'writerID', values: ['-1'] },
{ name: 'writerName', values: ['Lone'] },
],
},
{
id: '2',
score: 0.9,
fields: [
{ name: '__Icon', values: ['icon-document'] },
{ name: '__IndexType', values: ['content'] },
{ name: '__Key', values: ['903wyrqwjf-33wrefef-wefwef3-erw'] },
{ name: '__NodeId', values: ['1059'] },
{ name: '__NodeTypeAlias', values: ['Home'] },
{ name: '__Path', values: ['-1.345'] },
{ name: '__Published', values: ['y'] },
{ name: '__VariesByCulture', values: ['n'] },
{ name: 'createDate', values: ['30752539'] },
{ name: 'creatorId', values: ['-1'] },
{ name: 'creatorName', values: ['Lone'] },
{ name: 'icon', values: ['icon-document'] },
{ name: 'id', values: ['1059'] },
{ name: 'image', values: ['34343-3wdsw-sd35-3s', 'afewr-q5-23rd-3red'] },
{ name: 'level', values: ['1'] },
{ name: 'nodeName', values: ['Just a picture'] },
{ name: 'nodeType', values: ['1056'] },
{ name: 'parentID', values: ['-1'] },
{ name: 'path', values: ['-3.325'] },
{ name: 'sortOrder', values: ['1'] },
{ name: 'templateID', values: ['1055'] },
{ name: 'updateDate', values: ['9573024532945'] },
{ name: 'urlName', values: ['just-a-picture'] },
{ name: 'writerID', values: ['-1'] },
{ name: 'writerName', values: ['Lone'] },
],
},
];

View File

@@ -0,0 +1,73 @@
import { rest } from 'msw';
import { searchResultMockData, getIndexByName, PagedIndexers } from '../data/examine.data';
import { umbracoPath } from '@umbraco-cms/utils';
import { Index, PagedIndex, PagedSearcher, PagedSearchResult } from '@umbraco-cms/backend-api';
export const handlers = [
rest.get(umbracoPath('/search/index'), (_req, res, ctx) => {
return res(
// Respond with a 200 status code
ctx.status(200),
ctx.json<PagedIndex>(PagedIndexers)
);
}),
rest.get(umbracoPath('/search/index/:indexName'), (_req, res, ctx) => {
const indexName = _req.params.indexName as string;
if (!indexName) return;
const indexFound = getIndexByName(indexName);
if (indexFound) {
return res(ctx.status(200), ctx.json<Index>(indexFound));
} else {
return res(ctx.status(404));
}
}),
rest.post(umbracoPath('/search/index/:indexName/rebuild'), async (_req, res, ctx) => {
await new Promise((resolve) => setTimeout(resolve, (Math.random() + 1) * 1000)); // simulate a delay of 1-2 seconds
const indexName = _req.params.indexName as string;
if (!indexName) return;
const indexFound = getIndexByName(indexName);
if (indexFound) {
return res(ctx.status(201));
} else {
return res(ctx.status(404));
}
}),
rest.get(umbracoPath('/search/searcher'), (_req, res, ctx) => {
return res(
ctx.status(200),
ctx.json<PagedSearcher>({
total: 0,
items: [{ name: 'ExternalSearcher' }, { name: 'InternalSearcher' }, { name: 'InternalMemberSearcher' }],
})
);
}),
rest.get(umbracoPath('/search/searcher/:searcherName/search'), (_req, res, ctx) => {
const query = _req.url.searchParams.get('query');
const take = _req.url.searchParams.get('take');
const searcherName = _req.params.searcherName as string;
if (!searcherName || !query) return;
if (searcherName) {
return res(
ctx.status(200),
ctx.json<PagedSearchResult>({
total: 0,
items: searchResultMockData,
})
);
} else {
return res(ctx.status(404));
}
}),
];

View File

@@ -10,6 +10,8 @@ import { handlers as userHandlers } from './domains/user.handlers';
import { handlers as telemetryHandlers } from './domains/telemetry.handlers';
import { handlers as treeHandlers } from './domains/entity.handlers';
import { handlers as examineManagementHandlers } from './domains/examine-management.handlers';
export const handlers = [
serverHandlers.serverRunningHandler,
serverHandlers.serverVersionHandler,
@@ -24,4 +26,5 @@ export const handlers = [
...telemetryHandlers,
...publishedStatusHandlers,
...treeHandlers,
...examineManagementHandlers,
];

View File

@@ -0,0 +1,7 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export * from './modal';
export { UmbNotificationService } from './notification';
export type { UmbNotificationDefaultData } from './notification/layouts/default';

View File

@@ -1,2 +1,3 @@
export * from './modal.service';
export * from './modal-handler';
export * from './layouts/modal-layout.element';

View File

@@ -31,7 +31,8 @@
"@umbraco-cms/extensions-registry": ["src/core/extensions-registry"],
"@umbraco-cms/observable-api": ["src/core/observable-api"],
"@umbraco-cms/utils": ["src/core/utils"],
"@umbraco-cms/test-utils": ["src/core/test-utils"]
"@umbraco-cms/test-utils": ["src/core/test-utils"],
"@umbraco-cms/services": ["src/core/services"]
},
},
"include": [