argo-cd
465 строк · 27.4 Кб
1import {DataLoader, DropDown, DropDownMenu, MenuItem, Tooltip} from 'argo-ui';
2import * as PropTypes from 'prop-types';
3import * as React from 'react';
4import Moment from 'react-moment';
5
6import {AppContext} from '../../../shared/context';
7import {EmptyState} from '../../../shared/components';
8import {Application, ApplicationTree, HostResourceInfo, InfoItem, Node, Pod, ResourceName, ResourceNode, ResourceStatus} from '../../../shared/models';
9import {PodViewPreferences, services, ViewPreferences} from '../../../shared/services';
10
11import {ResourceTreeNode} from '../application-resource-tree/application-resource-tree';
12import {ResourceIcon} from '../resource-icon';
13import {ResourceLabel} from '../resource-label';
14import {ComparisonStatusIcon, isYoungerThanXMinutes, HealthStatusIcon, nodeKey, PodHealthIcon, deletePodAction} from '../utils';
15
16import './pod-view.scss';
17import {PodTooltip} from './pod-tooltip';
18
19interface PodViewProps {
20tree: ApplicationTree;
21onItemClick: (fullName: string) => void;
22app: Application;
23nodeMenu?: (node: ResourceNode) => React.ReactNode;
24quickStarts?: (node: ResourceNode) => React.ReactNode;
25}
26
27export type PodGroupType = 'topLevelResource' | 'parentResource' | 'node';
28
29export interface PodGroup extends Partial<ResourceNode> {
30type: PodGroupType;
31pods: Pod[];
32info?: InfoItem[];
33hostResourcesInfo?: HostResourceInfo[];
34resourceStatus?: Partial<ResourceStatus>;
35renderMenu?: () => React.ReactNode;
36renderQuickStarts?: () => React.ReactNode;
37fullName?: string;
38}
39
40export class PodView extends React.Component<PodViewProps> {
41private get appContext(): AppContext {
42return this.context as AppContext;
43}
44
45public static contextTypes = {
46apis: PropTypes.object
47};
48
49public render() {
50return (
51<DataLoader load={() => services.viewPreferences.getPreferences()}>
52{prefs => {
53const podPrefs = prefs.appDetails.podView || ({} as PodViewPreferences);
54const groups = this.processTree(podPrefs.sortMode, this.props.tree.hosts || []) || [];
55
56return (
57<React.Fragment>
58<div className='pod-view__settings'>
59<div className='pod-view__settings__section'>
60GROUP BY:
61<DropDownMenu
62anchor={() => (
63<button className='argo-button argo-button--base-o'>
64{labelForSortMode[podPrefs.sortMode]}
65<i className='fa fa-chevron-circle-down' />
66</button>
67)}
68items={this.menuItemsFor(['node', 'parentResource', 'topLevelResource'], prefs)}
69/>
70</div>
71{podPrefs.sortMode === 'node' && (
72<div className='pod-view__settings__section'>
73<button
74className={`argo-button argo-button--base${podPrefs.hideUnschedulable ? '-o' : ''}`}
75style={{border: 'none', width: '170px'}}
76onClick={() =>
77services.viewPreferences.updatePreferences({
78appDetails: {...prefs.appDetails, podView: {...podPrefs, hideUnschedulable: !podPrefs.hideUnschedulable}}
79})
80}>
81<i className={`fa fa-${podPrefs.hideUnschedulable ? 'eye-slash' : 'eye'}`} style={{width: '15px', marginRight: '5px'}} />
82UNSCHEDULABLE
83</button>
84</div>
85)}
86</div>
87{groups.length > 0 ? (
88<div className='pod-view__nodes-container'>
89{groups.map(group => {
90if (group.type === 'node' && group.name === 'Unschedulable' && podPrefs.hideUnschedulable) {
91return <React.Fragment />;
92}
93return (
94<div className={`pod-view__node white-box ${group.kind === 'node' && 'pod-view__node--large'}`} key={group.fullName || group.name}>
95<div
96className='pod-view__node__container--header'
97onClick={() => this.props.onItemClick(group.fullName)}
98style={group.kind === 'node' ? {} : {cursor: 'pointer'}}>
99<div style={{display: 'flex', alignItems: 'center'}}>
100<div style={{marginRight: '10px'}}>
101<ResourceIcon kind={group.kind || 'Unknown'} />
102<br />
103{<div style={{textAlign: 'center'}}>{ResourceLabel({kind: group.kind})}</div>}
104</div>
105<div style={{lineHeight: '15px'}}>
106<b style={{wordWrap: 'break-word'}}>{group.name || 'Unknown'}</b>
107{group.resourceStatus && (
108<div>
109{group.resourceStatus.health && <HealthStatusIcon state={group.resourceStatus.health} />}
110
111{group.resourceStatus.status && (
112<ComparisonStatusIcon status={group.resourceStatus.status} resource={group.resourceStatus} />
113)}
114</div>
115)}
116</div>
117<div style={{marginLeft: 'auto'}}>
118{group.renderMenu && (
119<DropDown
120isMenu={true}
121anchor={() => (
122<button className='argo-button argo-button--light argo-button--lg argo-button--short'>
123<i className='fa fa-ellipsis-v' />
124</button>
125)}>
126{() => group.renderMenu()}
127</DropDown>
128)}
129</div>
130</div>
131{group.type === 'node' ? (
132<div className='pod-view__node__info--large'>
133{(group.info || []).map(item => (
134<div key={item.name}>
135{item.name}: <div>{item.value}</div>
136</div>
137))}
138</div>
139) : (
140<div className='pod-view__node__info'>
141{group.createdAt ? (
142<div>
143<Moment fromNow={true} ago={true}>
144{group.createdAt}
145</Moment>
146</div>
147) : null}
148{group.info?.map(infoItem => (
149<div key={infoItem.name}>{infoItem.value}</div>
150))}
151</div>
152)}
153</div>
154<div className='pod-view__node__container'>
155{(group.hostResourcesInfo || []).length > 0 && (
156<div className='pod-view__node__container pod-view__node__container--stats'>
157{group.hostResourcesInfo.map(info => renderStats(info))}
158</div>
159)}
160<div className='pod-view__node__pod-container pod-view__node__container'>
161<div className='pod-view__node__pod-container__pods'>
162{group.pods.map(pod => (
163<DropDownMenu
164key={pod.uid}
165anchor={() => (
166<Tooltip
167content={<PodTooltip pod={pod} />}
168popperOptions={{
169modifiers: {
170preventOverflow: {
171enabled: true
172},
173hide: {
174enabled: false
175},
176flip: {
177enabled: false
178}
179}
180}}
181key={pod.metadata.name}>
182<div style={{position: 'relative'}}>
183{isYoungerThanXMinutes(pod, 30) && (
184<i className='fas fa-circle pod-view__node__pod pod-view__node__pod__new-pod-icon' />
185)}
186<div className={`pod-view__node__pod pod-view__node__pod--${pod.health.toLowerCase()}`}>
187<PodHealthIcon state={{status: pod.health, message: ''}} />
188</div>
189</div>
190</Tooltip>
191)}
192items={[
193{
194title: (
195<React.Fragment>
196<i className='fa fa-info-circle' /> Info
197</React.Fragment>
198),
199action: () => this.props.onItemClick(pod.fullName)
200},
201{
202title: (
203<React.Fragment>
204<i className='fa fa-align-left' /> Logs
205</React.Fragment>
206),
207action: () => {
208this.appContext.apis.navigation.goto('.', {node: pod.fullName, tab: 'logs'}, {replace: true});
209}
210},
211{
212title: (
213<React.Fragment>
214<i className='fa fa-terminal' /> Exec
215</React.Fragment>
216),
217action: () => {
218this.appContext.apis.navigation.goto('.', {node: pod.fullName, tab: 'exec'}, {replace: true});
219}
220},
221{
222title: (
223<React.Fragment>
224<i className='fa fa-times-circle' /> Delete
225</React.Fragment>
226),
227action: () => {
228deletePodAction(
229pod,
230this.appContext,
231this.props.app.metadata.name,
232this.props.app.metadata.namespace
233);
234}
235}
236]}
237/>
238))}
239</div>
240<div className='pod-view__node__label'>PODS</div>
241{(podPrefs.sortMode === 'parentResource' || podPrefs.sortMode === 'topLevelResource') && (
242<div key={group.uid}>{group.renderQuickStarts()}</div>
243)}
244</div>
245</div>
246</div>
247);
248})}
249</div>
250) : (
251<EmptyState icon=' fa fa-th'>
252<h4>Your application has no pod groups</h4>
253<h5>Try switching to tree or list view</h5>
254</EmptyState>
255)}
256</React.Fragment>
257);
258}}
259</DataLoader>
260);
261}
262
263private menuItemsFor(modes: PodGroupType[], prefs: ViewPreferences): MenuItem[] {
264const podPrefs = prefs.appDetails.podView || ({} as PodViewPreferences);
265return modes.map(mode => ({
266title: (
267<React.Fragment>
268{podPrefs.sortMode === mode && <i className='fa fa-check' />} {labelForSortMode[mode]}{' '}
269</React.Fragment>
270),
271action: () => {
272this.appContext.apis.navigation.goto('.', {podSortMode: mode});
273services.viewPreferences.updatePreferences({appDetails: {...prefs.appDetails, podView: {...podPrefs, sortMode: mode}}});
274}
275}));
276}
277
278private processTree(sortMode: PodGroupType, initNodes: Node[]): PodGroup[] {
279const tree = this.props.tree;
280if (!tree) {
281return [];
282}
283const groupRefs: {[key: string]: PodGroup} = {};
284const parentsFor: {[key: string]: PodGroup[]} = {};
285
286if (sortMode === 'node' && initNodes) {
287initNodes.forEach(infraNode => {
288const nodeName = infraNode.name;
289groupRefs[nodeName] = {
290...infraNode,
291type: 'node',
292kind: 'node',
293name: nodeName,
294pods: [],
295info: [
296{name: 'Kernel Version', value: infraNode.systemInfo.kernelVersion},
297{name: 'OS/Arch', value: `${infraNode.systemInfo.operatingSystem}/${infraNode.systemInfo.architecture}`}
298],
299hostResourcesInfo: infraNode.resourcesInfo
300};
301});
302}
303
304const statusByKey = new Map<string, ResourceStatus>();
305this.props.app.status?.resources?.forEach(res => statusByKey.set(nodeKey(res), res));
306
307(tree.nodes || []).forEach((rnode: ResourceTreeNode) => {
308// make sure each node has not null/undefined parentRefs field
309rnode.parentRefs = rnode.parentRefs || [];
310
311if (sortMode !== 'node') {
312parentsFor[rnode.uid] = rnode.parentRefs as PodGroup[];
313const fullName = nodeKey(rnode);
314const status = statusByKey.get(fullName);
315
316if ((rnode.parentRefs || []).length === 0) {
317rnode.root = rnode;
318}
319groupRefs[rnode.uid] = {
320pods: [] as Pod[],
321fullName,
322...groupRefs[rnode.uid],
323...rnode,
324info: (rnode.info || []).filter(i => !i.name.includes('Resource.')),
325createdAt: rnode.createdAt,
326resourceStatus: {health: rnode.health, status: status ? status.status : null, requiresPruning: status && status.requiresPruning ? true : false},
327renderMenu: () => this.props.nodeMenu(rnode),
328renderQuickStarts: () => this.props.quickStarts(rnode)
329};
330}
331});
332(tree.nodes || []).forEach((rnode: ResourceTreeNode) => {
333if (rnode.kind !== 'Pod') {
334return;
335}
336
337const p: Pod = {
338...rnode,
339fullName: nodeKey(rnode),
340metadata: {name: rnode.name},
341spec: {nodeName: 'Unknown'},
342health: rnode.health ? rnode.health.status : 'Unknown'
343} as Pod;
344
345// Get node name for Pod
346rnode.info?.forEach(i => {
347if (i.name === 'Node') {
348p.spec.nodeName = i.value;
349}
350});
351
352if (sortMode === 'node') {
353if (groupRefs[p.spec.nodeName]) {
354const curNode = groupRefs[p.spec.nodeName];
355curNode.pods.push(p);
356} else {
357if (groupRefs.Unschedulable) {
358groupRefs.Unschedulable.pods.push(p);
359} else {
360groupRefs.Unschedulable = {
361type: 'node',
362kind: 'node',
363name: 'Unschedulable',
364pods: [p],
365info: [
366{name: 'Kernel Version', value: 'N/A'},
367{name: 'OS/Arch', value: 'N/A'}
368],
369hostResourcesInfo: []
370};
371}
372}
373} else if (sortMode === 'parentResource') {
374rnode.parentRefs.forEach(parentRef => {
375if (!groupRefs[parentRef.uid]) {
376groupRefs[parentRef.uid] = {
377kind: parentRef.kind,
378type: sortMode,
379name: parentRef.name,
380pods: [p]
381};
382} else {
383groupRefs[parentRef.uid].pods.push(p);
384}
385});
386} else if (sortMode === 'topLevelResource') {
387let cur = rnode.uid;
388let parents = parentsFor[rnode.uid];
389while ((parents || []).length > 0) {
390cur = parents[0].uid;
391parents = parentsFor[cur];
392}
393if (groupRefs[cur]) {
394groupRefs[cur].pods.push(p);
395}
396}
397});
398
399Object.values(groupRefs).forEach(group => group.pods.sort((first, second) => nodeKey(first).localeCompare(nodeKey(second))));
400
401return Object.values(groupRefs)
402.sort((a, b) => (a.name > b.name ? 1 : a.name === b.name ? 0 : -1)) // sort by name
403.filter(i => (i.pods || []).length > 0); // filter out groups with no pods
404}
405}
406
407const labelForSortMode = {
408node: 'Node',
409parentResource: 'Parent Resource',
410topLevelResource: 'Top Level Resource'
411};
412
413const sizes = ['Bytes', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'];
414function formatSize(bytes: number) {
415if (!bytes) {
416return '0 Bytes';
417}
418const k = 1024;
419const dm = 2;
420const i = Math.floor(Math.log(bytes) / Math.log(k));
421return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
422}
423
424function formatMetric(name: ResourceName, val: number) {
425if (name === ResourceName.ResourceStorage || name === ResourceName.ResourceMemory) {
426// divide by 1000 to convert "milli bytes" to bytes
427return formatSize(val / 1000);
428}
429// cpu millicores
430return (val || '0') + 'm';
431}
432
433function renderStats(info: HostResourceInfo) {
434const neighborsHeight = 100 * (info.requestedByNeighbors / info.capacity);
435const appHeight = 100 * (info.requestedByApp / info.capacity);
436return (
437<div className='pod-view__node__pod__stat' key={info.resourceName}>
438<Tooltip
439key={info.resourceName}
440content={
441<React.Fragment>
442<div>{info.resourceName.toUpperCase()}:</div>
443<div className='pod-view__node__pod__stat-tooltip'>
444<div>Requests:</div>
445<div>
446{' '}
447<i className='pod-view__node__pod__stat-icon-app' /> {formatMetric(info.resourceName, info.requestedByApp)} (App)
448</div>
449<div>
450{' '}
451<i className='pod-view__node__pod__stat-icon-neighbors' /> {formatMetric(info.resourceName, info.requestedByNeighbors)} (Neighbors)
452</div>
453<div>Capacity: {formatMetric(info.resourceName, info.capacity)}</div>
454</div>
455</React.Fragment>
456}>
457<div className='pod-view__node__pod__stat__bar'>
458<div className='pod-view__node__pod__stat__bar--fill pod-view__node__pod__stat__bar--neighbors' style={{height: `${neighborsHeight}%`}} />
459<div className='pod-view__node__pod__stat__bar--fill' style={{bottom: `${neighborsHeight}%`, height: `${appHeight}%`}} />
460</div>
461</Tooltip>
462<div className='pod-view__node__label'>{info.resourceName.slice(0, 3).toUpperCase()}</div>
463</div>
464);
465}
466