Skip to content

Commit

Permalink
implementation (Content Analytics Search): #30427 Apply updated Design (
Browse files Browse the repository at this point in the history
#30492)

### Proposed Changes
* Apply new design updates, using the `p-splitter`
* Json format validation before be able to make the request




### Additional Info
** any additional useful context or info **

### Screenshots

<img width="1323" alt="image"
src="https://github.com/user-attachments/assets/daaed8ce-7285-4aa5-88ad-894a2a520df3">

<img width="1301" alt="image"
src="https://github.com/user-attachments/assets/c62ce545-4ddb-417e-9643-e4e1434db905">

<img width="1359" alt="image"
src="https://github.com/user-attachments/assets/edb41833-aebf-471f-aba9-cea22c9c7d0e">
  • Loading branch information
hmoreras authored Oct 29, 2024
1 parent 2e2a9c2 commit ab715b9
Show file tree
Hide file tree
Showing 9 changed files with 177 additions and 57 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
@use "variables" as *;

.p-splitter {
border: 1px solid $color-palette-gray-400;
background: $white;
border-radius: $border-radius-sm;

.p-splitter-gutter,
.p-splitter-gutter-resizing {
background: $color-palette-gray-100;

.p-splitter-gutter-handle {
background: $color-palette-gray-300;

&:focus-visible {
outline: none;
outline-offset: 0;
box-shadow: $shadow-xs;
}
}
}
}
1 change: 1 addition & 0 deletions core-web/libs/dotcms-scss/angular/dotcms-theme/theme.scss
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
@use "components/tooltip";
@use "components/tree";
@use "components/table";
@use "components/splitter";

@use "utils/validation";
@use "utils/password";
Original file line number Diff line number Diff line change
@@ -1,27 +1,44 @@
<section class="content-analytics__query">
<div class="content-analytics__header">
<h4>{{ 'analytics.search.query' | dm }}</h4>
<div class="content-analytics__actions">
<button
pButton
(click)="handleRequest()"
[label]="'analytics.search.run.query' | dm"
icon="pi pi-arrow-right"
iconPos="right"
data-testId="run-query"></button>
</div>
</div>
<ngx-monaco-editor
[(ngModel)]="queryEditor"
[options]="ANALYTICS_MONACO_EDITOR_OPTIONS"
data-testId="query-editor"></ngx-monaco-editor>
</section>
<section class="content-analytics__results">
<div class="content-analytics__header">
<h4>{{ 'analytics.search.results' | dm }}</h4>
</div>
<ngx-monaco-editor
[ngModel]="results$()"
[options]="ANALYTICS__RESULTS_MONACO_EDITOR_OPTIONS"
data-testId="results-editor"></ngx-monaco-editor>
</section>
<p-splitter [panelSizes]="[35, 65]" styleClass="mb-5">
<ng-template pTemplate>
<section>
<div class="content-analytics__query-header">
<h4>{{ 'analytics.search.query' | dm }}</h4>
<button
class="p-button-rounded p-button-link p-button-sm"
pButton
icon="pi pi-question-circle"></button>
</div>
<ngx-monaco-editor
[(ngModel)]="queryEditor"
[options]="ANALYTICS_MONACO_EDITOR_OPTIONS"
(ngModelChange)="handleQueryChange($event)"
data-testId="query-editor"></ngx-monaco-editor>
<div class="content-analytics__actions">
<span
tooltipPosition="top"
[pTooltip]="$isValidJson() ? '' : ('analytics.search.valid.json' | dm)">
<button
pButton
(click)="handleRequest()"
[disabled]="!$isValidJson()"
[label]="'analytics.search.execute.query' | dm"
data-testId="run-query"></button>
</span>
</div>
</section>
</ng-template>
<ng-template pTemplate>
@if ($results() === null) {
<dot-empty-container
[configuration]="$emptyState()"
[hideContactUsLink]="true"></dot-empty-container>
} @else {
<section class="content-analytics__results">
<ngx-monaco-editor
[ngModel]="$results()"
[options]="ANALYTICS__RESULTS_MONACO_EDITOR_OPTIONS"
data-testId="results-editor"></ngx-monaco-editor>
</section>
}
</ng-template>
</p-splitter>
Original file line number Diff line number Diff line change
Expand Up @@ -6,40 +6,56 @@
overflow: auto;
background: $white;
display: flex;
flex-direction: row;
flex-direction: column;
padding: $spacing-3 $spacing-4;
gap: $spacing-3;

