CTL command history page
This adds a history page, it could be argued that it should be a popup or an accordion on each sub page but this seemed to work the best. It may require the existing sqlite database to be delete Change-Id: Id48d513ab2d25e60351bd34e8ba84db80d266d87
This commit is contained in:
parent
c13a04669b
commit
c004a202dc
@ -72,6 +72,10 @@ export class AppComponent implements OnInit, WsReceiver {
|
||||
displayName: 'Secret',
|
||||
route: 'ctl/secret',
|
||||
iconName: 'security'
|
||||
}, {
|
||||
displayName: 'History',
|
||||
route: 'ctl/history',
|
||||
iconName: 'history'
|
||||
}]
|
||||
}, {
|
||||
displayName: 'Dashboards',
|
||||
|
@ -21,6 +21,7 @@ import { DocumentComponent } from './document/document.component';
|
||||
import { ImageComponent } from './image/image.component';
|
||||
import { PhaseComponent } from './phase/phase.component';
|
||||
import { SecretComponent } from './secret/secret.component';
|
||||
import { HistoryComponent } from './history/history.component';
|
||||
import { WsConstants } from 'src/services/ws/ws.models';
|
||||
import { AuthGuard } from 'src/services/auth-guard/auth-guard.service';
|
||||
|
||||
@ -52,6 +53,10 @@ const routes: Routes = [{
|
||||
path: WsConstants.SECRET,
|
||||
canActivate: [AuthGuard],
|
||||
component: SecretComponent,
|
||||
}, {
|
||||
path: WsConstants.HISTORY,
|
||||
canActivate: [AuthGuard],
|
||||
component: HistoryComponent,
|
||||
}];
|
||||
|
||||
@NgModule({
|
||||
|
@ -22,6 +22,7 @@ import { CtlRoutingModule } from './ctl-routing.module';
|
||||
import { PhaseModule } from './phase/phase.module';
|
||||
import { SecretModule } from './secret/secret.module';
|
||||
import { ConfigModule } from './config/config.module';
|
||||
import { HistoryModule } from './history/history.module';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
@NgModule({
|
||||
@ -34,7 +35,8 @@ import { CommonModule } from '@angular/common';
|
||||
DocumentModule,
|
||||
BaremetalModule,
|
||||
PhaseModule,
|
||||
SecretModule
|
||||
SecretModule,
|
||||
HistoryModule
|
||||
],
|
||||
declarations: [CtlComponent],
|
||||
providers: []
|
||||
|
21
client/src/app/ctl/history/history.component.css
Executable file
21
client/src/app/ctl/history/history.component.css
Executable file
@ -0,0 +1,21 @@
|
||||
/*
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
*/
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
77
client/src/app/ctl/history/history.component.html
Executable file
77
client/src/app/ctl/history/history.component.html
Executable file
@ -0,0 +1,77 @@
|
||||
<h1>Airship CTL Command History</h1>
|
||||
|
||||
<div class="container">
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<b>Command History: </b>
|
||||
<select id="displaySelect" (change)="displayChange($event.target.value)">
|
||||
<option value="baremetal">Baremetal</option>
|
||||
<option value="cluster">Cluster</option>
|
||||
<option value="config">Config</option>
|
||||
<option value="document">Document</option>
|
||||
<option value="image">Image</option>
|
||||
<option value="phase">Phase</option>
|
||||
<option value="secret">Secret</option>
|
||||
</select>
|
||||
<button type="submit" id="refreshButton" (click)="refresh()">Refresh</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<br>
|
||||
<div id="HistoryFound" hidden>
|
||||
<mat-form-field>
|
||||
<mat-label>Filter</mat-label>
|
||||
<input id="filterInput" matInput (keyup)="applyFilter($event)" placeholder="Ex. set phaser to stun" #input>
|
||||
</mat-form-field>
|
||||
<div class="mat-elevation-z8">
|
||||
<table mat-table [dataSource]="dataSource" matSort>
|
||||
<ng-container matColumnDef="subComponent">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> subComponent </th>
|
||||
<td mat-cell *matCellDef="let row"> {{row.subComponent}} </td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="user">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> user </th>
|
||||
<td mat-cell *matCellDef="let row"> {{row.user}} </td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="type">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> type </th>
|
||||
<td mat-cell *matCellDef="let row"> {{row.type}} </td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="target">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> target </th>
|
||||
<td mat-cell *matCellDef="let row"> {{row.target}} </td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="success">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> success </th>
|
||||
<td mat-cell *matCellDef="let row"> {{row.success}} </td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="started">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> started </th>
|
||||
<td mat-cell *matCellDef="let row"> {{row.started}} </td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="elapsed">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> elapsed </th>
|
||||
<td mat-cell *matCellDef="let row"> {{row.elapsed}} </td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="stopped">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> stopped </th>
|
||||
<td mat-cell *matCellDef="let row"> {{row.stopped}} </td>
|
||||
</ng-container>
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
<!-- Row shown when there is no matching data. -->
|
||||
<tr class="mat-row" *matNoDataRow>
|
||||
<td class="mat-cell" colspan="4">No data matching the filter "{{input.value}}"</td>
|
||||
</tr>
|
||||
</table>
|
||||
<mat-paginator [pageSizeOptions]="[5, 10, 25, 50, 100]" [pageSize]=10></mat-paginator>
|
||||
</div>
|
||||
</div>
|
||||
<div id="LoadingHistory" class="center">
|
||||
<h2>Loading history for {{selectedHistory}} please wait...</h2>
|
||||
</div>
|
||||
<div id="HistoryNotFound" class="center" hidden>
|
||||
<h2>No command history data is available for {{selectedHistory}}</h2>
|
||||
</div>
|
||||
</div>
|
66
client/src/app/ctl/history/history.component.spec.ts
Executable file
66
client/src/app/ctl/history/history.component.spec.ts
Executable file
@ -0,0 +1,66 @@
|
||||
/*
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
*/
|
||||
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { HistoryComponent } from './history.component';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatPaginatorModule } from '@angular/material/paginator';
|
||||
import { ToastrModule } from 'ngx-toastr';
|
||||
import { MatSortModule } from '@angular/material/sort';
|
||||
import { MatExpansionModule } from '@angular/material/expansion';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
|
||||
describe('HistoryComponent', () => {
|
||||
const component: HistoryComponent = null;
|
||||
// let fixture: ComponentFixture<BaremetalComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
MatExpansionModule,
|
||||
MatCardModule,
|
||||
MatDividerModule,
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
MatCheckboxModule,
|
||||
MatFormFieldModule,
|
||||
MatTableModule,
|
||||
MatPaginatorModule,
|
||||
MatInputModule,
|
||||
MatSortModule,
|
||||
ToastrModule.forRoot()
|
||||
],
|
||||
declarations: [
|
||||
HistoryComponent
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
// beforeEach(() => {
|
||||
// fixture = TestBed.createComponent(BaremetalComponent);
|
||||
// component = fixture.componentInstance;
|
||||
// fixture.detectChanges();
|
||||
// });
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeFalsy();
|
||||
});
|
||||
});
|
142
client/src/app/ctl/history/history.component.ts
Executable file
142
client/src/app/ctl/history/history.component.ts
Executable file
@ -0,0 +1,142 @@
|
||||
/*
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
*/
|
||||
|
||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { WsService } from 'src/services/ws/ws.service';
|
||||
import { WsMessage, WsReceiver, WsConstants } from 'src/services/ws/ws.models';
|
||||
import { Log } from 'src/services/log/log.service';
|
||||
import { LogMessage } from 'src/services/log/log-message';
|
||||
import { MatPaginator } from '@angular/material/paginator';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { StatData } from './history.models';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bare-metal',
|
||||
templateUrl: './history.component.html',
|
||||
styleUrls: ['./history.component.css']
|
||||
})
|
||||
|
||||
export class HistoryComponent implements WsReceiver, OnInit {
|
||||
className = this.constructor.name;
|
||||
type = WsConstants.CTL;
|
||||
component = WsConstants.HISTORY;
|
||||
|
||||
selectedHistory = 'baremetal';
|
||||
|
||||
displayedColumns: string[] = ['subComponent', 'user', 'type', 'target', 'success', 'started', 'elapsed', 'stopped'];
|
||||
dataSources: Map<string, MatTableDataSource<StatData>> = new Map();
|
||||
dataSource: MatTableDataSource<StatData> = new MatTableDataSource();
|
||||
@ViewChild(MatSort) sort: MatSort;
|
||||
@ViewChild(MatPaginator) paginator: MatPaginator;
|
||||
|
||||
constructor(private websocketService: WsService) {
|
||||
this.websocketService.registerFunctions(this);
|
||||
}
|
||||
|
||||
async receiver(message: WsMessage): Promise<void> {
|
||||
if (message.hasOwnProperty(WsConstants.ERROR)) {
|
||||
this.websocketService.printIfToast(message);
|
||||
} else {
|
||||
switch (message.subComponent) {
|
||||
case WsConstants.GET_DEFAULTS:
|
||||
this.pushData(message.data);
|
||||
break;
|
||||
default:
|
||||
Log.Error(new LogMessage('History message sub component not handled', this.className, message));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
// Filters the table based on the user input
|
||||
// taken partly from the example: https://material.angular.io/components/table/overview
|
||||
applyFilter(event: Event): void {
|
||||
// get the filter value
|
||||
const filterValue = (event.target as HTMLInputElement).value;
|
||||
this.dataSource.filter = filterValue.trim().toLowerCase();
|
||||
|
||||
if (this.dataSource.paginator) {
|
||||
this.dataSource.paginator.firstPage();
|
||||
}
|
||||
}
|
||||
|
||||
// hide / show tables based on what's selected
|
||||
displayChange(displaying): void {
|
||||
// tag the replace string with the current context
|
||||
this.selectedHistory = displaying;
|
||||
|
||||
// clear filters if they exist
|
||||
const filter = (document.getElementById('filterInput') as HTMLInputElement);
|
||||
if (filter.value.length > 0) {
|
||||
filter.value = '';
|
||||
this.dataSource.filter = '';
|
||||
}
|
||||
|
||||
// if we have data show the table otherwise show nothing. Do you hear me Lebowski? NOTHING!
|
||||
if (displaying in this.dataSources) {
|
||||
this.dataSource = this.dataSources[displaying];
|
||||
this.dataSource.paginator = this.paginator;
|
||||
this.dataSource.sort = this.sort;
|
||||
|
||||
document.getElementById('HistoryFound').removeAttribute('hidden');
|
||||
document.getElementById('LoadingHistory').setAttribute('hidden', 'true');
|
||||
document.getElementById('HistoryNotFound').setAttribute('hidden', 'true');
|
||||
} else {
|
||||
document.getElementById('HistoryFound').setAttribute('hidden', 'true');
|
||||
document.getElementById('LoadingHistory').setAttribute('hidden', 'true');
|
||||
document.getElementById('HistoryNotFound').removeAttribute('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
refresh(): void {
|
||||
document.getElementById('HistoryFound').setAttribute('hidden', 'true');
|
||||
document.getElementById('LoadingHistory').removeAttribute('hidden');
|
||||
document.getElementById('HistoryNotFound').setAttribute('hidden', 'true');
|
||||
|
||||
const message = new WsMessage(this.type, this.component, WsConstants.GET_DEFAULTS);
|
||||
Log.Debug(new LogMessage('Attempting to ask for node data', this.className, message));
|
||||
this.websocketService.sendMessage(message);
|
||||
}
|
||||
|
||||
// extract the data structure sent from the backend & render it to the table
|
||||
private pushData(data): void {
|
||||
Object.keys(data).forEach(key => {
|
||||
const recordSet: StatData[] = new Array();
|
||||
data[key].forEach(record => {
|
||||
// we do it this way instead of a straight convert because we want to format dates and success / fail messages
|
||||
// it could be argued that this should be done on the backend
|
||||
recordSet.push({
|
||||
subComponent: record.SubComponent,
|
||||
user: record.User,
|
||||
type: record.ActionType,
|
||||
target: record.Target,
|
||||
success: record.Success ? 'Succeeded' : 'Failed',
|
||||
started: new Date(record.Started).toString(),
|
||||
elapsed: record.Elapsed + (record.Elapsed < 1000 ? ' ms' : ' seconds'),
|
||||
stopped: new Date(record.Stopped).toString()
|
||||
});
|
||||
});
|
||||
|
||||
this.dataSources[key] = new MatTableDataSource(recordSet);
|
||||
});
|
||||
|
||||
const displaying = (document.getElementById('displaySelect') as HTMLInputElement).value;
|
||||
this.displayChange(displaying);
|
||||
}
|
||||
}
|
25
client/src/app/ctl/history/history.models.ts
Executable file
25
client/src/app/ctl/history/history.models.ts
Executable file
@ -0,0 +1,25 @@
|
||||
/*
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
*/
|
||||
|
||||
// used to populate the stat data
|
||||
export interface StatData {
|
||||
subComponent: string;
|
||||
user: string;
|
||||
type: string;
|
||||
target: string;
|
||||
success: string;
|
||||
started: string;
|
||||
elapsed: string;
|
||||
stopped: string;
|
||||
}
|
41
client/src/app/ctl/history/history.module.ts
Executable file
41
client/src/app/ctl/history/history.module.ts
Executable file
@ -0,0 +1,41 @@
|
||||
/*
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
*/
|
||||
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { HistoryComponent } from './history.component';
|
||||
import { ToastrModule } from 'ngx-toastr';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatPaginatorModule } from '@angular/material/paginator';
|
||||
import { MatSortModule } from '@angular/material/sort';
|
||||
import { MatExpansionModule } from '@angular/material/expansion';
|
||||
@NgModule({
|
||||
imports: [
|
||||
MatCheckboxModule,
|
||||
MatFormFieldModule,
|
||||
MatTableModule,
|
||||
MatPaginatorModule,
|
||||
MatInputModule,
|
||||
MatSortModule,
|
||||
MatExpansionModule,
|
||||
ToastrModule.forRoot()
|
||||
],
|
||||
declarations: [
|
||||
HistoryComponent
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class HistoryModule { }
|
1
client/src/assets/icons/history.svg
Executable file
1
client/src/assets/icons/history.svg
Executable file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M13 3c-4.97 0-9 4.03-9 9H1l3.89 3.89.07.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42C8.27 19.99 10.51 21 13 21c4.97 0 9-4.03 9-9s-4.03-9-9-9zm-1 5v5l4.28 2.54.72-1.21-3.5-2.08V8H12z"/></svg>
|
After Width: | Height: | Size: 362 B |
@ -33,5 +33,6 @@ export enum Icons {
|
||||
check_circle = 'check_circle',
|
||||
close = 'close',
|
||||
lock = 'lock',
|
||||
lock_open = 'lock_open'
|
||||
lock_open = 'lock_open',
|
||||
history = 'history'
|
||||
}
|
||||
|
@ -105,6 +105,7 @@ export class WsConstants {
|
||||
public static readonly AUTH = 'auth';
|
||||
public static readonly AUTHENTICATE = 'authenticate';
|
||||
public static readonly DENIED = 'denied';
|
||||
public static readonly HISTORY = 'history';
|
||||
public static readonly INITIALIZE = 'initialize';
|
||||
public static readonly KEEPALIVE = 'keepalive';
|
||||
public static readonly LOG = 'log';
|
||||
|
@ -109,6 +109,7 @@ const (
|
||||
Cluster WsComponentType = "cluster"
|
||||
CTLConfig WsComponentType = "config"
|
||||
Document WsComponentType = "document"
|
||||
History WsComponentType = "history"
|
||||
Image WsComponentType = "image"
|
||||
Phase WsComponentType = "phase"
|
||||
Secret WsComponentType = "secret"
|
||||
|
@ -37,6 +37,7 @@ var CTLFunctionMap = map[configs.WsComponentType]func(*string, configs.WsMessage
|
||||
configs.Cluster: HandleClusterRequest,
|
||||
configs.CTLConfig: HandleConfigRequest,
|
||||
configs.Document: HandleDocumentRequest,
|
||||
configs.History: HandleHistoryRequest,
|
||||
configs.Image: HandleImageRequest,
|
||||
configs.Phase: HandlePhaseRequest,
|
||||
configs.Secret: HandleSecretRequest,
|
||||
|
139
pkg/ctl/history.go
Executable file
139
pkg/ctl/history.go
Executable file
@ -0,0 +1,139 @@
|
||||
/*
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package ctl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"opendev.org/airship/airshipui/pkg/configs"
|
||||
"opendev.org/airship/airshipui/pkg/log"
|
||||
"opendev.org/airship/airshipui/pkg/statistics"
|
||||
)
|
||||
|
||||
// This is close to but not exactly a transaction structure from statistics, it's redone here because reasons
|
||||
type record struct {
|
||||
SubComponent configs.WsSubComponentType
|
||||
User *string
|
||||
ActionType *string
|
||||
Target *string
|
||||
Success bool
|
||||
Started int64
|
||||
Elapsed int64
|
||||
Stopped int64
|
||||
}
|
||||
|
||||
// HandleHistoryRequest will flop between requests so we don't have to have them all mapped as function calls
|
||||
// This will wait for the sub component to complete before responding. The assumption is this is an async request
|
||||
func HandleHistoryRequest(user *string, request configs.WsMessage) configs.WsMessage {
|
||||
response := configs.WsMessage{
|
||||
Type: configs.CTL,
|
||||
Component: configs.History,
|
||||
SubComponent: request.SubComponent,
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
subComponent := request.SubComponent
|
||||
switch subComponent {
|
||||
case configs.GetDefaults:
|
||||
response.Data, err = getData(nil, nil)
|
||||
default:
|
||||
err = fmt.Errorf("Subcomponent %s not found", request.SubComponent)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
e := err.Error()
|
||||
response.Error = &e
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// getData will return all rows within a specific date range
|
||||
func getData(notBefore *int64, notAfter *int64) (map[string][]record, error) {
|
||||
var wherePstmt strings.Builder
|
||||
where := false
|
||||
// because we may want data within a range add range slice where statements
|
||||
if notBefore != nil {
|
||||
where = true
|
||||
wherePstmt.WriteString(fmt.Sprintf(" where started >= %d", notBefore))
|
||||
}
|
||||
if notAfter != nil {
|
||||
if where {
|
||||
wherePstmt.WriteString(fmt.Sprintf(" and stopped <= %d", notAfter))
|
||||
} else {
|
||||
wherePstmt.WriteString(fmt.Sprintf(" where stopped <= %d", notAfter))
|
||||
}
|
||||
}
|
||||
|
||||
data := map[string][]record{}
|
||||
for _, table := range statistics.Tables {
|
||||
// create a basic prepared statement to get data
|
||||
// Why a prepared statement? Little Bobby Tables is why:
|
||||
// https://xkcd.com/327/
|
||||
pstmt := fmt.Sprintf("select * from %s", table)
|
||||
if where {
|
||||
pstmt += wherePstmt.String()
|
||||
}
|
||||
|
||||
// Dark Helmet: Why are we always preparing? Just go!
|
||||
stmt, err := statistics.DB.Prepare(pstmt)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get the rows back from the query
|
||||
rows, err := stmt.Query()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
records := []record{}
|
||||
for rows.Next() {
|
||||
var r record
|
||||
err = rows.Scan(
|
||||
&r.SubComponent,
|
||||
&r.User,
|
||||
&r.ActionType,
|
||||
&r.Target,
|
||||
&r.Success,
|
||||
&r.Started,
|
||||
&r.Elapsed,
|
||||
&r.Stopped,
|
||||
)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
records = append(records, r)
|
||||
}
|
||||
|
||||
err = rows.Err()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(records) > 0 {
|
||||
data[table] = records
|
||||
}
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
@ -42,8 +42,10 @@ var (
|
||||
doNotRecordRegex = regexp.MustCompile(`(?i)^get|^init$`) // the default gets we don't care about
|
||||
|
||||
writeMutex sync.Mutex
|
||||
db *sql.DB
|
||||
tables = []string{"baremetal", "cluster", "config", "document", "image", "phase", "secret"}
|
||||
// DB is public so other packages can do selects on it
|
||||
DB *sql.DB
|
||||
// Tables is public so other packages can range over it
|
||||
Tables = []string{"baremetal", "cluster", "config", "document", "image", "phase", "secret"}
|
||||
)
|
||||
|
||||
const (
|
||||
@ -54,9 +56,9 @@ const (
|
||||
type text check(type in ('direct', 'phase')) null,
|
||||
target text null,
|
||||
success tinyint(1) default 0,
|
||||
started timestamp,
|
||||
started bigint,
|
||||
elapsed bigint,
|
||||
stopped timestamp)`
|
||||
stopped bigint)`
|
||||
// the prepared statement used for inserts
|
||||
// TODO (aschiefe): determine if we need to batch inserts
|
||||
insert = `INSERT INTO table(subcomponent,
|
||||
@ -81,7 +83,7 @@ func Init() {
|
||||
var err error
|
||||
// TODO (aschiefe): encrypt & password protect the database
|
||||
// TODO (aschiefe): pull the db location out to the confing
|
||||
db, err = sql.Open("sqlite3", "./sqlite/statistics.db")
|
||||
DB, err = sql.Open("sqlite3", "./sqlite/statistics.db")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@ -95,8 +97,8 @@ func Init() {
|
||||
|
||||
// createTables is only used when there is no database to write the correct structure for the records
|
||||
func createTables() error {
|
||||
for _, table := range tables {
|
||||
stmt, err := db.Prepare(strings.ReplaceAll(tableCreate, "table", table))
|
||||
for _, table := range Tables {
|
||||
stmt, err := DB.Prepare(strings.ReplaceAll(tableCreate, "table", table))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
@ -127,7 +129,7 @@ func NewTransaction(user *string, request configs.WsMessage) *Transaction {
|
||||
// Complete will put an entry into the statistics database for the transaction
|
||||
func (transaction *Transaction) Complete(errorMessageNotPresent bool) {
|
||||
if transaction.User != nil && transaction.Recordable {
|
||||
stmt, err := db.Prepare(strings.ReplaceAll(insert, "table", string(transaction.Table)))
|
||||
stmt, err := DB.Prepare(strings.ReplaceAll(insert, "table", string(transaction.Table)))
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
|
Loading…
Reference in New Issue
Block a user