mirror of https://github.com/portainer/portainer
refactor(docker/containers): migrate inspect view to react [EE-2190] (#11005)
parent
de473fc10e
commit
2100155ab5
|
@ -2,7 +2,6 @@ import 'bootstrap/dist/css/bootstrap.css';
|
||||||
import 'toastr/build/toastr.css';
|
import 'toastr/build/toastr.css';
|
||||||
import 'xterm/dist/xterm.css';
|
import 'xterm/dist/xterm.css';
|
||||||
import 'angularjs-slider/dist/rzslider.css';
|
import 'angularjs-slider/dist/rzslider.css';
|
||||||
import 'angular-json-tree/dist/angular-json-tree.css';
|
|
||||||
import 'angular-loading-bar/build/loading-bar.css';
|
import 'angular-loading-bar/build/loading-bar.css';
|
||||||
import 'angular-moment-picker/dist/angular-moment-picker.min.css';
|
import 'angular-moment-picker/dist/angular-moment-picker.min.css';
|
||||||
import 'spinkit/spinkit.min.css';
|
import 'spinkit/spinkit.min.css';
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import _ from 'lodash-es';
|
import _ from 'lodash-es';
|
||||||
import { hideShaSum, joinCommand, nodeStatusBadge, taskStatusBadge, trimSHA, trimVersionTag } from './utils';
|
import { hideShaSum, joinCommand, nodeStatusBadge, taskStatusBadge, trimContainerName, trimSHA, trimVersionTag } from './utils';
|
||||||
|
|
||||||
function includeString(text, values) {
|
function includeString(text, values) {
|
||||||
return values.some(function (val) {
|
return values.some(function (val) {
|
||||||
|
@ -76,15 +76,7 @@ angular
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter('nodestatusbadge', () => nodeStatusBadge)
|
.filter('nodestatusbadge', () => nodeStatusBadge)
|
||||||
.filter('trimcontainername', function () {
|
.filter('trimcontainername', () => trimContainerName)
|
||||||
'use strict';
|
|
||||||
return function (name) {
|
|
||||||
if (name) {
|
|
||||||
return name.indexOf('/') === 0 ? name.slice(1) : name;
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.filter('getstatetext', function () {
|
.filter('getstatetext', function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
return function (state) {
|
return function (state) {
|
||||||
|
|
|
@ -83,3 +83,10 @@ export function nodeStatusBadge(text: NodeStatus['State']) {
|
||||||
export function hideShaSum(imageName = '') {
|
export function hideShaSum(imageName = '') {
|
||||||
return imageName.split('@sha')[0];
|
return imageName.split('@sha')[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function trimContainerName(name?: string) {
|
||||||
|
if (name) {
|
||||||
|
return name.indexOf('/') === 0 ? name.slice(1) : name;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { withReactQuery } from '@/react-tools/withReactQuery';
|
||||||
import { withUIRouter } from '@/react-tools/withUIRouter';
|
import { withUIRouter } from '@/react-tools/withUIRouter';
|
||||||
import { LogView } from '@/react/docker/containers/LogView';
|
import { LogView } from '@/react/docker/containers/LogView';
|
||||||
import { CreateView } from '@/react/docker/containers/CreateView';
|
import { CreateView } from '@/react/docker/containers/CreateView';
|
||||||
|
import { InspectView } from '@/react/docker/containers/InspectView/InspectView';
|
||||||
|
|
||||||
export const containersModule = angular
|
export const containersModule = angular
|
||||||
.module('portainer.docker.react.views.containers', [])
|
.module('portainer.docker.react.views.containers', [])
|
||||||
|
@ -26,7 +27,10 @@ export const containersModule = angular
|
||||||
'containerLogView',
|
'containerLogView',
|
||||||
r2a(withUIRouter(withReactQuery(withCurrentUser(LogView))), [])
|
r2a(withUIRouter(withReactQuery(withCurrentUser(LogView))), [])
|
||||||
)
|
)
|
||||||
|
.component(
|
||||||
|
'dockerContainerInspectView',
|
||||||
|
r2a(withUIRouter(withReactQuery(withCurrentUser(InspectView))), [])
|
||||||
|
)
|
||||||
.config(config).name;
|
.config(config).name;
|
||||||
|
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
|
@ -95,8 +99,7 @@ function config($stateRegistryProvider: StateRegistry) {
|
||||||
url: '/inspect',
|
url: '/inspect',
|
||||||
views: {
|
views: {
|
||||||
'content@': {
|
'content@': {
|
||||||
templateUrl: '~@/docker/views/containers/inspect/containerinspect.html',
|
component: 'dockerContainerInspectView',
|
||||||
controller: 'ContainerInspectController',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
angular.module('portainer.docker').controller('ContainerInspectController', [
|
|
||||||
'$scope',
|
|
||||||
'$transition$',
|
|
||||||
'Notifications',
|
|
||||||
'ContainerService',
|
|
||||||
'HttpRequestHelper',
|
|
||||||
'endpoint',
|
|
||||||
function ($scope, $transition$, Notifications, ContainerService, HttpRequestHelper, endpoint) {
|
|
||||||
$scope.state = {
|
|
||||||
DisplayTextView: false,
|
|
||||||
};
|
|
||||||
$scope.containerInfo = {};
|
|
||||||
|
|
||||||
function initView() {
|
|
||||||
HttpRequestHelper.setPortainerAgentTargetHeader($transition$.params().nodeName);
|
|
||||||
ContainerService.inspect(endpoint.Id, $transition$.params().id)
|
|
||||||
.then(function success(d) {
|
|
||||||
$scope.containerInfo = d;
|
|
||||||
})
|
|
||||||
.catch(function error(e) {
|
|
||||||
Notifications.error('Failure', e, 'Unable to inspect container');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
initView();
|
|
||||||
},
|
|
||||||
]);
|
|
|
@ -1,28 +0,0 @@
|
||||||
<page-header
|
|
||||||
title="'Container inspect'"
|
|
||||||
breadcrumbs="[
|
|
||||||
{ label:'Containers', link:'docker.containers' },
|
|
||||||
{
|
|
||||||
label:(containerInfo.Name | trimcontainername),
|
|
||||||
link: 'docker.containers.container',
|
|
||||||
linkParams: { id: containerInfo.Id },
|
|
||||||
}, 'Inspect']"
|
|
||||||
>
|
|
||||||
</page-header>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
|
||||||
<rd-widget>
|
|
||||||
<rd-widget-header icon="circle" title-text="Inspect">
|
|
||||||
<span class="btn-group btn-group-sm">
|
|
||||||
<label class="btn btn-light" ng-model="state.DisplayTextView" uib-btn-radio="false"><pr-icon icon="'code'"></pr-icon>Tree</label>
|
|
||||||
<label class="btn btn-light" ng-model="state.DisplayTextView" uib-btn-radio="true"><pr-icon icon="'file'"></pr-icon>Text</label>
|
|
||||||
</span>
|
|
||||||
</rd-widget-header>
|
|
||||||
<rd-widget-body>
|
|
||||||
<pre ng-show="state.DisplayTextView">{{ containerInfo | json: 4 }}</pre>
|
|
||||||
<json-tree ng-hide="state.DisplayTextView" object="containerInfo" root-name="containerInfo.Id" start-expanded="true"></json-tree>
|
|
||||||
</rd-widget-body>
|
|
||||||
</rd-widget>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -34,7 +34,6 @@ angular
|
||||||
'ngResource',
|
'ngResource',
|
||||||
'angularUtils.directives.dirPagination',
|
'angularUtils.directives.dirPagination',
|
||||||
'LocalStorageModule',
|
'LocalStorageModule',
|
||||||
'angular-json-tree',
|
|
||||||
'angular-loading-bar',
|
'angular-loading-bar',
|
||||||
'angular-clipboard',
|
'angular-clipboard',
|
||||||
'ngFileSaver',
|
'ngFileSaver',
|
||||||
|
|
|
@ -1,30 +1,25 @@
|
||||||
/* json-tree override */
|
/* json-tree override */
|
||||||
.json-tree,
|
.json-tree {
|
||||||
json-tree {
|
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--blue-5);
|
color: var(--blue-5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.json-tree .key,
|
.json-tree .key {
|
||||||
json-tree .key {
|
color: var(--text-json-tree-color);
|
||||||
|
padding-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-tree .chevronIcon {
|
||||||
color: var(--text-json-tree-color);
|
color: var(--text-json-tree-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
json-tree .key {
|
.json-tree .branch-preview {
|
||||||
padding-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.json-tree .branch-preview,
|
|
||||||
json-tree .branch-preview {
|
|
||||||
color: var(--text-json-tree-branch-preview-color);
|
color: var(--text-json-tree-branch-preview-color);
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.json-tree .leaf-value,
|
.json-tree .leaf-value {
|
||||||
json-tree .leaf-value {
|
|
||||||
color: var(--text-json-tree-leaf-color);
|
color: var(--text-json-tree-leaf-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* !json-tree override */
|
|
|
@ -3,7 +3,7 @@ import { JsonView, defaultStyles } from 'react-json-view-lite';
|
||||||
import 'react-json-view-lite/dist/index.css';
|
import 'react-json-view-lite/dist/index.css';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import './JsonTree.css';
|
import styles from './JsonTree.module.css';
|
||||||
|
|
||||||
export function JsonTree({ style, ...props }: ComponentProps<typeof JsonView>) {
|
export function JsonTree({ style, ...props }: ComponentProps<typeof JsonView>) {
|
||||||
const currentStyle = getCurrentStyle(style);
|
const currentStyle = getCurrentStyle(style);
|
||||||
|
@ -25,17 +25,20 @@ function getCurrentStyle(style: StyleProps | undefined): StyleProps {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...defaultStyles,
|
...defaultStyles,
|
||||||
container: 'json-tree',
|
container: styles.jsonTree,
|
||||||
booleanValue: 'leaf-value',
|
booleanValue: styles.leafValue,
|
||||||
nullValue: 'leaf-value',
|
nullValue: styles.leafValue,
|
||||||
otherValue: 'leaf-value',
|
otherValue: styles.leafValue,
|
||||||
numberValue: 'leaf-value',
|
numberValue: styles.leafValue,
|
||||||
stringValue: 'leaf-value',
|
stringValue: styles.leafValue,
|
||||||
undefinedValue: 'leaf-value',
|
undefinedValue: styles.leafValue,
|
||||||
label: 'key',
|
label: styles.key,
|
||||||
punctuation: 'leaf-value',
|
punctuation: styles.leafValue,
|
||||||
collapseIcon: clsx(defaultStyles.collapseIcon, 'key'),
|
collapseIcon: clsx(defaultStyles.collapseIcon, styles.chevronIcon),
|
||||||
expandIcon: clsx(defaultStyles.expandIcon, 'key'),
|
expandIcon: clsx(defaultStyles.expandIcon, styles.chevronIcon),
|
||||||
collapsedContent: clsx(defaultStyles.collapsedContent, 'branch-preview'),
|
collapsedContent: clsx(
|
||||||
|
defaultStyles.collapsedContent,
|
||||||
|
styles.branchPreview
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { PropsWithChildren, ReactNode } from 'react';
|
import { ComponentProps, PropsWithChildren, ReactNode } from 'react';
|
||||||
|
|
||||||
import { AutomationTestingProps } from '@/types';
|
import { AutomationTestingProps } from '@/types';
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ export interface Option<T> {
|
||||||
value: T;
|
value: T;
|
||||||
label?: ReactNode;
|
label?: ReactNode;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
icon?: ComponentProps<typeof Button>['icon'];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props<T> {
|
interface Props<T> {
|
||||||
|
@ -49,6 +50,7 @@ export function ButtonSelector<T extends string | number | boolean>({
|
||||||
onChange={() => onChange(option.value)}
|
onChange={() => onChange(option.value)}
|
||||||
disabled={disabled || option.disabled}
|
disabled={disabled || option.disabled}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
|
icon={option.icon}
|
||||||
>
|
>
|
||||||
{option.label || option.value.toString()}
|
{option.label || option.value.toString()}
|
||||||
</OptionItem>
|
</OptionItem>
|
||||||
|
@ -62,6 +64,7 @@ interface OptionItemProps {
|
||||||
onChange(): void;
|
onChange(): void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
|
icon?: ComponentProps<typeof Button>['icon'];
|
||||||
}
|
}
|
||||||
|
|
||||||
function OptionItem({
|
function OptionItem({
|
||||||
|
@ -71,6 +74,7 @@ function OptionItem({
|
||||||
disabled,
|
disabled,
|
||||||
readOnly,
|
readOnly,
|
||||||
'data-cy': dataCy,
|
'data-cy': dataCy,
|
||||||
|
icon,
|
||||||
}: PropsWithChildren<OptionItemProps> & AutomationTestingProps) {
|
}: PropsWithChildren<OptionItemProps> & AutomationTestingProps) {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
@ -84,6 +88,7 @@ function OptionItem({
|
||||||
'!static !z-auto'
|
'!static !z-auto'
|
||||||
)}
|
)}
|
||||||
data-cy={dataCy}
|
data-cy={dataCy}
|
||||||
|
icon={icon}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<input
|
<input
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
import { useCurrentStateAndParams } from '@uirouter/react';
|
||||||
|
import { Circle, Code as CodeIcon, File } from 'lucide-react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { trimContainerName } from '@/docker/filters/utils';
|
||||||
|
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||||
|
|
||||||
|
import { JsonTree } from '@@/JsonTree';
|
||||||
|
import { PageHeader } from '@@/PageHeader';
|
||||||
|
import { Widget } from '@@/Widget';
|
||||||
|
import { ButtonSelector } from '@@/form-components/ButtonSelector/ButtonSelector';
|
||||||
|
import { Code } from '@@/Code';
|
||||||
|
|
||||||
|
import { useContainerInspect } from '../queries/useContainerInspect';
|
||||||
|
|
||||||
|
export function InspectView() {
|
||||||
|
const environmentId = useEnvironmentId();
|
||||||
|
const {
|
||||||
|
params: { id, nodeName },
|
||||||
|
} = useCurrentStateAndParams();
|
||||||
|
const inspectQuery = useContainerInspect(environmentId, id, { nodeName });
|
||||||
|
const [viewType, setViewType] = useState<'tree' | 'text'>('tree');
|
||||||
|
|
||||||
|
if (!inspectQuery.data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const containerInfo = inspectQuery.data;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHeader
|
||||||
|
title="Container inspect"
|
||||||
|
breadcrumbs={[
|
||||||
|
{ label: 'Containers', link: 'docker.containers' },
|
||||||
|
{
|
||||||
|
label: trimContainerName(containerInfo.Name),
|
||||||
|
link: '^',
|
||||||
|
// linkParams: { id: containerInfo.Id },
|
||||||
|
},
|
||||||
|
'Inspect',
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-lg-12 col-md-12 col-xs-12">
|
||||||
|
<Widget>
|
||||||
|
<Widget.Title icon={Circle} title="Inspect">
|
||||||
|
<ButtonSelector<'tree' | 'text'>
|
||||||
|
onChange={(value) => setViewType(value)}
|
||||||
|
value={viewType}
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
label: 'Tree',
|
||||||
|
icon: CodeIcon,
|
||||||
|
value: 'tree',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Text',
|
||||||
|
icon: File,
|
||||||
|
value: 'text',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Widget.Title>
|
||||||
|
<Widget.Body>
|
||||||
|
{viewType === 'text' && (
|
||||||
|
<Code showCopyButton>
|
||||||
|
{JSON.stringify(containerInfo, undefined, 4)}
|
||||||
|
</Code>
|
||||||
|
)}
|
||||||
|
{viewType === 'tree' && <JsonTree data={containerInfo} />}
|
||||||
|
</Widget.Body>
|
||||||
|
</Widget>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { useQuery } from 'react-query';
|
||||||
|
|
||||||
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||||
|
import { genericHandler } from '@/docker/rest/response/handlers';
|
||||||
|
|
||||||
|
import { ContainerId } from '../types';
|
||||||
|
import { urlBuilder } from '../containers.service';
|
||||||
|
import { addNodeName } from '../../proxy/addNodeName';
|
||||||
|
|
||||||
|
import { queryKeys } from './query-keys';
|
||||||
|
import { ContainerJSON } from './container';
|
||||||
|
|
||||||
|
export function useContainerInspect(
|
||||||
|
environmentId: EnvironmentId,
|
||||||
|
id: ContainerId,
|
||||||
|
params: { nodeName?: string } = {}
|
||||||
|
) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [...queryKeys.container(environmentId, id), params] as const,
|
||||||
|
queryFn: () => inspectContainer(environmentId, id, params),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function inspectContainer(
|
||||||
|
environmentId: EnvironmentId,
|
||||||
|
id: ContainerId,
|
||||||
|
{ nodeName }: { nodeName?: string } = {}
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get<ContainerJSON>(
|
||||||
|
urlBuilder(environmentId, id, 'json'),
|
||||||
|
{ transformResponse: genericHandler, headers: addNodeName(nodeName) }
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
} catch (e) {
|
||||||
|
throw parseAxiosError(e, 'Failed starting container');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { RawAxiosRequestHeaders } from 'axios';
|
||||||
|
|
||||||
|
const AgentTargetHeader = 'X-PortainerAgent-Target';
|
||||||
|
|
||||||
|
export function addNodeName(
|
||||||
|
nodeName?: string,
|
||||||
|
headers: RawAxiosRequestHeaders = {}
|
||||||
|
) {
|
||||||
|
if (!nodeName) {
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...headers,
|
||||||
|
[AgentTargetHeader]: nodeName,
|
||||||
|
};
|
||||||
|
}
|
|
@ -6,7 +6,6 @@ import 'angular-messages';
|
||||||
import 'angular-resource';
|
import 'angular-resource';
|
||||||
import 'angular-utils-pagination';
|
import 'angular-utils-pagination';
|
||||||
import 'angular-local-storage';
|
import 'angular-local-storage';
|
||||||
import 'angular-json-tree';
|
|
||||||
import 'angular-loading-bar';
|
import 'angular-loading-bar';
|
||||||
import 'angular-clipboard';
|
import 'angular-clipboard';
|
||||||
import 'angular-file-saver';
|
import 'angular-file-saver';
|
||||||
|
|
|
@ -61,7 +61,6 @@
|
||||||
"angular": "1.8.2",
|
"angular": "1.8.2",
|
||||||
"angular-clipboard": "^1.6.2",
|
"angular-clipboard": "^1.6.2",
|
||||||
"angular-file-saver": "^1.1.3",
|
"angular-file-saver": "^1.1.3",
|
||||||
"angular-json-tree": "1.1.0",
|
|
||||||
"angular-loading-bar": "~0.9.0",
|
"angular-loading-bar": "~0.9.0",
|
||||||
"angular-local-storage": "~0.5.2",
|
"angular-local-storage": "~0.5.2",
|
||||||
"angular-messages": "1.8.2",
|
"angular-messages": "1.8.2",
|
||||||
|
|
|
@ -6915,11 +6915,6 @@ angular-file-saver@^1.1.3:
|
||||||
blob-tmp "^1.0.0"
|
blob-tmp "^1.0.0"
|
||||||
file-saver "^1.3.3"
|
file-saver "^1.3.3"
|
||||||
|
|
||||||
angular-json-tree@1.1.0:
|
|
||||||
version "1.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/angular-json-tree/-/angular-json-tree-1.1.0.tgz#d7faba97130fc273fa29ef517dbed10342cc645e"
|
|
||||||
integrity sha512-HVLyrVkEoYVykcIgzMCdhtK2H8Y4jgNujGNqRXNG4x032tp2ZWp34j/hu/E7h6a7X+ODrSTAfRTbkF4f/JX/Fg==
|
|
||||||
|
|
||||||
angular-loading-bar@~0.9.0:
|
angular-loading-bar@~0.9.0:
|
||||||
version "0.9.0"
|
version "0.9.0"
|
||||||
resolved "https://registry.yarnpkg.com/angular-loading-bar/-/angular-loading-bar-0.9.0.tgz#37ef52c25f102c216e7b3cdfd2fc5a5df9628e45"
|
resolved "https://registry.yarnpkg.com/angular-loading-bar/-/angular-loading-bar-0.9.0.tgz#37ef52c25f102c216e7b3cdfd2fc5a5df9628e45"
|
||||||
|
|
Loading…
Reference in New Issue