section {
flex: 1;
display: flex;
flex-direction: column;
padding: $spacing-3;
gap: $spacing-3;
}

dot-empty-container {
flex-grow: 1;
}

p-splitter {
height: 100%;

::ng-deep .p-splitter {
height: 100%;
}
}

ngx-monaco-editor {
height: 300px;
width: 100%;
flex-grow: 1;
border: $field-border-size solid $color-palette-gray-400;
border-radius: $border-radius-md;
display: flex;
flex-grow: 1;
}

.content-analytics__header {
.content-analytics__query-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: $spacing-3;

h4 {
font-size: $font-size-lmd;
margin: $spacing-3 0;
font-size: $font-size-md;
margin: 0;
}

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

.content-analytics__actions {
display: flex;
gap: $spacing-3;
}

section {
flex: 1;
display: flex;
flex-direction: column;
justify-content: flex-end;
}

.content-analytics__results {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,21 +53,25 @@ 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"]}');
spectator.detectChanges();

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

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

it('should not call getResults with invalid JSON', () => {
const getResultsSpy = jest.spyOn(store, 'getResults');
spectator.component.queryEditor = 'invalid json';
spectator.component.handleQueryChange('invalid json');
spectator.detectChanges();

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

expect(getResultsSpy).not.toHaveBeenCalled();
expect(button).toBeDisabled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ import { JsonObject } from '@angular-devkit/core';
import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor';

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

import { ButtonDirective } from 'primeng/button';
import { DropdownModule } from 'primeng/dropdown';
import { SplitterModule } from 'primeng/splitter';
import { TooltipModule } from 'primeng/tooltip';

import { DotAnalyticsSearchService } from '@dotcms/data-access';
import { DotMessagePipe } from '@dotcms/ui';
import { DotAnalyticsSearchService, DotMessageService } from '@dotcms/data-access';
import { DotEmptyContainerComponent, DotMessagePipe, PrincipalConfiguration } from '@dotcms/ui';

import {
ANALYTICS_MONACO_EDITOR_OPTIONS,
Expand All @@ -22,7 +25,17 @@ import { DotAnalyticsSearchStore } from '../store/dot-analytics-search.store';
@Component({
selector: 'lib-dot-analytics-search',
standalone: true,
imports: [CommonModule, DotMessagePipe, ButtonDirective, MonacoEditorModule, FormsModule],
imports: [
CommonModule,
DotMessagePipe,
ButtonDirective,
MonacoEditorModule,
FormsModule,
SplitterModule,
DropdownModule,
DotEmptyContainerComponent,
TooltipModule
],
providers: [DotAnalyticsSearchStore, DotAnalyticsSearchService],
templateUrl: './dot-analytics-search.component.html',
styleUrl: './dot-analytics-search.component.scss'
Expand All @@ -34,12 +47,34 @@ export class DotAnalyticsSearchComponent {

readonly store = inject(DotAnalyticsSearchStore);

/**
* Represents the DotMessageService instance.
*/
readonly #dotMessageService = inject(DotMessageService);

/**
* The content of the query editor.
*/
queryEditor = '';

/**
* Signal representing the empty state configuration.
*/
$emptyState = signal<PrincipalConfiguration>({
title: this.#dotMessageService.get('analytics.search.no.results'),
icon: 'pi-search',
subtitle: this.#dotMessageService.get('analytics.search.execute.results')
});

/**
* 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(() => {
$results = computed(() => {
const results = this.store.results();

return results ? JSON.stringify(results, null, 2) : null;
Expand All @@ -59,8 +94,16 @@ export class DotAnalyticsSearchComponent {
const value = isValidJson(this.queryEditor);
if (value) {
this.store.getResults(value as JsonObject);
} else {
//TODO: handle query error.
}
}

/**
* Handles changes to the query editor content.
* Updates the $isValidJson signal based on the validity of the JSON.
*
* @param value - The new content of the query editor.
*/
handleQueryChange(value: string) {
this.$isValidJson.set(!!isValidJson(value));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ describe('DotAnalyticsSearchStore', () => {
it('should initialize with default state', () => {
expect(store.isEnterprise()).toEqual(false);
expect(store.results()).toEqual(null);
expect(store.query()).toEqual({ value: null, type: AnalyticsQueryType.DEFAULT });
expect(store.query()).toEqual({ value: null, type: AnalyticsQueryType.CUBE });
expect(store.state()).toEqual(ComponentStatus.INIT);
expect(store.errorMessage()).toEqual('');
});
Expand All @@ -74,7 +74,7 @@ describe('DotAnalyticsSearchStore', () => {

expect(dotAnalyticsSearchService.get).toHaveBeenCalledWith(
{ query: 'test' },
AnalyticsQueryType.DEFAULT
AnalyticsQueryType.CUBE
);

expect(store.results()).toEqual(mockResponse);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import { switchMap, tap } from 'rxjs/operators';
import { DotAnalyticsSearchService, DotHttpErrorManagerService } from '@dotcms/data-access';
import { AnalyticsQueryType, ComponentStatus } from '@dotcms/dotcms-models';

/**
* Type definition for the state of the DotContentAnalytics.
*/
export type DotContentAnalyticsState = {
isEnterprise: boolean;
results: JsonObject[] | null;
Expand All @@ -23,17 +26,23 @@ export type DotContentAnalyticsState = {
errorMessage: string;
};

/**
* Initial state for the DotContentAnalytics.
*/
export const initialState: DotContentAnalyticsState = {
isEnterprise: false,
results: null,
query: {
value: null,
type: AnalyticsQueryType.DEFAULT
type: AnalyticsQueryType.CUBE
},
state: ComponentStatus.INIT,
errorMessage: ''
};

/**
* Store for managing the state and actions related to DotAnalyticsSearch.
*/
export const DotAnalyticsSearchStore = signalStore(
withState(initialState),
withMethods(
Expand All @@ -43,15 +52,20 @@ export const DotAnalyticsSearchStore = signalStore(
dotHttpErrorManagerService = inject(DotHttpErrorManagerService)
) => ({
/**
* Set if initial state, including, the user is enterprise or not
* @param isEnterprise
* Initializes the state with the given enterprise status.
* @param isEnterprise - Boolean indicating if the user is an enterprise user.
*/
initLoad: (isEnterprise: boolean) => {
patchState(store, {
...initialState,
isEnterprise
});
},

/**
* Fetches the results based on the current query.
* @param query - The query to fetch results for.
*/
getResults: rxMethod<JsonObject>(
pipe(
tap(() => {
Expand Down
5 changes: 4 additions & 1 deletion dotCMS/src/main/webapp/WEB-INF/messages/Language.properties
Original file line number Diff line number Diff line change
Expand Up @@ -5812,8 +5812,11 @@ lts.expired.message = This version of dotCMS is no longer supported. Please cont
lts.expires.soon.message = Your dotCMS version will no longer be supported in {0} days. Please contact your customer success manager to schedule an upgrade.


analytics.search.run.query=Run Query
analytics.search.execute.query=Execute Query
analytics.search.query=Query
analytics.search.results=Results
analytics.search.no.results=No Results
analytics.search.execute.results=Execute a query to get results
analytics.search.valid.json=The query must be a valid JSON


0 comments on commit ab715b9

Please sign in to comment.