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:
parent
78dcba614d
commit
881e99df53
68 changed files with 1799 additions and 17 deletions
|
@ -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
9
app/nomad/logs/index.ts
Normal 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
3
app/nomad/logs/logs.html
Normal 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
6
app/nomad/logs/logs.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import controller from './logsController';
|
||||
|
||||
export const logsView = {
|
||||
templateUrl: './logs.html',
|
||||
controller,
|
||||
};
|
85
app/nomad/logs/logsController.js
Normal file
85
app/nomad/logs/logsController.js
Normal 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();
|
||||
}
|
1
app/nomad/logs/nomad-log-viewer/index.ts
Normal file
1
app/nomad/logs/nomad-log-viewer/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { nomadLogViewer } from './nomad-log-viewer';
|
12
app/nomad/logs/nomad-log-viewer/nomad-log-viewer.js
Normal file
12
app/nomad/logs/nomad-log-viewer/nomad-log-viewer.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
import controller from './nomadLogViewerController';
|
||||
|
||||
export const nomadLogViewer = {
|
||||
templateUrl: './nomadLogViewer.html',
|
||||
controller,
|
||||
bindings: {
|
||||
stderrLog: '<',
|
||||
stdoutLog: '<',
|
||||
resourceName: '<',
|
||||
logCollectionChange: '<',
|
||||
},
|
||||
};
|
95
app/nomad/logs/nomad-log-viewer/nomadLogViewer.html
Normal file
95
app/nomad/logs/nomad-log-viewer/nomadLogViewer.html
Normal 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>
|
64
app/nomad/logs/nomad-log-viewer/nomadLogViewerController.js
Normal file
64
app/nomad/logs/nomad-log-viewer/nomadLogViewerController.js
Normal 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');
|
||||
};
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue