1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-03 04:45:21 +02:00

refactor(nomad): sync frontend with EE [EE-3353] (#7758)

This commit is contained in:
Chaim Lev-Ari 2022-11-13 12:29:25 +02:00 committed by GitHub
parent 78dcba614d
commit 881e99df53
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
68 changed files with 1799 additions and 17 deletions

View file

@ -1,8 +1,121 @@
import angular from 'angular';
import { StateRegistry, StateService } from '@uirouter/angularjs';
import { isNomadEnvironment } from '@/react/portainer/environments/utils';
import { DashboardView } from '@/react/nomad/DashboardView';
import { r2a } from '@/react-tools/react2angular';
import { EventsView } from '@/react/nomad/jobs/EventsView';
import { withUIRouter } from '@/react-tools/withUIRouter';
import { withReactQuery } from '@/react-tools/withReactQuery';
import { withCurrentUser } from '@/react-tools/withCurrentUser';
import { JobsView } from '@/react/nomad/jobs/JobsView';
import { getLeader } from '@/react/nomad/nomad.service';
import { Environment } from '@/react/portainer/environments/types';
import { StateManager } from '@/portainer/services/types';
import { notifyError } from '@/portainer/services/notifications';
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
import { reactModule } from './react';
import { logsModule } from './logs';
export const nomadModule = angular.module('portainer.nomad', [
'portainer.app',
reactModule,
]).name;
export const nomadModule = angular
.module('portainer.nomad', [reactModule, logsModule])
.config(config)
.component(
'nomadDashboardView',
r2a(withUIRouter(withReactQuery(withCurrentUser(DashboardView))), [])
)
.component(
'nomadEventsView',
r2a(withUIRouter(withReactQuery(withCurrentUser(EventsView))), [])
)
.component(
'nomadJobsView',
r2a(withUIRouter(withReactQuery(withCurrentUser(JobsView))), [])
).name;
/* @ngInject */
function config($stateRegistryProvider: StateRegistry) {
// limits module to BE only
if (!isBE) {
return;
}
const nomad = {
name: 'nomad',
url: '/nomad',
parent: 'endpoint',
abstract: true,
onEnter: /* @ngInject */ function onEnter(
$async: (fn: () => Promise<void>) => Promise<void>,
$state: StateService,
endpoint: Environment,
StateManager: StateManager
) {
return $async(async () => {
if (!isNomadEnvironment(endpoint.Type)) {
$state.go('portainer.home');
return;
}
try {
await getLeader(endpoint.Id);
await StateManager.updateEndpointState(endpoint);
} catch (e) {
notifyError(
'Unable to contact Edge agent, please ensure that the agent is properly running on the remote environment.'
);
$state.go('portainer.home', {}, { reload: true });
}
});
},
};
const dashboard = {
name: 'nomad.dashboard',
url: '/dashboard',
views: {
'content@': {
component: 'nomadDashboardView',
},
},
};
const jobs = {
name: 'nomad.jobs',
url: '/jobs',
views: {
'content@': {
component: 'nomadJobsView',
},
},
};
const events = {
name: 'nomad.events',
url: '/jobs/:jobID/tasks/:taskName/allocations/:allocationID/events?namespace',
views: {
'content@': {
component: 'nomadEventsView',
},
},
};
const logs = {
name: 'nomad.logs',
url: '/jobs/:jobID/tasks/:taskName/allocations/:allocationID/logs?namespace',
views: {
'content@': {
component: 'nomadLogsView',
},
},
};
$stateRegistryProvider.register(nomad);
$stateRegistryProvider.register(dashboard);
$stateRegistryProvider.register(jobs);
$stateRegistryProvider.register(events);
$stateRegistryProvider.register(logs);
}

9
app/nomad/logs/index.ts Normal file
View file

@ -0,0 +1,9 @@
import angular from 'angular';
import { logsView } from './logs';
import { nomadLogViewer } from './nomad-log-viewer';
export const logsModule = angular
.module('portainer.app.nomad.logs', [])
.component('nomadLogViewer', nomadLogViewer)
.component('nomadLogsView', logsView).name;

3
app/nomad/logs/logs.html Normal file
View file

@ -0,0 +1,3 @@
<page-header title="'Task logs'" breadcrumbs="[{label:'Nomad Jobs', link:'nomad.jobs'}, jobID, taskName, 'Logs']"> </page-header>
<nomad-log-viewer stderr-log="stderrLog" stdout-log="stdoutLog" resource-name="taskName" log-collection-change="changeLogCollection"></nomad-log-viewer>

6
app/nomad/logs/logs.ts Normal file
View file

@ -0,0 +1,6 @@
import controller from './logsController';
export const logsView = {
templateUrl: './logs.html',
controller,
};

View file

@ -0,0 +1,85 @@
import axios from '@/portainer/services/axios';
/* @ngInject */
export default function LogsController($scope, $async, $state, Notifications) {
let controller = new AbortController();
$scope.stderrLog = [];
$scope.stdoutLog = [];
$scope.changeLogCollection = function (logCollectionStatus) {
if (!logCollectionStatus) {
controller.abort();
controller = new AbortController();
} else {
loadLogs('stderr', $scope.jobID, $scope.taskName, $scope.namespace, $scope.endpointId, controller);
loadLogs('stdout', $scope.jobID, $scope.taskName, $scope.namespace, $scope.endpointId, controller);
}
};
function stripEscapeCodes(logs) {
return logs.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
}
function formatLogs(logs, splitter = '\\n') {
if (!logs) {
return [];
}
const formattedLogs = [];
const logInLines = logs.trim().split(splitter);
for (const logInLine of logInLines) {
const line = stripEscapeCodes(logInLine).replace('\n', '').replace(/[""]+/g, '');
formattedLogs.push({ line, spans: [{ foregroundColor: null, backgroundColor: null, text: line }] });
}
return formattedLogs;
}
async function loadLogs(logType, jobID, taskName, namespace, endpointId, controller, refresh = true, offset = 50000) {
axios
.get(`/nomad/endpoints/${endpointId}/allocation/${$scope.allocationID}/logs`, {
params: {
jobID,
taskName,
namespace,
refresh,
logType,
offset,
},
signal: controller.signal,
onDownloadProgress: (progressEvent) => {
$scope[`${logType}Log`] = formatLogs(progressEvent.currentTarget.response);
$scope.$apply();
},
})
.then((response) => {
$scope[`${logType}Log`] = formatLogs(response.data, '\n');
$scope.$apply();
})
.catch((err) => {
if (err.message !== 'canceled') Notifications.error('Failure', err, 'Unable to retrieve task logs');
});
}
async function initView() {
return $async(async () => {
$scope.jobID = $state.params.jobID;
$scope.taskName = $state.params.taskName;
$scope.allocationID = $state.params.allocationID;
$scope.namespace = $state.params.namespace;
$scope.endpointId = $state.params.endpointId;
loadLogs('stderr', $scope.jobID, $scope.taskName, $scope.namespace, $scope.endpointId, controller);
loadLogs('stdout', $scope.jobID, $scope.taskName, $scope.namespace, $scope.endpointId, controller);
});
}
$scope.$on('$destroy', function () {
if (controller) {
controller.abort();
}
});
initView();
}

View file

@ -0,0 +1 @@
export { nomadLogViewer } from './nomad-log-viewer';

View file

@ -0,0 +1,12 @@
import controller from './nomadLogViewerController';
export const nomadLogViewer = {
templateUrl: './nomadLogViewer.html',
controller,
bindings: {
stderrLog: '<',
stdoutLog: '<',
resourceName: '<',
logCollectionChange: '<',
},
};

View file

@ -0,0 +1,95 @@
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-header icon="fa-file-alt" title-text="Nomad Log viewer settings"></rd-widget-header>
<rd-widget-body>
<form class="form-horizontal">
<div class="form-group">
<label for="repository_mechanism" class="col-sm-1 control-label text-left"> Log type </label>
<div class="col-sm-11">
<div class="input-group col-sm-10 input-group-sm">
<div class="btn-group btn-group-sm">
<label class="btn btn-primary" ng-click="$ctrl.onChangeLogType($ctrl.model.logType)" ng-model="$ctrl.model.logType" uib-btn-radio="'stderr'">stderr</label>
<label class="btn btn-primary" ng-click="$ctrl.onChangeLogType($ctrl.model.logType)" ng-model="$ctrl.model.logType" uib-btn-radio="'stdout'">stdout</label>
</div>
</div>
</div>
</div>
<div class="form-group">
<div class="col-sm-1">
<label for="tls" class="control-label text-left">
Auto-refresh
<portainer-tooltip message="'Disabling this option allows you to pause the log collection process and the auto-scrolling.'"></portainer-tooltip>
</label>
</div>
<div class="col-sm-11">
<label class="switch">
<input
type="checkbox"
ng-model="$ctrl.state.logCollection"
ng-change="$ctrl.state.autoScroll = $ctrl.state.logCollection; $ctrl.logCollectionChange($ctrl.state.logCollection)"
/><i></i>
</label>
</div>
</div>
<div class="form-group">
<label for="logs_search" class="col-sm-1 control-label text-left"> Search </label>
<div class="col-sm-11">
<input class="form-control" type="text" name="logs_search" ng-model="$ctrl.state.search" ng-change="$ctrl.state.selectedLines.length = 0;" placeholder="Filter..." />
</div>
</div>
<div class="form-group">
<div class="col-sm-1">
<label for="tls" class="control-label text-left"> Wrap lines </label>
</div>
<div class="col-sm-11">
<label class="switch"> <input type="checkbox" ng-model="$ctrl.state.wrapLines" /><i></i> </label>
</div>
</div>
<div class="form-group" ng-if="$ctrl.state.copySupported">
<label class="col-sm-1 control-label text-left"> Actions </label>
<div class="col-sm-11">
<button class="btn btn-primary btn-sm" type="button" ng-click="$ctrl.downloadLogs()" style="margin-left: 0"><i class="fa fa-download"></i> Download logs</button>
<button
class="btn btn-primary btn-sm"
ng-click="$ctrl.copy()"
ng-disabled="($ctrl.state[$ctrl.model.logType].filteredLogs.length === 1 && !$ctrl.state[$ctrl.model.logType].filteredLogs[0].line) || !$ctrl.state[$ctrl.model.logType].filteredLogs.length"
><i class="fa fa-copy space-right" aria-hidden="true"></i>Copy</button
>
<button
class="btn btn-primary btn-sm"
ng-click="$ctrl.copySelection()"
ng-disabled="($ctrl.state[$ctrl.model.logType].filteredLogs.length === 1 && !$ctrl.state[$ctrl.model.logType].filteredLogs[0].line) || !$ctrl.state[$ctrl.model.logType].filteredLogs.length || !$ctrl.state[$ctrl.model.logType].selectedLines.length"
><i class="fa fa-copy space-right" aria-hidden="true"></i>Copy selected lines</button
>
<button class="btn btn-primary btn-sm" ng-click="$ctrl.clearSelection()" ng-disabled="$ctrl.state[$ctrl.model.logType].selectedLines.length === 0"
><i class="fa fa-times space-right" aria-hidden="true"></i>Unselect</button
>
<span>
<i id="refreshRateChange" class="fa fa-check green-icon" aria-hidden="true" style="margin-left: 7px; display: none"></i>
</span>
</div>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" style="height: 54%">
<div class="col-sm-12" style="height: 100%" ng-if="$ctrl.model.logType === $ctrl.NomadLogType.STDERR">
<pre ng-class="{ wrap_lines: $ctrl.state.wrapLines }" class="log_viewer" scroll-glue="$ctrl.state.autoScroll" force-glue>
<div ng-if="$ctrl.stderrLog.length === 0 && $ctrl.state.stderr.filteredLogs.length === 0 && !$ctrl.state.logCollection" class="line"><p class="inner_line">No logs available</p></div>
<div ng-repeat="log in $ctrl.state.stderr.filteredLogs = ($ctrl.stderrLog | filter:{ 'line': $ctrl.state.search }) track by $index" class="line" ng-if="log.line"><p class="inner_line" ng-click="$ctrl.selectLine(log.line)" ng-class="{ 'line_selected': $ctrl.state.stderr.selectedLines.indexOf(log.line) > -1 }"><span ng-repeat="span in log.spans" ng-style="{ 'color': span.foregroundColor, 'background-color': span.backgroundColor }">{{ span.text }}</span></p></div>
<div ng-if="$ctrl.stderrLog.length !== 0 && !$ctrl.state.stderr.filteredLogs.length && $ctrl.state.search" class="line"><p class="inner_line">No log line matching the '{{ $ctrl.state.search }}' filter</p></div>
</pre>
</div>
<div class="col-sm-12" style="height: 100%" ng-if="$ctrl.model.logType === $ctrl.NomadLogType.STDOUT">
<pre ng-class="{ wrap_lines: $ctrl.state.wrapLines }" class="log_viewer" scroll-glue="$ctrl.state.autoScroll" force-glue>
<div ng-if="$ctrl.stdoutLog.length === 0 && $ctrl.state.stdout.filteredLogs.length === 0 && !$ctrl.state.logCollection" class="line"><p class="inner_line">No logs available</p></div>
<div ng-repeat="log in $ctrl.state.stdout.filteredLogs = ($ctrl.stdoutLog | filter:{ 'line': $ctrl.state.search }) track by $index" class="line" ng-if="log.line"><p class="inner_line" ng-click="$ctrl.selectLine(log.line)" ng-class="{ 'line_selected': $ctrl.state.stdout.selectedLines.indexOf(log.line) > -1 }"><span ng-repeat="span in log.spans" ng-style="{ 'color': span.foregroundColor, 'background-color': span.backgroundColor }">{{ span.text }}</span></p></div>
<div ng-if="$ctrl.stdoutLog.length !== 0 && !$ctrl.state.stdout.filteredLogs.length && $ctrl.state.search" class="line"><p class="inner_line">No log line matching the '{{ $ctrl.state.search }}' filter</p></div>
</pre>
</div>
</div>

View file

@ -0,0 +1,64 @@
import { concatLogsToString, NEW_LINE_BREAKER } from '@/docker/helpers/logHelper';
/* @ngInject */
export default function NomadLogViewerController(clipboard, Blob, FileSaver) {
this.NomadLogType = Object.freeze({
STDERR: 'stderr',
STDOUT: 'stdout',
});
this.state = {
copySupported: clipboard.supported,
logCollection: true,
autoScroll: true,
wrapLines: true,
search: '',
stderr: {
filteredLogs: [],
selectedLines: [],
},
stdout: {
filteredLogs: [],
selectedLines: [],
},
};
this.model = {
logType: this.NomadLogType.STDERR,
};
this.onChangeLogType = function (logType) {
this.model.logType = this.NomadLogType[logType.toUpperCase()];
};
this.copy = function () {
clipboard.copyText(this.state[this.model.logType].filteredLogs.map((log) => log.line).join(NEW_LINE_BREAKER));
$('#refreshRateChange').show();
$('#refreshRateChange').fadeOut(2000);
};
this.copySelection = function () {
clipboard.copyText(this.state[this.model.logType].selectedLines.join(NEW_LINE_BREAKER));
$('#refreshRateChange').show();
$('#refreshRateChange').fadeOut(2000);
};
this.clearSelection = function () {
this.state[this.model.logType].selectedLines = [];
};
this.selectLine = function (line) {
var idx = this.state[this.model.logType].selectedLines.indexOf(line);
if (idx === -1) {
this.state[this.model.logType].selectedLines.push(line);
} else {
this.state[this.model.logType].selectedLines.splice(idx, 1);
}
};
this.downloadLogs = function () {
const logsAsString = concatLogsToString(this.state[this.model.logType].filteredLogs);
const data = new Blob([logsAsString]);
FileSaver.saveAs(data, this.resourceName + '_logs.txt');
};
}