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 'xterm/dist/xterm.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-moment-picker/dist/angular-moment-picker.min.css';
|
||||
import 'spinkit/spinkit.min.css';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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) {
|
||||
return values.some(function (val) {
|
||||
|
@ -76,15 +76,7 @@ angular
|
|||
};
|
||||
})
|
||||
.filter('nodestatusbadge', () => nodeStatusBadge)
|
||||
.filter('trimcontainername', function () {
|
||||
'use strict';
|
||||
return function (name) {
|
||||
if (name) {
|
||||
return name.indexOf('/') === 0 ? name.slice(1) : name;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
})
|
||||
.filter('trimcontainername', () => trimContainerName)
|
||||
.filter('getstatetext', function () {
|
||||
'use strict';
|
||||
return function (state) {
|
||||
|
|
|
@ -83,3 +83,10 @@ export function nodeStatusBadge(text: NodeStatus['State']) {
|
|||
export function hideShaSum(imageName = '') {
|
||||
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 { LogView } from '@/react/docker/containers/LogView';
|
||||
import { CreateView } from '@/react/docker/containers/CreateView';
|
||||
import { InspectView } from '@/react/docker/containers/InspectView/InspectView';
|
||||
|
||||
export const containersModule = angular
|
||||
.module('portainer.docker.react.views.containers', [])
|
||||
|
@ -26,7 +27,10 @@ export const containersModule = angular
|
|||
'containerLogView',
|
||||
r2a(withUIRouter(withReactQuery(withCurrentUser(LogView))), [])
|
||||
)
|
||||
|
||||
.component(
|
||||
'dockerContainerInspectView',
|
||||
r2a(withUIRouter(withReactQuery(withCurrentUser(InspectView))), [])
|
||||
)
|
||||
.config(config).name;
|
||||
|
||||
/* @ngInject */
|
||||
|
@ -95,8 +99,7 @@ function config($stateRegistryProvider: StateRegistry) {
|
|||
url: '/inspect',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: '~@/docker/views/containers/inspect/containerinspect.html',
|
||||
controller: 'ContainerInspectController',
|
||||
component: 'dockerContainerInspectView',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
'angularUtils.directives.dirPagination',
|
||||
'LocalStorageModule',
|
||||
'angular-json-tree',
|
||||
'angular-loading-bar',
|
||||
'angular-clipboard',
|
||||
'ngFileSaver',
|
||||
|
|
|
@ -1,30 +1,25 @@
|
|||
/* json-tree override */
|
||||
.json-tree,
|
||||
json-tree {
|
||||
.json-tree {
|
||||
font-size: 13px;
|
||||
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);
|
||||
}
|
||||
|
||||
json-tree .key {
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.json-tree .branch-preview,
|
||||
json-tree .branch-preview {
|
||||
.json-tree .branch-preview {
|
||||
color: var(--text-json-tree-branch-preview-color);
|
||||
font-style: normal;
|
||||
font-size: 11px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.json-tree .leaf-value,
|
||||
json-tree .leaf-value {
|
||||
.json-tree .leaf-value {
|
||||
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 clsx from 'clsx';
|
||||
|
||||
import './JsonTree.css';
|
||||
import styles from './JsonTree.module.css';
|
||||
|
||||
export function JsonTree({ style, ...props }: ComponentProps<typeof JsonView>) {
|
||||
const currentStyle = getCurrentStyle(style);
|
||||
|
@ -25,17 +25,20 @@ function getCurrentStyle(style: StyleProps | undefined): StyleProps {
|
|||
|
||||
return {
|
||||
...defaultStyles,
|
||||
container: 'json-tree',
|
||||
booleanValue: 'leaf-value',
|
||||
nullValue: 'leaf-value',
|
||||
otherValue: 'leaf-value',
|
||||
numberValue: 'leaf-value',
|
||||
stringValue: 'leaf-value',
|
||||
undefinedValue: 'leaf-value',
|
||||
label: 'key',
|
||||
punctuation: 'leaf-value',
|
||||
collapseIcon: clsx(defaultStyles.collapseIcon, 'key'),
|
||||
expandIcon: clsx(defaultStyles.expandIcon, 'key'),
|
||||
collapsedContent: clsx(defaultStyles.collapsedContent, 'branch-preview'),
|
||||
container: styles.jsonTree,
|
||||
booleanValue: styles.leafValue,
|
||||
nullValue: styles.leafValue,
|
||||
otherValue: styles.leafValue,
|
||||
numberValue: styles.leafValue,
|
||||
stringValue: styles.leafValue,
|
||||
undefinedValue: styles.leafValue,
|
||||
label: styles.key,
|
||||
punctuation: styles.leafValue,
|
||||
collapseIcon: clsx(defaultStyles.collapseIcon, styles.chevronIcon),
|
||||
expandIcon: clsx(defaultStyles.expandIcon, styles.chevronIcon),
|
||||
collapsedContent: clsx(
|
||||
defaultStyles.collapsedContent,
|
||||
styles.branchPreview
|
||||
),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import clsx from 'clsx';
|
||||
import { PropsWithChildren, ReactNode } from 'react';
|
||||
import { ComponentProps, PropsWithChildren, ReactNode } from 'react';
|
||||
|
||||
import { AutomationTestingProps } from '@/types';
|
||||
|
||||
|
@ -12,6 +12,7 @@ export interface Option<T> {
|
|||
value: T;
|
||||
label?: ReactNode;
|
||||
disabled?: boolean;
|
||||
icon?: ComponentProps<typeof Button>['icon'];
|
||||
}
|
||||
|
||||
interface Props<T> {
|
||||
|
@ -49,6 +50,7 @@ export function ButtonSelector<T extends string | number | boolean>({
|
|||
onChange={() => onChange(option.value)}
|
||||
disabled={disabled || option.disabled}
|
||||
readOnly={readOnly}
|
||||
icon={option.icon}
|
||||
>
|
||||
{option.label || option.value.toString()}
|
||||
</OptionItem>
|
||||
|
@ -62,6 +64,7 @@ interface OptionItemProps {
|
|||
onChange(): void;
|
||||
disabled?: boolean;
|
||||
readOnly?: boolean;
|
||||
icon?: ComponentProps<typeof Button>['icon'];
|
||||
}
|
||||
|
||||
function OptionItem({
|
||||
|
@ -71,6 +74,7 @@ function OptionItem({
|
|||
disabled,
|
||||
readOnly,
|
||||
'data-cy': dataCy,
|
||||
icon,
|
||||
}: PropsWithChildren<OptionItemProps> & AutomationTestingProps) {
|
||||
return (
|
||||
<Button
|
||||
|
@ -84,6 +88,7 @@ function OptionItem({
|
|||
'!static !z-auto'
|
||||
)}
|
||||
data-cy={dataCy}
|
||||
icon={icon}
|
||||
>
|
||||
{children}
|
||||
<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-utils-pagination';
|
||||
import 'angular-local-storage';
|
||||
import 'angular-json-tree';
|
||||
import 'angular-loading-bar';
|
||||
import 'angular-clipboard';
|
||||
import 'angular-file-saver';
|
||||
|
|
|
@ -61,7 +61,6 @@
|
|||
"angular": "1.8.2",
|
||||
"angular-clipboard": "^1.6.2",
|
||||
"angular-file-saver": "^1.1.3",
|
||||
"angular-json-tree": "1.1.0",
|
||||
"angular-loading-bar": "~0.9.0",
|
||||
"angular-local-storage": "~0.5.2",
|
||||
"angular-messages": "1.8.2",
|
||||
|
|
|
@ -6915,11 +6915,6 @@ angular-file-saver@^1.1.3:
|
|||
blob-tmp "^1.0.0"
|
||||
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:
|
||||
version "0.9.0"
|
||||
resolved "https://registry.yarnpkg.com/angular-loading-bar/-/angular-loading-bar-0.9.0.tgz#37ef52c25f102c216e7b3cdfd2fc5a5df9628e45"
|
||||
|
|
Loading…
Reference in New Issue