Skip to content

Commit

Permalink
implementation ( Content Analytics) : #30622 CA Search: Add Help butt…
Browse files Browse the repository at this point in the history
…on (#30873)

### Proposed Changes
* Add a help button for the CA screen.
 


https://github.com/user-attachments/assets/88684404-ca75-4bf6-a3c3-7f9b56ce1b3b
  • Loading branch information
hmoreras authored Dec 6, 2024
1 parent 632b9af commit 8b77adb
Show file tree
Hide file tree
Showing 8 changed files with 220 additions and 69 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,40 +9,67 @@ <h4>{{ 'analytics.search.query' | dm }}</h4>
<button
class="p-button-rounded p-button-link p-button-sm"
pButton
(click)="$showDialog.set(true)"
data-testid="help-button"
icon="pi pi-question-circle"></button>
</div>
<ngx-monaco-editor
[(ngModel)]="queryEditor"
[ngModel]="store.query().value"
[options]="ANALYTICS_MONACO_EDITOR_OPTIONS"
(ngModelChange)="handleQueryChange($event)"
(ngModelChange)="store.setQuery($event)"
data-testId="query-editor"></ngx-monaco-editor>
<div class="content-analytics__actions">
<span
tooltipPosition="top"
[pTooltip]="$isValidJson() ? '' : ('analytics.search.valid.json' | dm)">
[pTooltip]="
store.query().isValidJson ? '' : ('analytics.search.valid.json' | dm)
">
<button
pButton
(click)="handleRequest()"
[disabled]="!$isValidJson()"
(click)="store.getResults()"
[disabled]="!store.query().isValidJson"
[label]="'analytics.search.execute.query' | dm"
data-testId="run-query"></button>
</span>
</div>
</section>
</ng-template>
<ng-template pTemplate>
@if ($results() === null) {
@if (!store.results()) {
<dot-empty-container
[configuration]="store.emptyResultsConfig()"
[hideContactUsLink]="true"></dot-empty-container>
} @else {
<section class="content-analytics__results">
<ngx-monaco-editor
[ngModel]="$results()"
[ngModel]="store.results()"
[options]="ANALYTICS__RESULTS_MONACO_EDITOR_OPTIONS"
data-testId="results-editor"></ngx-monaco-editor>
</section>
}
</ng-template>
</p-splitter>

@defer (when $showDialog()) {
<p-dialog
[(visible)]="$showDialog"
[header]="'analytics.search.help' | dm"
[modal]="true"
[style]="{ width: '45rem' }">
@for (example of store.queryExamples(); track $index) {
<div class="mb-4" data-testid="query-example-container">
<p class="mb-2">{{ example.title | dm }}</p>
<pre>
<code>{{ example.query }}</code>
<button pButton
class="p-button-outlined p-button-sm content-analytics__insert-btn"
data-testid="query-example-button"
[label]="(!!store.query().value ? 'analytics.search.replace' : 'analytics.search.insert' ) | dm "
(click)="addExampleQuery(example.query); ">
</button>
</pre>
</div>
}
</p-dialog>
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@

button {
color: $color-palette-gray-500;
visibility: hidden;
}
}

Expand All @@ -67,4 +66,17 @@
border-radius: $border-radius-md;
}
}

pre {
font-size: $font-size-sm;
background-color: $color-palette-gray-100;
border-radius: $border-radius-md;
position: relative;
}

