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;
+ }
+}