Skip to content

Commit

Permalink
Merge branch 'main' into 30215-select-existing-file-show-hostfolder-o…
Browse files Browse the repository at this point in the history
…n-the-left
  • Loading branch information
nicobytes authored Nov 5, 2024
2 parents 4748500 + 7a2d6bc commit 49da66b
Show file tree
Hide file tree
Showing 28 changed files with 636 additions and 243 deletions.
9 changes: 5 additions & 4 deletions core-web/apps/dotcms-ui/src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import { DotLogOutContainerComponent } from '@components/login/dot-logout-contai
import { DotLoginPageComponent } from '@components/login/main/dot-login-page.component';
import { MainCoreLegacyComponent } from '@components/main-core-legacy/main-core-legacy-component';
import { MainComponentLegacyComponent } from '@components/main-legacy/main-legacy.component';
import { EmaAppConfigurationService } from '@dotcms/data-access';
import { DotEnterpriseLicenseResolver } from '@dotcms/ui';
import { DotExperimentsService, EmaAppConfigurationService } from '@dotcms/data-access';
import { dotAnalyticsHealthCheckResolver, DotEnterpriseLicenseResolver } from '@dotcms/ui';
import { DotCustomReuseStrategyService } from '@shared/dot-custom-reuse-strategy/dot-custom-reuse-strategy.service';