.content-analytics__insert-btn {
position: absolute;
bottom: $spacing-2;
right: $spacing-2;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import { MockModule } from 'ng-mocks';

import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';
import { fakeAsync, tick } from '@angular/core/testing';
import { ActivatedRoute } from '@angular/router';

import { ButtonModule } from 'primeng/button';
import { Dialog, DialogModule } from 'primeng/dialog';
import { Splitter } from 'primeng/splitter';

import {
Expand All @@ -27,7 +30,7 @@ describe('DotAnalyticsSearchComponent', () => {

const createComponent = createComponentFactory({
component: DotAnalyticsSearchComponent,
imports: [MockModule(MonacoEditorModule)],
imports: [MockModule(MonacoEditorModule), ButtonModule, DialogModule],
componentProviders: [DotAnalyticsSearchStore, DotAnalyticsSearchService],
declarations: [],
mocks: [],
Expand Down Expand Up @@ -88,19 +91,19 @@ describe('DotAnalyticsSearchComponent', () => {
it('should call getResults with valid JSON', () => {
const getResultsSpy = jest.spyOn(store, 'getResults');

spectator.component.queryEditor = '{"measures": ["request.count"]}';
spectator.component.handleQueryChange('{"measures": ["request.count"]}');
store.setQuery('{"measures": ["request.count"]}');

spectator.detectChanges();

const button = spectator.query(byTestId('run-query')) as HTMLButtonElement;
spectator.click(button);

expect(getResultsSpy).toHaveBeenCalledWith({ measures: ['request.count'] });
expect(getResultsSpy).toHaveBeenCalled();
});

it('should not call getResults with invalid JSON', () => {
spectator.component.queryEditor = 'invalid json';
spectator.component.handleQueryChange('invalid json');
store.setQuery('invalid json');

spectator.detectChanges();

const button = spectator.query(byTestId('run-query')) as HTMLButtonElement;
Expand All @@ -113,6 +116,48 @@ describe('DotAnalyticsSearchComponent', () => {
spectator.detectChanges();
expect(spectator.query(Splitter)).toExist();
});

describe('when the help dialog is displayed', () => {
it('should display the help dialog when the help button is clicked', fakeAsync(() => {
const helpButton = spectator.query(byTestId('help-button')) as HTMLButtonElement;
spectator.click(helpButton);

tick();
spectator.detectChanges();

const dialog = spectator.query(Dialog);

expect(dialog).toExist();
expect(dialog).toBeVisible();
}));

it('should display the correct number of query examples in the dialog', fakeAsync(() => {
const helpButton = spectator.query(byTestId('help-button')) as HTMLButtonElement;
spectator.click(helpButton);

tick();
spectator.detectChanges();

const queryExamples = store.queryExamples();
const exampleElements = spectator.queryAll(byTestId('query-example-container'));
expect(exampleElements.length).toEqual(queryExamples.length);
}));

it('should call addExampleQuery when a query example button is clicked', fakeAsync(() => {
const setQuerySpy = jest.spyOn(store, 'setQuery');
const queryExamples = store.queryExamples();
const helpButton = spectator.query(byTestId('help-button')) as HTMLButtonElement;
spectator.click(helpButton);

tick();
spectator.detectChanges();

spectator.click(byTestId('query-example-button'));

expect(spectator.component.$showDialog()).toBeFalsy();
expect(setQuerySpy).toHaveBeenCalledWith(queryExamples[0].query);
}));
});
});

describe('when healthCheck is "NOT_CONFIGURED"', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { JsonObject } from '@angular-devkit/core';
import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor';

import { CommonModule } from '@angular/common';
import { Component, computed, inject, signal } from '@angular/core';
import { Component, inject, model } from '@angular/core';
import { FormsModule } from '@angular/forms';

import { ButtonDirective } from 'primeng/button';
import { DialogModule } from 'primeng/dialog';
import { DropdownModule } from 'primeng/dropdown';
import { SplitterModule } from 'primeng/splitter';
import { TooltipModule } from 'primeng/tooltip';
Expand All @@ -14,11 +14,7 @@ import { DotAnalyticsSearchService } from '@dotcms/data-access';
import { DotEmptyContainerComponent, DotMessagePipe } from '@dotcms/ui';

import { DotAnalyticsSearchStore } from '../store/dot-analytics-search.store';
import {
ANALYTICS_MONACO_EDITOR_OPTIONS,
ANALYTICS_RESULTS_MONACO_EDITOR_OPTIONS,
isValidJson
} from '../utils';
import { ANALYTICS_MONACO_EDITOR_OPTIONS, ANALYTICS_RESULTS_MONACO_EDITOR_OPTIONS } from '../utils';

@Component({
selector: 'lib-dot-analytics-search',
Expand All @@ -32,7 +28,8 @@ import {
SplitterModule,
DropdownModule,
DotEmptyContainerComponent,
TooltipModule
TooltipModule,
DialogModule
],
providers: [DotAnalyticsSearchStore, DotAnalyticsSearchService],
templateUrl: './dot-analytics-search.component.html',
Expand All @@ -45,42 +42,17 @@ export class DotAnalyticsSearchComponent {
readonly store = inject(DotAnalyticsSearchStore);

/**
* The content of the query editor.
* Boolean model to control the visibility of the dialog.
*/
queryEditor = '';
$showDialog = model<boolean>(false);

/**
* Signal representing whether the query editor content is valid JSON.
*/
$isValidJson = signal<boolean>(false);

/**
* Computed property to get the results from the store and format them as a JSON string.
*/
$results = computed(() => {
const results = this.store.results();

return results ? JSON.stringify(results, null, 2) : null;
});

/**
* Handles the request to get results based on the query editor content.
* Validates the JSON and calls the store's getResults method if valid.
*/
handleRequest() {
const value = isValidJson(this.queryEditor);
if (value) {
this.store.getResults(value as JsonObject);
}
}

/**
* Handles changes to the query editor content.
* Updates the $isValidJson signal based on the validity of the JSON.
* Adds an example query to the store and hides the dialog.
*
* @param value - The new content of the query editor.
* @param query - The example query to be added.
*/
handleQueryChange(value: string) {
this.$isValidJson.set(!!isValidJson(value));
addExampleQuery(query: string): void {
this.store.setQuery(query);
this.$showDialog.set(false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import { MockDotMessageService } from '@dotcms/utils-testing';

import { DotAnalyticsSearchStore } from './dot-analytics-search.store';

import { AnalyticsQueryExamples } from '../utils';

const mockResponse = [
{
'request.count': '5',
Expand Down Expand Up @@ -86,8 +88,12 @@ describe('DotAnalyticsSearchStore', () => {

it('should initialize with default state', () => {
expect(store.isEnterprise()).toEqual(true);
expect(store.results()).toEqual(null);
expect(store.query()).toEqual({ value: null, type: AnalyticsQueryType.CUBE });
expect(store.results()).toEqual('');
expect(store.query()).toEqual({
value: '',
type: AnalyticsQueryType.CUBE,
isValidJson: false
});
expect(store.state()).toEqual(ComponentStatus.INIT);
expect(store.healthCheck()).toEqual(HealthStatusTypes.OK);
expect(store.wallEmptyConfig()).toEqual(null);
Expand All @@ -96,6 +102,7 @@ describe('DotAnalyticsSearchStore', () => {
subtitle: 'Execute a query to get results',
title: 'No results'
});
expect(store.queryExamples()).toEqual(AnalyticsQueryExamples);
});
});

Expand All @@ -121,25 +128,43 @@ describe('DotAnalyticsSearchStore', () => {
dotHttpErrorManagerService = spectator.inject(DotHttpErrorManagerService);
});

it('should update the query state with a valid JSON query', () => {
const validQuery = '{"measures": ["request.count"]}';
store.setQuery(validQuery);

expect(store.query().value).toBe(validQuery);
expect(store.query().isValidJson).toBe(true);
});

it('should update the query state with an invalid JSON query', () => {
const invalidQuery = 'invalid json';
store.setQuery(invalidQuery);

expect(store.query().value).toBe(invalidQuery);
expect(store.query().isValidJson).toBe(false);
});

it('should perform a POST request to the base URL and return results', () => {
store.setQuery('{"measures": ["request.count"]}');

dotAnalyticsSearchService.get.mockReturnValue(of(mockResponse));

store.getResults({ query: 'test' });
store.getResults();

expect(dotAnalyticsSearchService.get).toHaveBeenCalledWith(
{ query: 'test' },
{ measures: ['request.count'] },
AnalyticsQueryType.CUBE
);

expect(store.results()).toEqual(mockResponse);
expect(store.results()).toEqual(JSON.stringify(mockResponse, null, 2));
});

it('should handle error while getting results', () => {
const mockError = new HttpErrorResponse({ status: 404, statusText: 'Not Found' });

dotAnalyticsSearchService.get.mockReturnValue(throwError(() => mockError));

store.getResults({ query: 'test' });
store.getResults();

expect(dotHttpErrorManagerService.handle).toHaveBeenCalled();
});
Expand Down
Loading

0 comments on commit 8b77adb

Please sign in to comment.