diff --git a/components/automate-ui/e2e/deprecated-routes.e2e-spec.ts b/components/automate-ui/e2e/deprecated-routes.e2e-spec.ts index 6556d12af15..d03211e022e 100644 --- a/components/automate-ui/e2e/deprecated-routes.e2e-spec.ts +++ b/components/automate-ui/e2e/deprecated-routes.e2e-spec.ts @@ -8,7 +8,8 @@ describe('Deprecated routes redirect to the correct new route for backwards comp // Note: This is "old path" -> "new path", and the new path will be checked // using a regular expression: the string, anchored to the right by '$'. [ - ['/admin/settings', '/settings/node-lifecycle'], + ['/settings/node-lifecycle', '/settings/data-lifecycle'], + ['/admin/settings', '/settings/data-lifecycle'], ['/admin/tokens', '/settings/tokens'], ['/admin/tokens/my-object', '/settings/tokens/my-object'], ['/admin/teams', '/settings/teams'], diff --git a/components/automate-ui/src/app/app-routing.module.ts b/components/automate-ui/src/app/app-routing.module.ts index 30d06f88640..19b283fdf49 100644 --- a/components/automate-ui/src/app/app-routing.module.ts +++ b/components/automate-ui/src/app/app-routing.module.ts @@ -73,7 +73,7 @@ const routes: Routes = [ component: SettingsLandingComponent }, { - path: 'node-lifecycle', + path: 'data-lifecycle', component: AutomateSettingsComponent }, { @@ -320,7 +320,12 @@ const routes: Routes = [ { path: 'admin/settings', pathMatch: 'full', - redirectTo: 'settings/node-lifecycle' + redirectTo: 'settings/data-lifecycle' + }, + { + path: 'settings/node-lifecycle', + pathMatch: 'full', + redirectTo: 'settings/data-lifecycle' }, { path: 'admin', diff --git a/components/automate-ui/src/app/components/settings-sidebar/settings-sidebar.component.html b/components/automate-ui/src/app/components/settings-sidebar/settings-sidebar.component.html index 58f3b971f15..5fd38642da4 100644 --- a/components/automate-ui/src/app/components/settings-sidebar/settings-sidebar.component.html +++ b/components/automate-ui/src/app/components/settings-sidebar/settings-sidebar.component.html @@ -15,7 +15,7 @@ icon="vpn_key" iconRotation="90">Node Credentials - Node Lifecycle + Data Lifecycle
Identity
diff --git a/components/automate-ui/src/app/entities/automate-settings/automate-settings.effects.ts b/components/automate-ui/src/app/entities/automate-settings/automate-settings.effects.ts index 8297ea90409..c735c71d1d6 100644 --- a/components/automate-ui/src/app/entities/automate-settings/automate-settings.effects.ts +++ b/components/automate-ui/src/app/entities/automate-settings/automate-settings.effects.ts @@ -1,5 +1,5 @@ import { map, mergeMap, catchError } from 'rxjs/operators'; -import { forkJoin, of } from 'rxjs'; +import { of } from 'rxjs'; import { Injectable } from '@angular/core'; import { Actions, Effect, ofType } from '@ngrx/effects'; @@ -32,18 +32,8 @@ export class AutomateSettingsEffects { @Effect() configureSettings$ = this.actions$.pipe( ofType(AutomateSettingsActionTypes.CONFIGURE_SETTINGS), - mergeMap((action: ConfigureSettings) => { - const jobsRequests = []; - action.payload.jobs.forEach((job) => { - jobsRequests.push(this.requests.configureIngestJob(job)); - }); - return forkJoin(jobsRequests).pipe( - map((_resp) => new ConfigureSettingsSuccess({})), - catchError((error) => of(new ConfigureSettingsFailure(error)))); - })); - - @Effect() - configureSettingsSuccess$ = this.actions$.pipe( - ofType(AutomateSettingsActionTypes.CONFIGURE_SETTINGS_SUCCESS), - map((_action) => new GetSettings({}))); + mergeMap((action: ConfigureSettings) => this.requests.configureIngestJobs(action.payload.jobs)), + map((_resp) => (new ConfigureSettingsSuccess({}))), + catchError((error) => of(new ConfigureSettingsFailure(error)) + )); } diff --git a/components/automate-ui/src/app/entities/automate-settings/automate-settings.model.ts b/components/automate-ui/src/app/entities/automate-settings/automate-settings.model.ts index cdfd9b6515b..d7bf733748b 100644 --- a/components/automate-ui/src/app/entities/automate-settings/automate-settings.model.ts +++ b/components/automate-ui/src/app/entities/automate-settings/automate-settings.model.ts @@ -1,15 +1,11 @@ +import { Validators } from '@angular/forms'; + export class JobSchedulerStatus { - running: boolean; jobs: IngestJob[]; - constructor(running: boolean, ingestJobs: IngestJob[]) { - this.running = running; + constructor(ingestJobs: IngestJob[]) { this.jobs = ingestJobs; } - - getJob(name: string): IngestJob { - return this.jobs.find((job: IngestJob) => job.name === name); - } } export interface ConfigureSettingsRequest { @@ -17,57 +13,174 @@ export interface ConfigureSettingsRequest { } export interface RespJob { - running: boolean; name: string; - every: string; + nested_name?: string; + disabled: boolean; + recurrence: string; threshold: string; - last_run: Date; - next_run: Date; - last_elapsed: Date; - started_on: Date; + purge_policies?: { + elasticsearch?: UnfurledJob[]; + }; + last_elapsed?: Date; + next_due_at?: Date; + last_enqueued_at?: Date; + last_started_at?: Date; + last_ended_at?: Date; } export interface RespJobSchedulerStatus { - running: boolean; - jobs: RespJob[]; + infra: { + jobs: RespJob[]; + }; + compliance: { + jobs: RespJob[]; + }; + event_feed: { + jobs: RespJob[]; + }; + services: { + jobs: RespJob[]; + }; } // IngestJobs is an enum that defines the list of jobs that the // JobScheduler has inside the ingest-service export enum IngestJobs { - // MissingNodes: Checks when a node hasn't check-in - // for a period of time - MissingNodes = 'missing_nodes', + // EventFeed + EventFeedRemoveData = 'eventFeedRemoveData', + EventFeedServerActions = 'eventFeedServerActions', + + // Service Groups + ServiceGroupNoHealthChecks = 'serviceGroupNoHealthChecks', + ServiceGroupRemoveServices = 'serviceGroupRemoveServices', + + // Client Runs + ClientRunsRemoveData = 'clientRunsRemoveData', + ClientRunsLabelMissing = 'clientRunsLabelMissing', + ClientRunsRemoveNodes = 'clientRunsRemoveNodes', - // MissingNodesForDeletion: Checks when a node has been missing - // for a period of time + // Compliance + ComplianceRemoveReports = 'complianceRemoveReports', + ComplianceRemoveScans = 'complianceRemoveScans' +} + +export enum JobCategories { + Infra = 'infra', + Compliance = 'compliance', + EventFeed = 'event_feed', + Services = 'services' +} + +export enum InfraJobName { + MissingNodes = 'missing_nodes', MissingNodesForDeletion = 'missing_nodes_for_deletion', + DeleteNodes = 'delete_nodes', + PeriodicPurgeTimeseries = 'periodic_purge_timeseries' +} - // DeleteNodes: Removes completely from elasticsearch nodes that - // have been marked for deletion - DeleteNodes = 'delete_nodes' +// Actions and ConvergeHistory are nested, but contained inside +// the InfraJobName of PeriodicPurgeTimeseries +export enum NestedJobName { + ComplianceReports = 'compliance-reports', + ComplianceScans = 'compliance-scans', + Feed = 'feed', + Actions = 'actions', + ConvergeHistory = 'converge-history' } export class IngestJob { - running: boolean; + category: JobCategories; name: string; + nested_name?: string; + recurrence?: string; threshold: string; - every?: string; - lastRun?: Date; - nextRun?: Date; - lastElapsed?: Date; - startedOn?: Date; + disabled: boolean; + purge_policies?: { + elasticsearch?: UnfurledJob[]; + }; + older_than_days?: number; + last_elapsed?: Date; + next_due_at?: Date; + last_enqueued_at?: Date; + last_started_at?: Date; + last_ended_at?: Date; - constructor(respJob: RespJob) { + constructor(category: JobCategories, respJob: RespJob) { if (respJob !== null) { - this.running = respJob.running; + this.category = category; this.name = respJob.name; - this.every = respJob.every; + this.nested_name = respJob.nested_name; + this.disabled = respJob.disabled; + this.recurrence = respJob.recurrence; this.threshold = respJob.threshold; - this.lastRun = new Date(respJob.last_run); - this.nextRun = new Date(respJob.next_run); - this.lastElapsed = new Date(respJob.last_elapsed); - this.startedOn = new Date(respJob.started_on); + this.purge_policies = respJob.purge_policies; + this.last_elapsed = new Date(respJob.last_elapsed); + this.next_due_at = new Date(respJob.next_due_at); + this.last_enqueued_at = new Date(respJob.last_enqueued_at); + this.last_started_at = new Date(respJob.last_started_at); + this.last_ended_at = new Date(respJob.last_ended_at); } } } + +export class UnfurledJob { + disabled: boolean; + policy_name?: string; + older_than_days?: number; + name?: string; + threshold?: string; +} + +// A JobRequestComponent is very flexible so that it may contain +// contain an older API Job object or a newer API Job Object +export class JobRequestComponent { + disabled?: boolean; + name?: string; + threshold?: string | number; + purge_policies?: { + elasticsearch?: UnfurledJob[]; + }; +} + +export interface JobRequestBody { + infra: { + job_settings: JobRequestComponent[]; + }; + compliance: { + job_settings: JobRequestComponent[]; + }; + event_feed: { + job_settings: JobRequestComponent[]; + }; + // services has not yet been implemented so we will leave as optional for now + services?: { + job_settings: JobRequestComponent[]; + }; +} + +export interface SingleDefaultForm { + category: JobCategories; + name?: string; // TODO; make stricter after services implemented + nested_name?: NestedJobName; + unit: { + value: string; + disabled: boolean; + }; + threshold: [{ + value: string; + disabled: boolean; + }, Validators]; + disabled: boolean; +} + +export interface DefaultFormData { + eventFeedRemoveData: SingleDefaultForm; + eventFeedServerActions: SingleDefaultForm; + serviceGroupNoHealthChecks: SingleDefaultForm; + serviceGroupRemoveServices: SingleDefaultForm; + clientRunsRemoveData: SingleDefaultForm; + clientRunsLabelMissing: SingleDefaultForm; + clientRunsRemoveNodes: SingleDefaultForm; + complianceRemoveReports: SingleDefaultForm; + complianceRemoveScans: SingleDefaultForm; +} diff --git a/components/automate-ui/src/app/entities/automate-settings/automate-settings.requests.ts b/components/automate-ui/src/app/entities/automate-settings/automate-settings.requests.ts index c06b1e594a7..f8398a08cd4 100644 --- a/components/automate-ui/src/app/entities/automate-settings/automate-settings.requests.ts +++ b/components/automate-ui/src/app/entities/automate-settings/automate-settings.requests.ts @@ -3,15 +3,20 @@ import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; + import { JobSchedulerStatus, RespJob, IngestJob, - IngestJobs, - RespJobSchedulerStatus + RespJobSchedulerStatus, + UnfurledJob, + JobRequestBody, + InfraJobName, + JobCategories } from './automate-settings.model'; -import { environment } from '../../../environments/environment'; +import { environment } from 'environments/environment'; + const RETENTION_URL = environment.retention_url; @Injectable() @@ -22,49 +27,123 @@ export class AutomateSettingsRequests { // fetchJobSchedulerStatus sends an HTTP GET Request to read the status of the JobScheduler // living inside the ingest-service public fetchJobSchedulerStatus(_params): Observable { - const url = `${RETENTION_URL}/nodes/status`; + + const url = `${RETENTION_URL}/status`; return this.http .get(url).pipe( map((res) => this.convertResponseToJobSchedulerStatus(res))); } - // configureIngestJob sends an HTTP POST Request to the provided ingest job to configure + // configureIngestJob sends an HTTP PUT Request to the provided ingest job to configure // it with the provided threshold and running state - configureIngestJob(job: IngestJob): Observable { - let url: string; + configureIngestJobs(jobs: IngestJob[]): Observable { + const url = `${RETENTION_URL}/config`; - switch (job.name) { - case IngestJobs.MissingNodes: { - url = `${RETENTION_URL}/nodes/missing-nodes/config`; - break; + const body: JobRequestBody = { + infra : { + job_settings: [ + { + name: 'periodic_purge_timeseries', + purge_policies: { + elasticsearch: [] + } + } + ] + }, + compliance: { + job_settings: [ + { + name: 'periodic_purge', + purge_policies: { + elasticsearch: [] + } + } + ] + }, + event_feed: { + job_settings: [ + { + name: 'periodic_purge', + purge_policies: { + elasticsearch: [] + } + } + ] } - case IngestJobs.MissingNodesForDeletion: { - url = `${RETENTION_URL}/nodes/missing-nodes-deletion/config`; - break; - } - case IngestJobs.DeleteNodes: { - url = `${RETENTION_URL}/nodes/delete-nodes/config`; - break; + }; + + jobs.forEach(job => { + let thisJob = new UnfurledJob; + + switch (job.name) { + case InfraJobName.DeleteNodes: // fallthrough + case InfraJobName.MissingNodes: // fallthrough + case InfraJobName.MissingNodesForDeletion: + thisJob = this.unfurlIngestJob(job); + // Protect against an undefined value + if (body[job.category].job_settings) { + body[job.category].job_settings.push(thisJob); + } else { + console.error(`Error: unable to save ${job.name}.`); + } + break; + + case InfraJobName.PeriodicPurgeTimeseries: // fallthrough + + case 'periodic_purge': // all other nested jobs are + // contained in 'periodic_purge' + thisJob = this.unfurlIngestJob(job, true); + + const thisObject = body[job.category].job_settings.find(item => item.name === job.name); + // Protect against an undefined value + if (thisObject) { + thisObject.purge_policies.elasticsearch.push(thisJob); + } else { + console.error(`Error: unable to save ${job.name}.`); + } + break; + + default: + break; } - default: - return; - } - const body = { - 'threshold': job.threshold, - 'running': job.running - // We can also modify how often the job runs (every X time) - // but we don't need that now! - // 'every': job.every - }; + }); + + return this.http.put(url, body); + } - return this.http.post(url, body); + private unfurlIngestJob(job: IngestJob, nested: boolean = false): UnfurledJob { + if (nested) { + return { + policy_name: job.nested_name, + older_than_days: parseInt(job.threshold, 10), + disabled: job.disabled + }; + } else { + return { + name: job.name, + threshold: job.threshold, + disabled: job.disabled + }; + } } + private convertResponseToJobSchedulerStatus( respJobSchedulerStatus: RespJobSchedulerStatus): JobSchedulerStatus { - const jobs = respJobSchedulerStatus.jobs.map((respJob: RespJob) => new IngestJob(respJob)); - return new JobSchedulerStatus(respJobSchedulerStatus.running, jobs); + + const allJobs = []; + + Object.keys(respJobSchedulerStatus).forEach((category: JobCategories) => { + if (respJobSchedulerStatus[category]) { + const catJobs = respJobSchedulerStatus[category].jobs + .map((respJob: RespJob) => new IngestJob(category, respJob)); + allJobs.push(...catJobs); + } + }); + + return new JobSchedulerStatus(allJobs); } + } diff --git a/components/automate-ui/src/app/entities/layout/layout-sidebar.service.ts b/components/automate-ui/src/app/entities/layout/layout-sidebar.service.ts index bf6fecb0553..ce174c40caf 100644 --- a/components/automate-ui/src/app/entities/layout/layout-sidebar.service.ts +++ b/components/automate-ui/src/app/entities/layout/layout-sidebar.service.ts @@ -119,7 +119,7 @@ export class LayoutSidebarService { }], settings: [ { - name: 'Node Management', + name: 'General Settings', items: [ { name: 'Notifications', @@ -138,6 +138,19 @@ export class LayoutSidebarService { }, visible$: new BehaviorSubject(this.ServiceNowFeatureFlagOn) }, + { + name: 'Data Lifecycle', + icon: 'storage', + route: '/settings/data-lifecycle', + authorized: { + anyOf: ['/retention/nodes/status', 'get'] + } + } + ] + }, + { + name: 'Node Management', + items: [ { name: 'Node Integrations', icon: 'settings_input_component', @@ -154,14 +167,6 @@ export class LayoutSidebarService { authorized: { anyOf: ['/secrets/search', 'post'] } - }, - { - name: 'Node Lifecycle', - icon: 'storage', - route: '/settings/node-lifecycle', - authorized: { - anyOf: ['/retention/nodes/status', 'get'] - } } ] }, diff --git a/components/automate-ui/src/app/pages/automate-settings/automate-settings.component.html b/components/automate-ui/src/app/pages/automate-settings/automate-settings.component.html index e74d5fd90fb..3285c301273 100644 --- a/components/automate-ui/src/app/pages/automate-settings/automate-settings.component.html +++ b/components/automate-ui/src/app/pages/automate-settings/automate-settings.component.html @@ -10,61 +10,189 @@ - Node Lifecycle - Control the lifecycle of nodes. + Data Lifecycle + Manage the retention of events, service groups, Chef Client runs, compliance reports and scans in Chef Automate.
- - - - Missing Nodes - If no Chef Client runs have completed in the specified window, nodes will be marked as "Missing". - + +
+ + + Save Changes + Saving... + +
+ +
+

Event Feed

+
+
+ + +
+

Remove event feed data after + days +

+
+ +
+
+ + +
+

Remove Chef Server actions after + days +

+
+
+ +
+

Service Groups

+
+
+ + Changing this setting in the UI is not supported. + + - Never mark + [checked]="!serviceGroupNoHealthChecks.value.disabled" + disabled="true"> + - - - - - - - Minutes - Hours - Days - - - - - Delete Missing Nodes - Nodes set to missing will be removed at this interval. - + +
+ +

When no health check reports have been received for a service in + + + + minutes + hours + days + + + , label the service as disconnected

+
+ +
+
+ + Changing this setting in the UI is not supported. + + - Never delete + [checked]="!serviceGroupRemoveServices.value.disabled" + (change)="handleFormActivation(serviceGroupRemoveServices, $event.detail)" + disabled="true"> + - - - - - - - Minutes - Hours - Days - - - - - + +
+

Remove services labeled as disconnected after + + + + minutes + hours + days + + +

+
+
+ +
+

Client Runs

+
+
+ + +
+

Remove data for Chef Client runs after + days +

+
-
- Apply Changes +
+
+ + +
+ +

When no Chef Client run data has been received from a node in + + + + minutes + hours + days + + + , label the node as missing +

+
+ +
+
+ + +
+

Remove nodes labeled as missing after + days +

+
+ +
+

Compliance

+
+
+ + +
+

Remove compliance reports after + days +

+
+ +
+
+ + +
+

Remove compliance scans after + days +

+
+
+
diff --git a/components/automate-ui/src/app/pages/automate-settings/automate-settings.component.scss b/components/automate-ui/src/app/pages/automate-settings/automate-settings.component.scss index cc7cc35a131..1d8b8527597 100644 --- a/components/automate-ui/src/app/pages/automate-settings/automate-settings.component.scss +++ b/components/automate-ui/src/app/pages/automate-settings/automate-settings.component.scss @@ -1,35 +1,89 @@ @import "~styles/variables"; -chef-td { - &.description { - flex-basis: 45%; - } +.container { + background-color: $chef-white; - &.input { - flex: none; + main { + background-color: $chef-white; } } -input { - width: 60px; - height: 100%; +h2 { + font-size: 16px; + font-weight: 600; } -chef-select { - width: 120px; +.group-section { + margin-bottom: 30px; + + chef-button { + margin: 0; + } + + .checkbox-row { + display: flex; + + input { + width: 60px; + } + + p { + margin: 0; + } + + &+.checkbox-row { + margin-top: 12px; + + &.secondary-row { + margin-left: 69px; + } + } + + + .checkbox-container { + margin-top: 12px; + } + + chef-checkbox { + margin-right: 8px; + font-size: 17px; + } + } } -.center { - margin: auto; - padding: 10px; - width: 10%; +.chef-input { + margin: 0 8px; + + &.ng-invalid { + border: 1px solid $chef-critical; + } } chef-checkbox { - font-size: 17px; - width: 110px; - ::ng-deep .mat-checkbox-layout { padding-bottom: 0; } } + +mat-form-field { + margin: 0 8px 0 0; + width: 120px; + + ::ng-deep .mat-form-field-wrapper { + padding-bottom: 0; + } + + ::ng-deep .mat-form-field-infix { + padding: 0; + border-top: 0; + } + + ::ng-deep .mat-select-disabled { + background-color: $chef-light-grey; + opacity: 0.5; + + .mat-select-value { + color: rgb(84, 84, 84); + } + } +} diff --git a/components/automate-ui/src/app/pages/automate-settings/automate-settings.component.spec.ts b/components/automate-ui/src/app/pages/automate-settings/automate-settings.component.spec.ts index 9d080ee9b01..5b451c1e78d 100644 --- a/components/automate-ui/src/app/pages/automate-settings/automate-settings.component.spec.ts +++ b/components/automate-ui/src/app/pages/automate-settings/automate-settings.component.spec.ts @@ -2,25 +2,43 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { StoreModule } from '@ngrx/store'; import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { FormGroup, FormBuilder } from '@angular/forms'; +import { FormGroup, FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatSelectModule } from '@angular/material/select'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { ngrxReducers, runtimeChecks } from 'app/ngrx.reducers'; import { + JobSchedulerStatus, IngestJob, - IngestJobs, - JobSchedulerStatus + JobCategories } from 'app/entities/automate-settings/automate-settings.model'; import { FeatureFlagsService } from 'app/services/feature-flags/feature-flags.service'; import { TelemetryService } from '../../services/telemetry/telemetry.service'; import { AutomateSettingsComponent } from './automate-settings.component'; +import { using } from 'app/testing/spec-helpers'; + let mockJobSchedulerStatus: JobSchedulerStatus = null; class MockTelemetryService { track() { } } +// A reusable list of all the form names +const ALL_FORMS = [ + 'eventFeedRemoveData', + 'eventFeedServerActions', + 'serviceGroupNoHealthChecks', + 'serviceGroupRemoveServices', + 'clientRunsRemoveData', + 'clientRunsLabelMissing', + 'clientRunsRemoveNodes', + 'complianceRemoveReports', + 'complianceRemoveScans' +]; + describe('AutomateSettingsComponent', () => { let component: AutomateSettingsComponent; let fixture: ComponentFixture; @@ -28,6 +46,11 @@ describe('AutomateSettingsComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ + FormsModule, + ReactiveFormsModule, + MatFormFieldModule, + MatSelectModule, + BrowserAnimationsModule, HttpClientTestingModule, StoreModule.forRoot(ngrxReducers, { runtimeChecks }) ], @@ -63,21 +86,40 @@ describe('AutomateSettingsComponent', () => { it('sets defaults for all form groups', () => { expect(component.automateSettingsForm).not.toEqual(null); expect(component.automateSettingsForm instanceof FormGroup).toBe(true); - expect(Object.keys(component.automateSettingsForm.controls)).toEqual([ - 'eventFeed', - 'clientRuns', - 'complianceData', - 'missingNodes', - 'deleteMissingNodes' - ]); + expect(Object.keys(component.automateSettingsForm.controls)).toEqual(ALL_FORMS); }); - describe('patchDisableValue(form, value)', () => { - it('updates the value of the disable control from the provided form', () => { - expect(component.clientRunsForm.value.disable).toEqual(false); - component.patchDisableValue(component.clientRunsForm, true); - expect(component.clientRunsForm.value.disable).toEqual(true); + describe('handleFormActivation()', () => { + + using(ALL_FORMS + // Service Groups are not currently uncheckable through the UI + .filter( form => !['serviceGroupNoHealthChecks', 'serviceGroupRemoveServices'] + .includes(form)), + function( form: string) { + it(`deactivates the associated ${form} form`, () => { + expect(component[form].value.disabled).toEqual(false); + component.handleFormActivation(component[form], false); + expect(component[form].value.disabled).toEqual(true); + expect(component[form].get('unit').disabled).toBe(true); + expect(component[form].get('threshold').disabled).toBe(true); + }); }); + + using(ALL_FORMS + // Service Groups are not currently uncheckable through the UI + .filter( form => !['serviceGroupsNoHealthChecks', 'serviceGroupRemoveServices'] + .includes(form)), + function (form: string) { + it(`activates the associated ${form} form`, () => { + component[form].patchValue({disabled: true}); // Deactivate form to start + expect(component[form].value.disabled).toEqual(true); + component.handleFormActivation(component[form], true); + expect(component[form].value.disabled).toEqual(false); + expect(component[form].get('unit').disabled).toBe(false); + expect(component[form].get('threshold').disabled).toBe(false); + }); + }); + }); describe('when jobSchedulerStatus is null', () => { @@ -88,78 +130,230 @@ describe('AutomateSettingsComponent', () => { }); }); - describe('noChanges()', () => { - it('reports if there has been any changes to the form', () => { - component.ngOnInit(); - expect(component.noChanges()).toEqual(true); - component.patchDisableValue(component.deleteMissingNodesForm, true); - expect(component.noChanges()).toEqual(false); - }); - }); - describe('when jobSchedulerStatus is set', () => { beforeAll(() => { - const jobMissingNodes: IngestJob = { - running: false, - name: IngestJobs.MissingNodes, - threshold: '60m', - every: '1h' + const eventFeedRemoveData: IngestJob = { + name: 'periodic_purge', + category: JobCategories.EventFeed, + disabled: true, + threshold: '', + purge_policies: { + elasticsearch: [ + { + name: 'feed', + older_than_days: 53, + disabled: false + } + ] + } }; - const jobMissingNodesForDeletion: IngestJob = { - running: true, - name: IngestJobs.MissingNodesForDeletion, - threshold: '24h', - every: '60m' + + const infraNestedForms: IngestJob = { + name: 'periodic_purge_timeseries', + category: JobCategories.Infra, + disabled: false, + threshold: '', + purge_policies: { + elasticsearch: [ + { + name: 'actions', + older_than_days: 22, // default is 30, since disabled + disabled: true // is true older than should be null + }, + { + name: 'converge-history', + older_than_days: 12, + disabled: false + } + ] + } + }; + + const complianceForms: IngestJob = { + category: JobCategories.Compliance, + name: 'periodic_purge', + threshold: '', + disabled: true, + purge_policies: { + elasticsearch: [ + { + name: 'compliance-reports', + older_than_days: 105, + disabled: false + }, + { + name: 'compliance-scans', + older_than_days: 92, + disabled: false + } + ] + } + }; + + const clientRunsRemoveData: IngestJob = { + category: JobCategories.Infra, + name: 'missing_nodes', + disabled : false, + threshold : '7d' }; - mockJobSchedulerStatus = new JobSchedulerStatus(true, [ - jobMissingNodes, - jobMissingNodesForDeletion + + const clientRunsLabelMissing: IngestJob = { + category: JobCategories.Infra, + name: 'missing_nodes_for_deletion', + disabled: false, + threshold: '14m' + }; + + mockJobSchedulerStatus = new JobSchedulerStatus([ + eventFeedRemoveData, + infraNestedForms, + clientRunsRemoveData, + clientRunsLabelMissing, + complianceForms ]); }); - it('updates the "missingNodes" form group correctly', () => { - component.updateForm(mockJobSchedulerStatus); + function genInjestJob(category: string, name: string, threshold: string, disabled: boolean) { + return { category, name, threshold, disabled }; + } + + + function genNestedIngestJob(category: string, name: string, nested_name: string, + threshold: number, disabled: boolean) { + return { + name, + category, + purge_policies: { + elasticsearch: [ + { + name: nested_name, + older_than_days: threshold, + disabled + } + ] + } + }; + } + + using([ + // Event Feed + ['eventFeedRemoveData', 'feed', + genNestedIngestJob('event_feed', 'periodic_purge', 'feed', 1, false)], + ['eventFeedServerActions', 'actions', + genNestedIngestJob('infra', 'periodic_purge_timeseries', 'actions', 2, false)], + + // Services --> not yet enabled + // ['serviceGroupNoHealthChecks'], + // ['serviceGroupRemoveServices'], + + // Client Runs + ['clientRunsRemoveNodes', 'converge-history', + genNestedIngestJob('infra', 'periodic_purge_timeseries', 'converge-history', 7, false)], + + // Compliance + ['complianceRemoveReports', 'compliance-reports', + genNestedIngestJob('compliance', 'periodic_purge', 'compliance-reports', 8, false)], + ['complianceRemoveScans', 'compliance-scans', + genNestedIngestJob('compliance', 'periodic_purge', 'compliance-scans', 9, false)] + ], function(formName: string, nestedName: string, job: IngestJob) { + it(`when updating ${formName} form, + the form data is extracted from the nested form`, () => { + const thisJobScheduler = new JobSchedulerStatus([job]); + component.updateForm(thisJobScheduler); + + const newFormValues = component[formName].value; + const jobData = job.purge_policies.elasticsearch.find(item => item.name === nestedName); + + expect(newFormValues.threshold).toEqual(jobData.older_than_days); + expect(newFormValues.disabled).toEqual(jobData.disabled); + }); + }); + + using([ + // Client Runs + ['clientRunsRemoveData', genInjestJob('infra', 'missing_nodes', '5m', false)], + ['clientRunsLabelMissing', genInjestJob('infra', 'missing_nodes_for_deletion', '6h', false)] + ], function (formName: string, job: IngestJob) { + it(`when updating ${formName} form, + the form data is extracted from the non-nested form`, () => { + const thisJobScheduler = new JobSchedulerStatus([job]); + component.updateForm(thisJobScheduler); - const missingNodesValues = component.automateSettingsForm - .controls.missingNodes.value; + const newFormValues = component[formName].value; - expect(missingNodesValues.disable).toEqual(true); - expect(missingNodesValues.threshold).toEqual('60'); - expect(missingNodesValues.unit).toEqual('m'); + // non-nested threshold is stored differently, so we need to separate it + // into threshold and unit first. + const [jobThreshold, jobUnit] = [job.threshold.slice(0, job.threshold.length - 1), + job.threshold.slice(-1)]; + expect(newFormValues.threshold).toEqual(jobThreshold); + expect(newFormValues.unit).toEqual(jobUnit); + expect(newFormValues.disabled).toEqual(job.disabled); + }); }); - it('updates the "deleteMissingNodes" form group correctly', () => { - component.updateForm(mockJobSchedulerStatus); + using([ + // Event Feed + ['eventFeedRemoveData', 'feed', + genNestedIngestJob('event_feed', 'periodic_purge', 'feed', 1, true)], + ['eventFeedServerActions', 'actions', + genNestedIngestJob('infra', 'periodic_purge_timeseries', 'actions', 2, true)], - const deleteMssingNodesValues = component.automateSettingsForm - .controls.deleteMissingNodes.value; + // Services --> not yet enabled + // ['serviceGroupNoHealthChecks'], + // ['serviceGroupRemoveServices'], - expect(deleteMssingNodesValues.disable).toEqual(false); - expect(deleteMssingNodesValues.threshold).toEqual('24'); - expect(deleteMssingNodesValues.unit).toEqual('h'); + // Client Runs + ['clientRunsRemoveNodes', 'converge-history', + genNestedIngestJob('infra', 'periodic_purge_timeseries', 'converge-history', 7, true)], + + // Compliance + ['complianceRemoveReports', 'compliance-reports', + genNestedIngestJob('compliance', 'periodic_purge', 'compliance-reports', 8, true)], + ['complianceRemoveScans', 'compliance-scans', + genNestedIngestJob('compliance', 'periodic_purge', 'compliance-scans', 9, true)] + ], function (formName: string, nestedName: string, job: IngestJob) { + it(`when ${formName} form is saved as disabled, threshold is undefined + because it is not present in the nested form anymore`, () => { + const thisJobScheduler = new JobSchedulerStatus([job]); + component.updateForm(thisJobScheduler); + + const newFormValues = component[formName].value; + const jobData = job.purge_policies.elasticsearch.find(item => item.name === nestedName); + + expect(newFormValues.threshold).toEqual(undefined); + expect(newFormValues.disabled).toEqual(jobData.disabled); + }); }); - it('does not updates the "eventFeed" form group', () => { - component.updateForm(mockJobSchedulerStatus); + using([ + // Client Runs + ['clientRunsRemoveData', genInjestJob('infra', 'missing_nodes', '5m', true)], + ['clientRunsLabelMissing', genInjestJob('infra', 'missing_nodes_for_deletion', '6h', true)] + ], function (formName: string, job: IngestJob) { + it(`when ${formName} form is saved as disabled, unit and threshold are undefined + because they are not present in the non-nested form anymore`, () => { + const thisJobScheduler = new JobSchedulerStatus([job]); + component.updateForm(thisJobScheduler); - const eventFeedValues = component.automateSettingsForm - .controls.eventFeed.value; + const newFormValues = component[formName].value; - // These are the defaults - expect(eventFeedValues.disable).toEqual(false); - expect(eventFeedValues.threshold).toEqual(''); - expect(eventFeedValues.unit).toEqual('d'); + expect(newFormValues.threshold).toEqual(undefined); + expect(newFormValues.unit).toEqual(undefined); + expect(newFormValues.disabled).toEqual(job.disabled); + }); }); + + describe('when user applyChanges()', () => { it('saves settings', () => { component.updateForm(mockJobSchedulerStatus); component.applyChanges(); - expect(component.formChanged).toEqual(false); + + // expect(component.notificationVisible).toBe(true); expect(component.notificationType).toEqual('info'); expect(component.notificationMessage) - .toEqual('All settings have been updated successfully'); - expect(component.notificationVisible).toEqual(true); + .toEqual('Settings saved.'); }); xdescribe('and there is an error', () => { @@ -172,6 +366,8 @@ describe('AutomateSettingsComponent', () => { expect(component.notificationVisible).toEqual(true); }); }); + }); + }); }); diff --git a/components/automate-ui/src/app/pages/automate-settings/automate-settings.component.ts b/components/automate-ui/src/app/pages/automate-settings/automate-settings.component.ts index 0248665c55b..a21fb8a2937 100644 --- a/components/automate-ui/src/app/pages/automate-settings/automate-settings.component.ts +++ b/components/automate-ui/src/app/pages/automate-settings/automate-settings.component.ts @@ -1,8 +1,11 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, OnDestroy } from '@angular/core'; import { HttpErrorResponse } from '@angular/common/http'; -import { FormGroup, FormBuilder } from '@angular/forms'; -import { Store } from '@ngrx/store'; +import { FormGroup, FormBuilder, AbstractControl, Validators } from '@angular/forms'; +import { Store, select } from '@ngrx/store'; import { NgrxStateAtom } from '../../ngrx.reducers'; +import { Subject } from 'rxjs'; +import { distinctUntilKeyChanged, takeUntil, filter } from 'rxjs/operators'; +import { pendingState } from 'app/entities/entities'; import { LayoutFacadeService, Sidebar } from 'app/entities/layout/layout.facade'; import { automateSettingsState, @@ -15,7 +18,11 @@ import { import { JobSchedulerStatus, IngestJob, - IngestJobs + IngestJobs, + InfraJobName, + NestedJobName, + DefaultFormData, + JobCategories } from '../../entities/automate-settings/automate-settings.model'; import { TelemetryService } from '../../services/telemetry/telemetry.service'; @@ -24,75 +31,163 @@ import { TelemetryService } from '../../services/telemetry/telemetry.service'; styleUrls: ['./automate-settings.component.scss'] }) -export class AutomateSettingsComponent implements OnInit { - private defaultFormData = { - eventFeed: { - unit: 'd', - threshold: '', - disable: false +export class AutomateSettingsComponent implements OnInit, OnDestroy { + + private defaultFormData: DefaultFormData = { + eventFeedRemoveData: { + category: JobCategories.EventFeed, + name: 'periodic_purge', + nested_name: NestedJobName.Feed, + unit: { value: 'd', disabled: false }, + threshold: [{ value: '30', disabled: false}, Validators.required], + disabled: false + }, + eventFeedServerActions: { + category: JobCategories.Infra, + name: 'periodic_purge_timeseries', + nested_name: NestedJobName.Actions, + unit: { value: 'd', disabled: false }, + threshold: [{ value: '30', disabled: false }, Validators.required], + disabled: false + }, + serviceGroupNoHealthChecks: { + category: JobCategories.Services, + name: '', + unit: { value: 'm', disabled: true}, + threshold: [{ value: '5', disabled: true }, Validators.required], + disabled: false // special case: only alterable by the API so we want to show as enabled }, - clientRuns: { - unit: 'd', - threshold: '', - disable: false + serviceGroupRemoveServices: { + category: JobCategories.Services, + name: '', + unit: { value: 'd', disabled: true }, + threshold: [{ value: '5', disabled: true }, Validators.required], + disabled: false // special case: API not ready to alter }, - complianceData: { - unit: 'd', - threshold: '', - disable: false + clientRunsRemoveData: { + category: JobCategories.Infra, + name: 'missing_nodes', + unit: { value: 'd', disabled: false }, + threshold: [{ value: '30', disabled: false }, Validators.required], + disabled: false }, - missingNodes: { - unit: 'd', - threshold: '', - disable: false + clientRunsLabelMissing: { + category: JobCategories.Infra, + name: 'missing_nodes_for_deletion', + unit: { value: 'd', disabled: false }, + threshold: [{ value: '30', disabled: false }, Validators.required], + disabled: false }, - deleteMissingNodes: { - unit: 'd', - threshold: '', - disable: false + clientRunsRemoveNodes: { + category: JobCategories.Infra, + name: 'periodic_purge_timeseries', + nested_name: NestedJobName.ConvergeHistory, + unit: { value: 'd', disabled: false }, + threshold: [{ value: '30', disabled: false }, Validators.required], + disabled: false + }, + complianceRemoveReports: { + category: JobCategories.Compliance, + name: 'periodic_purge', + nested_name: NestedJobName.ComplianceReports, + unit: { value: 'd', disabled: false }, + threshold: [{ value: '30', disabled: false }, Validators.required], + disabled: false + }, + complianceRemoveScans: { + category: JobCategories.Compliance, + name: 'periodic_purge', + nested_name: NestedJobName.ComplianceScans, + unit: { value: 'd', disabled: false }, + threshold: [{ value: '30', disabled: false }, Validators.required], + disabled: false } }; - eventFeedForm: FormGroup; - clientRunsForm: FormGroup; - complianceDataForm: FormGroup; - missingNodesForm: FormGroup; - deleteMissingNodesForm: FormGroup; + + + // Event Feed + eventFeedRemoveData: FormGroup; + eventFeedServerActions: FormGroup; + + // Service Groups + serviceGroupNoHealthChecks: FormGroup; + serviceGroupRemoveServices: FormGroup; + + // Client Runs + clientRunsRemoveData: FormGroup; + clientRunsLabelMissing: FormGroup; + clientRunsRemoveNodes: FormGroup; + + // Compliance + complianceRemoveReports: FormGroup; + complianceRemoveScans: FormGroup; + automateSettingsForm: FormGroup; jobSchedulerStatus: JobSchedulerStatus; - // Has the form changed? - formChanged = false; + // Are settings currently saving + saving = false; // Notification bits notificationVisible = false; notificationType = 'info'; - notificationMessage = 'All settings have been updated successfully'; + notificationMessage = 'Settings saved.'; + + private isDestroyed = new Subject(); constructor( private store: Store, private layoutFacade: LayoutFacadeService, - private formBuilder: FormBuilder, + private fb: FormBuilder, private telemetryService: TelemetryService ) { const formDetails = this.defaultFormData; - this.eventFeedForm = this.formBuilder.group(formDetails['eventFeed']); - this.clientRunsForm = this.formBuilder.group(formDetails['clientRuns']); - this.complianceDataForm = this.formBuilder.group(formDetails['complianceData']); - this.missingNodesForm = this.formBuilder.group(formDetails['missingNodes']); - this.deleteMissingNodesForm = this.formBuilder.group(formDetails['deleteMissingNodes']); - this.automateSettingsForm = this.formBuilder.group({ - eventFeed: this.eventFeedForm, - clientRuns: this.clientRunsForm, - complianceData: this.complianceDataForm, - missingNodes: this.missingNodesForm, - deleteMissingNodes: this.deleteMissingNodesForm + +// EventFeed + this.eventFeedRemoveData = this.fb.group(formDetails.eventFeedRemoveData); + this.eventFeedServerActions = this.fb.group(formDetails.eventFeedServerActions); + + // Service Groups + this.serviceGroupNoHealthChecks = this.fb.group(formDetails.serviceGroupNoHealthChecks); + this.serviceGroupRemoveServices = this.fb.group(formDetails.serviceGroupRemoveServices); + + // Client Runs + this.clientRunsRemoveData = this.fb.group(formDetails.clientRunsRemoveData); + this.clientRunsLabelMissing = this.fb.group(formDetails.clientRunsLabelMissing); + this.clientRunsRemoveNodes = this.fb.group(formDetails.clientRunsRemoveNodes); + + // Compliance + this.complianceRemoveReports = this.fb.group(formDetails.complianceRemoveReports); + this.complianceRemoveScans = this.fb.group(formDetails.complianceRemoveScans); + + // Put the whole form together + this.automateSettingsForm = this.fb.group({ + // Event Feed + eventFeedRemoveData: this.eventFeedRemoveData, + eventFeedServerActions: this.eventFeedServerActions, + // Service Groups + serviceGroupNoHealthChecks: this.serviceGroupNoHealthChecks, + serviceGroupRemoveServices: this.serviceGroupRemoveServices, + + // Client Runs + clientRunsRemoveData: this.clientRunsRemoveData, + clientRunsLabelMissing: this.clientRunsLabelMissing, + clientRunsRemoveNodes: this.clientRunsRemoveNodes, + + // Compliance + complianceRemoveReports: this.complianceRemoveReports, + complianceRemoveScans: this.complianceRemoveScans }); } ngOnInit() { this.layoutFacade.showSidebar(Sidebar.Settings); this.store.dispatch(new GetSettings({})); - this.store.select(automateSettingsState) + + this.store.select(automateSettingsState).pipe( + takeUntil(this.isDestroyed), + distinctUntilKeyChanged('jobSchedulerStatus') + ) .subscribe((automateSettingsSelector) => { if (automateSettingsSelector.errorResp !== null) { const error = automateSettingsSelector.errorResp; @@ -102,65 +197,117 @@ export class AutomateSettingsComponent implements OnInit { this.jobSchedulerStatus = automateSettingsSelector.jobSchedulerStatus; this.telemetryService.track('lifecycleConfiguration', this.jobSchedulerStatus); this.updateForm(this.jobSchedulerStatus); - this.onChanges(); } }); + + // subscribe to changeConfiguration + this.store.pipe( + select(changeConfiguration), + filter(change => this.saving && !pendingState(change)), + takeUntil(this.isDestroyed) + ) + .subscribe((changeConfigurationSelector) => { + if (changeConfigurationSelector.errorResp !== null) { + const error = changeConfigurationSelector.errorResp; + const errMsg = 'Unable to update one or more settings.'; + this.showErrorNotification(error, errMsg); + this.store.dispatch(new GetSettings({})); // reset form to previously stored settings + this.automateSettingsForm.markAsPristine(); + this.saving = false; + } else if (changeConfigurationSelector.status === 'loadingSuccess') { + this.showSuccessNotification(); + this.automateSettingsForm.markAsPristine(); + this.saving = false; + } + }); } - // Has the form changed? - public noChanges() { - return !this.formChanged; + ngOnDestroy(): void { + this.isDestroyed.next(true); + this.isDestroyed.complete(); } - // patchUnitValue is the workaround for the chef-select molecule since it is not - // an input annotation we need to patch the value inside the FormGroup - public patchUnitValue(form, event) { - form.controls['unit'].setValue(event.target.value); + // This prevents a user from being allowed to enter negative numbers + // or other actions that we don't want to allow + public preventNegatives(key: string) { + const allowedKeys = [ + '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', + 'Backspace', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Tab' + ]; + return allowedKeys.includes(key); } - // isUnitValue is the workaround for the chef-select molecule since it is not - // a select field we need to check value to use inside FormGroup - public isUnitValue(form, unit: string) { - return form.controls['unit'].value === unit; + private setEnabled(control: AbstractControl, enabled: boolean): void { + if (enabled) { + control.enable(); + control.setValidators([Validators.required]); + control.updateValueAndValidity(); + } else { + control.disable(); + } } - // patchDisableValue is the workaround for the chef-checkbox molecule since it is not - // an input annotation we need to patch the value inside the FormGroup - public patchDisableValue(form, checked: boolean) { - form.controls['disable'].setValue(checked); + public handleFormActivation(form: FormGroup, checked: boolean): void { + // patchValue does not mark the form dirty, so we need to do it manually; + form.get('disabled').markAsDirty(); + // patchValue is a workaround for the chef-checkbox because we need to be + // able to store a reference to it being checked or not + form.patchValue({ + disabled: !checked + }); + + + // this disables the relevant controls; + this.setEnabled(form.controls.unit, checked); + this.setEnabled(form.controls.threshold, checked); } // Apply the changes that the user updated in the forms public applyChanges() { - // (@afiune) At the moment the only two forms that are enabled are: - // => 'missingNodes' - // => 'deleteMissingNodes' - // - // We will apply the changes on the rest when we expose the forms + this.saving = true; + // Note: Services are currently not enabled through the form const jobs: IngestJob[] = [ - IngestJobs.MissingNodes, - IngestJobs.MissingNodesForDeletion + // Event Feed + IngestJobs.EventFeedRemoveData, + IngestJobs.EventFeedServerActions, + // Service Groups + IngestJobs.ServiceGroupNoHealthChecks, + IngestJobs.ServiceGroupRemoveServices, + // Client Runs + IngestJobs.ClientRunsRemoveData, + IngestJobs.ClientRunsLabelMissing, + IngestJobs.ClientRunsRemoveNodes, + // Compliance + IngestJobs.ComplianceRemoveReports, + IngestJobs.ComplianceRemoveScans ].map(jobName => { - const jobForm = this.getJobForm(jobName); - const job = new IngestJob(null); - job.name = jobName; - job.running = !jobForm.disable; - job.threshold = jobForm.threshold + jobForm.unit; + // Extract the raw values from the formGroup so that + // we can make sure to keep disabled inputs populated in the UI + const jobForm = this[jobName].getRawValue(); + + const isNested = jobForm.nested_name ? true : false; + const job = new IngestJob(null, null); + job.category = jobForm.category; + job.name = jobForm.name; + job.disabled = jobForm.disabled; + + // If the user doesn't enter any number at all - this defaults to 0 + jobForm.threshold === null + // When not nested, threshold needs to be a string ex: '4w' + ? job.threshold = '0' + jobForm.unit + : job.threshold = jobForm.threshold + jobForm.unit; + + if ( isNested ) { + // When nested, threshold needs to be a number ex: 16 + job.nested_name = jobForm.nested_name; + job.threshold = jobForm.threshold; // Automatically becomes a 0 from + // parseInt in request if left blank + } + return job; }); this.store.dispatch(new ConfigureSettings({jobs: jobs})); - this.store.select(changeConfiguration) - .subscribe((changeConfigurationSelector) => { - if (changeConfigurationSelector.errorResp !== null) { - const error = changeConfigurationSelector.errorResp; - const errMsg = 'Unable to update one or more settings.'; - this.showErrorNotification(error, errMsg); - } else { - this.formChanged = false; - this.showSuccessNotification(); - } - }); } // Hides the notification banner @@ -178,12 +325,6 @@ export class AutomateSettingsComponent implements OnInit { } } - // Subscribes to any change inside the automateSettingsForm - private onChanges() { - this.automateSettingsForm.valueChanges - .subscribe(_change => this.formChanged = true); - } - private showErrorNotification(error: HttpErrorResponse, msg: string) { // Extract the error message from the HttpErrorResponse // if it is available inside the body. @@ -211,61 +352,132 @@ export class AutomateSettingsComponent implements OnInit { // Update forms until we get the job scheduler status public updateForm(jobSchedulerStatus: JobSchedulerStatus) { + if (jobSchedulerStatus === null) { return; } jobSchedulerStatus.jobs.forEach((job: IngestJob) => { - const [threshold, unit] = this.splitThreshold(job.threshold); - const form = { - disable: !job.running, - threshold: threshold, - unit: unit - }; - switch (job.name) { - case IngestJobs.MissingNodes: { - this.missingNodesForm = this.formBuilder.group(form); - break; + switch (job.category) { + case 'infra': { + this.populateInfra(job); } - case IngestJobs.MissingNodesForDeletion: { - this.deleteMissingNodesForm = this.formBuilder.group(form); - break; + break; + + case 'compliance': // fallthrough + case 'event_feed': { + this.populateNested(job); } - // TODO @afiune missing forms to add, at the moment we can't modify - // this parameter/settings since the services take it at startup. - // (we need to change that first) - // - // this.clientRunsForm = this.formBuilder.group(form); - // this.complianceDataForm = this.formBuilder.group(form); - // this.eventFeedForm = this.formBuilder.group(form); + break; + + default: + break; } }); + } - this.automateSettingsForm = this.formBuilder.group({ - eventFeed: this.eventFeedForm, - clientRuns: this.clientRunsForm, - complianceData: this.complianceDataForm, - missingNodes: this.missingNodesForm, - deleteMissingNodes: this.deleteMissingNodesForm - }); + // Splits a packed threshold into a number and a unit, where unit is a single character + // example: '12d' => ['12', 'd'] + private splitThreshold(threshold: string): [string, string] { + return [ + threshold.slice(0, threshold.length - 1), + threshold.slice(-1) + ]; } - private getJobForm(job: string) { - switch (job) { - case IngestJobs.MissingNodes: { - return this.automateSettingsForm.value['missingNodes']; + private populateInfra(job: IngestJob): void { + let formThreshold, formUnit; + + switch (job.name) { + case InfraJobName.MissingNodes: { + this.handleDisable(this.clientRunsRemoveData, job.disabled); + [formThreshold, formUnit] = this.splitThreshold(job.threshold); + this.clientRunsRemoveData.patchValue({ + unit: formUnit, + threshold: formThreshold, + disabled: job.disabled + }); } - case IngestJobs.MissingNodesForDeletion: { - return this.automateSettingsForm.value['deleteMissingNodes']; + break; + + case InfraJobName.MissingNodesForDeletion: { + this.handleDisable(this.clientRunsLabelMissing, job.disabled); + [formThreshold, formUnit] = this.splitThreshold(job.threshold); + this.clientRunsLabelMissing.patchValue({ + unit: formUnit, + threshold: formThreshold, + disabled: job.disabled + }); } + break; + + case InfraJobName.DeleteNodes: { + // delete_nodes not yet implemented + } + break; + + case InfraJobName.PeriodicPurgeTimeseries: { + this.populateNested(job); + } + break; + + default: + break; } } - private splitThreshold(threshold: string) { - return [ - threshold.slice(0, threshold.length - 1), - threshold.slice(-1) - ]; + private populateNested(job: IngestJob): void { + const _jobs = job.purge_policies.elasticsearch; + + _jobs.forEach(_job => { + const form = { + threshold: _job.older_than_days, + disabled: _job.disabled + }; + + switch (_job.name) { + case NestedJobName.ComplianceReports: { + this.handleDisable(this.complianceRemoveReports, _job.disabled); + this.complianceRemoveReports.patchValue(form); + } + break; + + case NestedJobName.ComplianceScans: { + this.handleDisable(this.complianceRemoveScans, _job.disabled); + this.complianceRemoveScans.patchValue(form); + } + break; + + case NestedJobName.Feed: { + this.handleDisable(this.eventFeedRemoveData, _job.disabled); + this.eventFeedRemoveData.patchValue(form); + } + break; + + case NestedJobName.Actions: { + this.handleDisable(this.eventFeedServerActions, _job.disabled); + this.eventFeedServerActions.patchValue(form); + } + break; + + case NestedJobName.ConvergeHistory: { + this.handleDisable(this.clientRunsRemoveNodes, _job.disabled); + this.clientRunsRemoveNodes.patchValue(form); + } + break; + + default: + break; + } + }); } + + private handleDisable(form: FormGroup, disabled: boolean = false): void { + // this disables the relevant controls + // We have to pass in !disabled because the function is initially build for enabling + this.setEnabled(form.controls.unit, !disabled); + this.setEnabled(form.controls.threshold, !disabled); + } + } diff --git a/components/automate-ui/src/app/pages/settings-landing/settings-landing.component.ts b/components/automate-ui/src/app/pages/settings-landing/settings-landing.component.ts index 94fe5c076c4..190a9f1d986 100644 --- a/components/automate-ui/src/app/pages/settings-landing/settings-landing.component.ts +++ b/components/automate-ui/src/app/pages/settings-landing/settings-landing.component.ts @@ -14,7 +14,7 @@ export class SettingsLandingComponent { { anyOfCheck: [['/datafeed/destinations', 'post', '']], route: '/settings/data-feed' }, { anyOfCheck: [['/nodemanagers/search', 'post', '']], route: '/settings/node-integrations' }, { anyOfCheck: [['/secrets/search', 'post', '']], route: '/settings/node-credentials' }, - { anyOfCheck: [['/retention/nodes/status', 'get', '']], route: '/settings/node-lifecycle' }, + { anyOfCheck: [['/retention/nodes/status', 'get', '']], route: '/settings/data-lifecycle' }, { allOfCheck: [['/iam/v2/users', 'get', '']], route: '/settings/users' }, { allOfCheck: [['/iam/v2/teams', 'get', '']], route: '/settings/teams' }, { allOfCheck: [['/iam/v2/tokens', 'get', '']], route: '/settings/tokens' }, diff --git a/components/automate-ui/src/environments/environment.no_auth.ts b/components/automate-ui/src/environments/environment.no_auth.ts index 3f89608e27b..a8b749a39ce 100644 --- a/components/automate-ui/src/environments/environment.no_auth.ts +++ b/components/automate-ui/src/environments/environment.no_auth.ts @@ -9,7 +9,7 @@ export const environment = { profiles_url: '/api/v0/profiles', compliance_url: '/api/v0/compliance', config_mgmt_url: '/api/v0/cfgmgmt', - retention_url: '/api/v0/retention', + retention_url: '/api/v0/data-lifecycle', ingest_url: '/api/v0/ingest', infra_proxy_url: '/api/v0/infra', deployment_url: '/api/v0/deployment', diff --git a/components/automate-ui/src/environments/environment.prod.ts b/components/automate-ui/src/environments/environment.prod.ts index 34b09ab5ace..44afe12c0e8 100644 --- a/components/automate-ui/src/environments/environment.prod.ts +++ b/components/automate-ui/src/environments/environment.prod.ts @@ -16,7 +16,7 @@ export const environment = { profiles_url: '/api/v0/profiles', compliance_url: '/api/v0/compliance', config_mgmt_url: '/api/v0/cfgmgmt', - retention_url: '/api/v0/retention', + retention_url: '/api/v0/data-lifecycle', ingest_url: '/api/v0/ingest', infra_proxy_url: '/api/v0/infra', deployment_url: '/api/v0/deployment', diff --git a/components/automate-ui/src/environments/environment.ts b/components/automate-ui/src/environments/environment.ts index 2e2034e8a08..c0b72fed4e3 100644 --- a/components/automate-ui/src/environments/environment.ts +++ b/components/automate-ui/src/environments/environment.ts @@ -22,7 +22,7 @@ export const environment = { profiles_url: '/api/v0/profiles', compliance_url: '/api/v0/compliance', config_mgmt_url: '/api/v0/cfgmgmt', - retention_url: '/api/v0/retention', + retention_url: '/api/v0/data-lifecycle', ingest_url: '/api/v0/ingest', deployment_url: '/api/v0/deployment', infra_proxy_url: '/api/v0/infra', diff --git a/components/automate-ui/src/styles.scss b/components/automate-ui/src/styles.scss index 7869a451b46..328003832bf 100644 --- a/components/automate-ui/src/styles.scss +++ b/components/automate-ui/src/styles.scss @@ -347,3 +347,23 @@ input { text-align: center; top: -2px; } + +// mat-select dropdown menu + +[panelClass="chef-dropdown"] { + height: 45px; + background-color: $chef-white; + padding: 1em; + font-size: 14px; + border: 1px solid $chef-light-grey; + border-radius: 4px; + box-sizing: border-box; + font-family: inherit; + transition: border 0.4s ease; + + &:focus, + &:active { + border-color: $chef-primary-bright; + outline: none; + } +}