import { AuthGuardService } from './api/services/guards/auth-guard.service';
Expand Down Expand Up @@ -75,9 +75,10 @@ const PORTLETS_ANGULAR: Route[] = [
path: 'analytics-search',
canActivate: [MenuGuardService],
canActivateChild: [MenuGuardService],
providers: [DotEnterpriseLicenseResolver],
providers: [DotEnterpriseLicenseResolver, DotExperimentsService],
resolve: {
isEnterprise: DotEnterpriseLicenseResolver
isEnterprise: DotEnterpriseLicenseResolver,
healthCheck: dotAnalyticsHealthCheckResolver
},
data: {
reuseRoute: false
Expand Down
Original file line number Diff line number Diff line change
@@ -1,44 +1,48 @@
<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)">
@if (store.wallEmptyConfig()) {
<dot-empty-container [configuration]="store.wallEmptyConfig()" [hideContactUsLink]="true" />
} @else {
<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
(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">
icon="pi pi-question-circle"></button>
</div>
<ngx-monaco-editor
[ngModel]="$results()"
[options]="ANALYTICS__RESULTS_MONACO_EDITOR_OPTIONS"
data-testId="results-editor"></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>
</p-splitter>
</ng-template>
<ng-template pTemplate>
@if ($results() === null) {
<dot-empty-container
[configuration]="store.emptyResultsConfig()"
[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,7 +6,16 @@ import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';
import { ActivatedRoute } from '@angular/router';

import { DotAnalyticsSearchService, DotHttpErrorManagerService } from '@dotcms/data-access';
import { Splitter } from 'primeng/splitter';

import {
DotAnalyticsSearchService,
DotHttpErrorManagerService,
DotMessageService
} from '@dotcms/data-access';
import { HealthStatusTypes } from '@dotcms/dotcms-models';
import { DotEmptyContainerComponent } from '@dotcms/ui';
import { MockDotMessageService } from '@dotcms/utils-testing';

import { DotAnalyticsSearchComponent } from './dot-analytics-search.component';

Expand All @@ -25,53 +34,111 @@ describe('DotAnalyticsSearchComponent', () => {
providers: [
provideHttpClient(),
provideHttpClientTesting(),

mockProvider(DotHttpErrorManagerService),
{
provide: ActivatedRoute,
useValue: {
snapshot: {
data: {
isEnterprise: true
}
}
}
},
mockProvider(DotHttpErrorManagerService)
provide: DotMessageService,
useValue: new MockDotMessageService({
'analytics.search.no.configured': 'No configuration found',
'analytics.search.no.configured.subtitle':
'Please configure the analytics search',
'analytics.search.config.error': 'Configuration error',
'analytics.search.config.error.subtitle':
'There was an error in the configuration',
'analytics.search.no.licence': 'No license found',
'analytics.search.no.license.subtitle': 'Please provide a valid license',
'analytics.search.no.results': 'No results',
'analytics.search.execute.results': 'Execute a query to get results'
})
}
]
});

beforeEach(() => {
spectator = createComponent();
store = spectator.inject(DotAnalyticsSearchStore, true);
});

it('should initialize store with enterprise state on init', () => {
const initLoadSpy = jest.spyOn(store, 'initLoad');
spectator.component.ngOnInit();

expect(initLoadSpy).toHaveBeenCalledWith(true);
});

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'] });
describe('when healthCheck is "OK"', () => {
beforeEach(() => {
spectator = createComponent({
providers: [
{
provide: ActivatedRoute,
useValue: {
snapshot: {
data: {
isEnterprise: true,
healthCheck: HealthStatusTypes.OK
}
}
}
}
]
});
store = spectator.inject(DotAnalyticsSearchStore, true);
});

it('should render dot-empty-container with the correct configuration', () => {
spectator.detectChanges();
const dotEmptyContainer = spectator.query(DotEmptyContainerComponent);
expect(dotEmptyContainer).toExist();
expect(dotEmptyContainer.configuration).toEqual({
subtitle: 'Execute a query to get results',
icon: 'pi-search',
title: 'No results'
});
});

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', () => {
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(button).toBeDisabled();
});

it('should render the Splitter when healthCheck is "OK"', () => {
spectator.detectChanges();
expect(spectator.query(Splitter)).toExist();
});
});

it('should not call getResults with invalid JSON', () => {
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(button).toBeDisabled();
describe('when healthCheck is "NOT_CONFIGURED"', () => {
beforeEach(() => {
spectator = createComponent({
providers: [
{
provide: ActivatedRoute,
useValue: {
snapshot: {
data: {
isEnterprise: true,
healthCheck: HealthStatusTypes.NOT_CONFIGURED
}
}
}
}
]
});
store = spectator.inject(DotAnalyticsSearchStore, true);
});

it('should render dot-empty-container', () => {
spectator.detectChanges();
const dotEmptyContainer = spectator.query(DotEmptyContainerComponent);
expect(dotEmptyContainer).toExist();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,21 @@ import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor';
import { CommonModule } from '@angular/common';
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, DotMessageService } from '@dotcms/data-access';
import { DotEmptyContainerComponent, DotMessagePipe, PrincipalConfiguration } from '@dotcms/ui';
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 { DotAnalyticsSearchStore } from '../store/dot-analytics-search.store';
} from '../utils';

@Component({
selector: 'lib-dot-analytics-search',
Expand All @@ -41,31 +39,16 @@ import { DotAnalyticsSearchStore } from '../store/dot-analytics-search.store';
styleUrl: './dot-analytics-search.component.scss'
})
export class DotAnalyticsSearchComponent {
private readonly route = inject(ActivatedRoute);
ANALYTICS_MONACO_EDITOR_OPTIONS = ANALYTICS_MONACO_EDITOR_OPTIONS;
ANALYTICS__RESULTS_MONACO_EDITOR_OPTIONS = ANALYTICS_RESULTS_MONACO_EDITOR_OPTIONS;

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.
*/
Expand All @@ -80,12 +63,6 @@ export class DotAnalyticsSearchComponent {
return results ? JSON.stringify(results, null, 2) : null;
});

ngOnInit() {
const { isEnterprise } = this.route.snapshot.data;

this.store.initLoad(isEnterprise);
}

/**
* Handles the request to get results based on the query editor content.
* Validates the JSON and calls the store's getResults method if valid.
Expand Down
Loading

0 comments on commit 49da66b

Please sign in to comment.