From 881e99df53476ea984fb4089a66c7def8e538720 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Sun, 13 Nov 2022 12:29:25 +0200 Subject: [PATCH] refactor(nomad): sync frontend with EE [EE-3353] (#7758) --- app/constants.js | 6 +- .../log-viewer/logViewerController.js | 3 +- .../helpers/logHelper/concatLogsToString.ts | 3 +- app/docker/helpers/logHelper/constants.ts | 3 + app/docker/helpers/logHelper/index.ts | 1 + app/nomad/index.ts | 121 ++++++++++- app/nomad/logs/index.ts | 9 + app/nomad/logs/logs.html | 3 + app/nomad/logs/logs.ts | 6 + app/nomad/logs/logsController.js | 85 ++++++++ app/nomad/logs/nomad-log-viewer/index.ts | 1 + .../logs/nomad-log-viewer/nomad-log-viewer.js | 12 ++ .../logs/nomad-log-viewer/nomadLogViewer.html | 95 ++++++++ .../nomadLogViewerController.js | 64 ++++++ app/react/constants.ts | 11 + app/react/nomad/.keep | 0 app/react/nomad/DashboardView/.keep | 0 .../nomad/DashboardView/DashboardView.tsx | 83 +++++++ .../nomad/DashboardView/RunningStatus.tsx | 25 +++ app/react/nomad/DashboardView/index.ts | 1 + app/react/nomad/DashboardView/useDashboard.ts | 41 ++++ app/react/nomad/jobs/.keep | 0 app/react/nomad/jobs/EventsView/.keep | 0 .../EventsDatatable/EventsDatatable.tsx | 163 ++++++++++++++ .../EventsDatatable/columns/date.tsx | 12 ++ .../EventsDatatable/columns/index.tsx | 9 + .../EventsDatatable/columns/message.tsx | 11 + .../EventsDatatable/columns/type.tsx | 11 + .../jobs/EventsView/EventsDatatable/index.ts | 1 + .../nomad/jobs/EventsView/EventsView.tsx | 62 ++++++ app/react/nomad/jobs/EventsView/index.ts | 1 + app/react/nomad/jobs/EventsView/useEvents.ts | 75 +++++++ app/react/nomad/jobs/JobsView/.keep | 0 .../JobsView/JobsDatatable/JobsDatatable.tsx | 204 ++++++++++++++++++ .../JobsDatatable/JobsDatatableSettings.tsx | 19 ++ .../TasksDatatable/TasksDatatable.tsx | 97 +++++++++ .../TasksDatatable/columns/actions.tsx | 45 ++++ .../TasksDatatable/columns/allocationID.tsx | 11 + .../TasksDatatable/columns/index.tsx | 15 ++ .../TasksDatatable/columns/started.tsx | 19 ++ .../TasksDatatable/columns/taskGroup.tsx | 11 + .../TasksDatatable/columns/taskName.tsx | 11 + .../TasksDatatable/columns/taskStatus.tsx | 35 +++ .../JobsDatatable/TasksDatatable/index.ts | 1 + .../JobsDatatable/actions/JobActions.tsx | 49 +++++ .../JobsView/JobsDatatable/actions/delete.tsx | 22 ++ .../JobsDatatable/columns/actions.tsx | 24 +++ .../JobsDatatable/columns/created.tsx | 13 ++ .../JobsView/JobsDatatable/columns/index.tsx | 11 + .../JobsView/JobsDatatable/columns/name.tsx | 24 +++ .../JobsDatatable/columns/namespace.tsx | 11 + .../JobsView/JobsDatatable/columns/status.tsx | 11 + .../jobs/JobsView/JobsDatatable/index.ts | 1 + .../jobs/JobsView/JobsDatatable/types.ts | 5 + app/react/nomad/jobs/JobsView/JobsView.tsx | 46 ++++ app/react/nomad/jobs/JobsView/index.ts | 1 + app/react/nomad/jobs/JobsView/useJobs.ts | 34 +++ app/react/nomad/jobs/jobs.service.ts | 16 ++ app/react/nomad/nomad.service.ts | 20 ++ app/react/nomad/types.ts | 25 +++ .../EnvironmentList/EnvironmentList.tsx | 22 +- app/react/portainer/environments/types.ts | 3 + .../environments/utils/get-platform-icon.ts | 2 + .../portainer/environments/utils/index.ts | 12 +- app/react/sidebar/EnvironmentSidebar.tsx | 9 +- .../NomadSidebar/NomadSidebar.test.tsx | 38 ++++ .../sidebar/NomadSidebar/NomadSidebar.tsx | 30 +++ app/react/sidebar/NomadSidebar/index.ts | 1 + 68 files changed, 1799 insertions(+), 17 deletions(-) create mode 100644 app/docker/helpers/logHelper/constants.ts create mode 100644 app/nomad/logs/index.ts create mode 100644 app/nomad/logs/logs.html create mode 100644 app/nomad/logs/logs.ts create mode 100644 app/nomad/logs/logsController.js create mode 100644 app/nomad/logs/nomad-log-viewer/index.ts create mode 100644 app/nomad/logs/nomad-log-viewer/nomad-log-viewer.js create mode 100644 app/nomad/logs/nomad-log-viewer/nomadLogViewer.html create mode 100644 app/nomad/logs/nomad-log-viewer/nomadLogViewerController.js create mode 100644 app/react/constants.ts delete mode 100644 app/react/nomad/.keep delete mode 100644 app/react/nomad/DashboardView/.keep create mode 100644 app/react/nomad/DashboardView/DashboardView.tsx create mode 100644 app/react/nomad/DashboardView/RunningStatus.tsx create mode 100644 app/react/nomad/DashboardView/index.ts create mode 100644 app/react/nomad/DashboardView/useDashboard.ts delete mode 100644 app/react/nomad/jobs/.keep delete mode 100644 app/react/nomad/jobs/EventsView/.keep create mode 100644 app/react/nomad/jobs/EventsView/EventsDatatable/EventsDatatable.tsx create mode 100644 app/react/nomad/jobs/EventsView/EventsDatatable/columns/date.tsx create mode 100644 app/react/nomad/jobs/EventsView/EventsDatatable/columns/index.tsx create mode 100644 app/react/nomad/jobs/EventsView/EventsDatatable/columns/message.tsx create mode 100644 app/react/nomad/jobs/EventsView/EventsDatatable/columns/type.tsx create mode 100644 app/react/nomad/jobs/EventsView/EventsDatatable/index.ts create mode 100644 app/react/nomad/jobs/EventsView/EventsView.tsx create mode 100644 app/react/nomad/jobs/EventsView/index.ts create mode 100644 app/react/nomad/jobs/EventsView/useEvents.ts delete mode 100644 app/react/nomad/jobs/JobsView/.keep create mode 100644 app/react/nomad/jobs/JobsView/JobsDatatable/JobsDatatable.tsx create mode 100644 app/react/nomad/jobs/JobsView/JobsDatatable/JobsDatatableSettings.tsx create mode 100644 app/react/nomad/jobs/JobsView/JobsDatatable/TasksDatatable/TasksDatatable.tsx create mode 100644 app/react/nomad/jobs/JobsView/JobsDatatable/TasksDatatable/columns/actions.tsx create mode 100644 app/react/nomad/jobs/JobsView/JobsDatatable/TasksDatatable/columns/allocationID.tsx create mode 100644 app/react/nomad/jobs/JobsView/JobsDatatable/TasksDatatable/columns/index.tsx create mode 100644 app/react/nomad/jobs/JobsView/JobsDatatable/TasksDatatable/columns/started.tsx create mode 100644 app/react/nomad/jobs/JobsView/JobsDatatable/TasksDatatable/columns/taskGroup.tsx create mode 100644 app/react/nomad/jobs/JobsView/JobsDatatable/TasksDatatable/columns/taskName.tsx create mode 100644 app/react/nomad/jobs/JobsView/JobsDatatable/TasksDatatable/columns/taskStatus.tsx create mode 100644 app/react/nomad/jobs/JobsView/JobsDatatable/TasksDatatable/index.ts create mode 100644 app/react/nomad/jobs/JobsView/JobsDatatable/actions/JobActions.tsx create mode 100644 app/react/nomad/jobs/JobsView/JobsDatatable/actions/delete.tsx create mode 100644 app/react/nomad/jobs/JobsView/JobsDatatable/columns/actions.tsx create mode 100644 app/react/nomad/jobs/JobsView/JobsDatatable/columns/created.tsx create mode 100644 app/react/nomad/jobs/JobsView/JobsDatatable/columns/index.tsx create mode 100644 app/react/nomad/jobs/JobsView/JobsDatatable/columns/name.tsx create mode 100644 app/react/nomad/jobs/JobsView/JobsDatatable/columns/namespace.tsx create mode 100644 app/react/nomad/jobs/JobsView/JobsDatatable/columns/status.tsx create mode 100644 app/react/nomad/jobs/JobsView/JobsDatatable/index.ts create mode 100644 app/react/nomad/jobs/JobsView/JobsDatatable/types.ts create mode 100644 app/react/nomad/jobs/JobsView/JobsView.tsx create mode 100644 app/react/nomad/jobs/JobsView/index.ts create mode 100644 app/react/nomad/jobs/JobsView/useJobs.ts create mode 100644 app/react/nomad/jobs/jobs.service.ts create mode 100644 app/react/nomad/nomad.service.ts create mode 100644 app/react/nomad/types.ts create mode 100644 app/react/sidebar/NomadSidebar/NomadSidebar.test.tsx create mode 100644 app/react/sidebar/NomadSidebar/NomadSidebar.tsx create mode 100644 app/react/sidebar/NomadSidebar/index.ts diff --git a/app/constants.js b/app/constants.js index 5212ba524..2d26ba851 100644 --- a/app/constants.js +++ b/app/constants.js @@ -1,3 +1,5 @@ +import { BROWSER_OS_PLATFORM } from './react/constants'; + export const API_ENDPOINT_AUTH = 'api/auth'; export const API_ENDPOINT_BACKUP = 'api/backup'; export const API_ENDPOINT_CUSTOM_TEMPLATES = 'api/custom_templates'; @@ -21,7 +23,6 @@ export const API_ENDPOINT_TEAMS = 'api/teams'; export const API_ENDPOINT_TEAM_MEMBERSHIPS = 'api/team_memberships'; export const API_ENDPOINT_TEMPLATES = 'api/templates'; export const API_ENDPOINT_WEBHOOKS = 'api/webhooks'; -export const DEFAULT_TEMPLATES_URL = 'https://raw.githubusercontent.com/portainer/templates/master/templates.json'; export const PAGINATION_MAX_ITEMS = 10; export const APPLICATION_CACHE_VALIDITY = 3600; export const CONSOLE_COMMANDS_LABEL_PREFIX = 'io.portainer.commands.'; @@ -31,8 +32,6 @@ export const KUBERNETES_SYSTEM_NAMESPACES = ['kube-system', 'kube-public', 'kube export const PORTAINER_FADEOUT = 1500; export const STACK_NAME_VALIDATION_REGEX = '^[-_a-z0-9]+$'; export const TEMPLATE_NAME_VALIDATION_REGEX = '^[-_a-z0-9]+$'; -export const BROWSER_OS_PLATFORM = navigator.userAgent.indexOf('Windows') > -1 ? 'win' : navigator.userAgent.indexOf('Mac') > -1 ? 'mac' : 'lin'; -export const NEW_LINE_BREAKER = BROWSER_OS_PLATFORM === 'win' ? '\r\n' : '\n'; // don't declare new constants, either: // - if only used in one file or module, declare in that file or module (as a regular js constant) @@ -62,7 +61,6 @@ angular .constant('API_ENDPOINT_TEAM_MEMBERSHIPS', API_ENDPOINT_TEAM_MEMBERSHIPS) .constant('API_ENDPOINT_TEMPLATES', API_ENDPOINT_TEMPLATES) .constant('API_ENDPOINT_WEBHOOKS', API_ENDPOINT_WEBHOOKS) - .constant('DEFAULT_TEMPLATES_URL', DEFAULT_TEMPLATES_URL) .constant('PAGINATION_MAX_ITEMS', PAGINATION_MAX_ITEMS) .constant('APPLICATION_CACHE_VALIDITY', APPLICATION_CACHE_VALIDITY) .constant('CONSOLE_COMMANDS_LABEL_PREFIX', CONSOLE_COMMANDS_LABEL_PREFIX) diff --git a/app/docker/components/log-viewer/logViewerController.js b/app/docker/components/log-viewer/logViewerController.js index 07b47267e..632187e47 100644 --- a/app/docker/components/log-viewer/logViewerController.js +++ b/app/docker/components/log-viewer/logViewerController.js @@ -1,7 +1,6 @@ import moment from 'moment'; -import { NEW_LINE_BREAKER } from '@/constants'; -import { concatLogsToString } from '@/docker/helpers/logHelper'; +import { concatLogsToString, NEW_LINE_BREAKER } from '@/docker/helpers/logHelper'; angular.module('portainer.docker').controller('LogViewerController', [ '$scope', diff --git a/app/docker/helpers/logHelper/concatLogsToString.ts b/app/docker/helpers/logHelper/concatLogsToString.ts index b67d4312e..164089536 100644 --- a/app/docker/helpers/logHelper/concatLogsToString.ts +++ b/app/docker/helpers/logHelper/concatLogsToString.ts @@ -1,5 +1,4 @@ -import { NEW_LINE_BREAKER } from '@/constants'; - +import { NEW_LINE_BREAKER } from './constants'; import { FormattedLine } from './types'; type FormatFunc = (line: FormattedLine) => string; diff --git a/app/docker/helpers/logHelper/constants.ts b/app/docker/helpers/logHelper/constants.ts new file mode 100644 index 000000000..c0e22d21a --- /dev/null +++ b/app/docker/helpers/logHelper/constants.ts @@ -0,0 +1,3 @@ +import { BROWSER_OS_PLATFORM } from '@/react/constants'; + +export const NEW_LINE_BREAKER = BROWSER_OS_PLATFORM === 'win' ? '\r\n' : '\n'; diff --git a/app/docker/helpers/logHelper/index.ts b/app/docker/helpers/logHelper/index.ts index 91fe97ed7..9b9bfe195 100644 --- a/app/docker/helpers/logHelper/index.ts +++ b/app/docker/helpers/logHelper/index.ts @@ -1,2 +1,3 @@ export { formatLogs } from './formatLogs'; export { concatLogsToString } from './concatLogsToString'; +export { NEW_LINE_BREAKER } from './constants'; diff --git a/app/nomad/index.ts b/app/nomad/index.ts index 7f5d76abd..09037dc3f 100644 --- a/app/nomad/index.ts +++ b/app/nomad/index.ts @@ -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) => Promise, + $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); +} diff --git a/app/nomad/logs/index.ts b/app/nomad/logs/index.ts new file mode 100644 index 000000000..9dc9afd95 --- /dev/null +++ b/app/nomad/logs/index.ts @@ -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; diff --git a/app/nomad/logs/logs.html b/app/nomad/logs/logs.html new file mode 100644 index 000000000..a33545891 --- /dev/null +++ b/app/nomad/logs/logs.html @@ -0,0 +1,3 @@ + + + diff --git a/app/nomad/logs/logs.ts b/app/nomad/logs/logs.ts new file mode 100644 index 000000000..4824024aa --- /dev/null +++ b/app/nomad/logs/logs.ts @@ -0,0 +1,6 @@ +import controller from './logsController'; + +export const logsView = { + templateUrl: './logs.html', + controller, +}; diff --git a/app/nomad/logs/logsController.js b/app/nomad/logs/logsController.js new file mode 100644 index 000000000..4975843be --- /dev/null +++ b/app/nomad/logs/logsController.js @@ -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(); +} diff --git a/app/nomad/logs/nomad-log-viewer/index.ts b/app/nomad/logs/nomad-log-viewer/index.ts new file mode 100644 index 000000000..df2a5ab97 --- /dev/null +++ b/app/nomad/logs/nomad-log-viewer/index.ts @@ -0,0 +1 @@ +export { nomadLogViewer } from './nomad-log-viewer'; diff --git a/app/nomad/logs/nomad-log-viewer/nomad-log-viewer.js b/app/nomad/logs/nomad-log-viewer/nomad-log-viewer.js new file mode 100644 index 000000000..df4621b0e --- /dev/null +++ b/app/nomad/logs/nomad-log-viewer/nomad-log-viewer.js @@ -0,0 +1,12 @@ +import controller from './nomadLogViewerController'; + +export const nomadLogViewer = { + templateUrl: './nomadLogViewer.html', + controller, + bindings: { + stderrLog: '<', + stdoutLog: '<', + resourceName: '<', + logCollectionChange: '<', + }, +}; diff --git a/app/nomad/logs/nomad-log-viewer/nomadLogViewer.html b/app/nomad/logs/nomad-log-viewer/nomadLogViewer.html new file mode 100644 index 000000000..8937213f4 --- /dev/null +++ b/app/nomad/logs/nomad-log-viewer/nomadLogViewer.html @@ -0,0 +1,95 @@ +
+
+ + + +
+
+ +
+
+
+ + +
+
+
+
+
+
+ +
+
+ +
+
+
+ +
+ +
+
+
+
+ +
+
+ +
+
+
+ +
+ + + + + + + +
+
+
+
+
+
+
+ +
+
+
+      

No logs available

+

{{ span.text }}

+

No log line matching the '{{ $ctrl.state.search }}' filter

+
+
+ +
+
+      

No logs available

+

{{ span.text }}

+

No log line matching the '{{ $ctrl.state.search }}' filter

+
+
+
diff --git a/app/nomad/logs/nomad-log-viewer/nomadLogViewerController.js b/app/nomad/logs/nomad-log-viewer/nomadLogViewerController.js new file mode 100644 index 000000000..4191a13bf --- /dev/null +++ b/app/nomad/logs/nomad-log-viewer/nomadLogViewerController.js @@ -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'); + }; +} diff --git a/app/react/constants.ts b/app/react/constants.ts new file mode 100644 index 000000000..a2a191a11 --- /dev/null +++ b/app/react/constants.ts @@ -0,0 +1,11 @@ +export const BROWSER_OS_PLATFORM = getOs(); + +function getOs() { + const { userAgent } = navigator; + + if (userAgent.includes('Windows')) { + return 'win'; + } + + return userAgent.includes('Mac') ? 'mac' : 'lin'; +} diff --git a/app/react/nomad/.keep b/app/react/nomad/.keep deleted file mode 100644 index e69de29bb..000000000 diff --git a/app/react/nomad/DashboardView/.keep b/app/react/nomad/DashboardView/.keep deleted file mode 100644 index e69de29bb..000000000 diff --git a/app/react/nomad/DashboardView/DashboardView.tsx b/app/react/nomad/DashboardView/DashboardView.tsx new file mode 100644 index 000000000..5255ef8b3 --- /dev/null +++ b/app/react/nomad/DashboardView/DashboardView.tsx @@ -0,0 +1,83 @@ +import { useEnvironmentId } from '@/react/hooks/useEnvironmentId'; + +import { DashboardItem } from '@@/DashboardItem'; +import { Widget, WidgetTitle, WidgetBody } from '@@/Widget'; +import { PageHeader } from '@@/PageHeader'; +import { DashboardGrid } from '@@/DashboardItem/DashboardGrid'; + +import { useDashboard } from './useDashboard'; +import { RunningStatus } from './RunningStatus'; + +export function DashboardView() { + const environmentId = useEnvironmentId(); + const dashboardQuery = useDashboard(environmentId); + + const running = dashboardQuery.data?.RunningTaskCount || 0; + const stopped = (dashboardQuery.data?.TaskCount || 0) - running; + + return ( + <> + + + {dashboardQuery.isLoading ? ( +
+ Connecting to the Edge environment... + +
+ ) : ( + <> +
+
+ {/* cluster info */} + + + + + + + + + + +
Nodes in the cluster{dashboardQuery.data?.NodeCount ?? '-'}
+
+
+
+
+ +
+ + {/* jobs */} + + {/* groups */} + + {/* tasks */} + + {/* running status of tasks */} + + + +
+ + )} + + ); +} diff --git a/app/react/nomad/DashboardView/RunningStatus.tsx b/app/react/nomad/DashboardView/RunningStatus.tsx new file mode 100644 index 000000000..ebb1a6195 --- /dev/null +++ b/app/react/nomad/DashboardView/RunningStatus.tsx @@ -0,0 +1,25 @@ +interface Props { + running: number; + stopped: number; +} + +export function RunningStatus({ running, stopped }: Props) { + return ( +
+
+