github.com/argoproj/argo-cd/v3@v3.2.1/ui/src/app/applications/components/resource-details/resource-details.tsx (about)

     1  import {DataLoader, DropDown, Tab, Tabs} from 'argo-ui';
     2  import * as React from 'react';
     3  import {useState} from 'react';
     4  import {EventsList, YamlEditor} from '../../../shared/components';
     5  import * as models from '../../../shared/models';
     6  import {ErrorBoundary} from '../../../shared/components/error-boundary/error-boundary';
     7  import {AppContext, Context} from '../../../shared/context';
     8  import {Application, ApplicationTree, Event, ResourceNode, State, SyncStatuses} from '../../../shared/models';
     9  import {services} from '../../../shared/services';
    10  import {ResourceTabExtension} from '../../../shared/services/extensions-service';
    11  import {NodeInfo, SelectNode} from '../application-details/application-details';
    12  import {ApplicationNodeInfo} from '../application-node-info/application-node-info';
    13  import {ApplicationParameters} from '../application-parameters/application-parameters';
    14  import {ApplicationResourceEvents} from '../application-resource-events/application-resource-events';
    15  import {ResourceTreeNode} from '../application-resource-tree/application-resource-tree';
    16  import {ApplicationResourcesDiff} from '../application-resources-diff/application-resources-diff';
    17  import {ApplicationSummary} from '../application-summary/application-summary';
    18  import {PodsLogsViewer} from '../pod-logs-viewer/pod-logs-viewer';
    19  import {PodTerminalViewer} from '../pod-terminal-viewer/pod-terminal-viewer';
    20  import {ResourceIcon} from '../resource-icon';
    21  import {ResourceLabel} from '../resource-label';
    22  import * as AppUtils from '../utils';
    23  import './resource-details.scss';
    24  
    25  const jsonMergePatch = require('json-merge-patch');
    26  
    27  interface ResourceDetailsProps {
    28      selectedNode: ResourceNode;
    29      updateApp: (app: Application, query: {validate?: boolean}) => Promise<any>;
    30      application: Application;
    31      isAppSelected: boolean;
    32      tree: ApplicationTree;
    33      tab?: string;
    34      appCxt: AppContext;
    35  }
    36  
    37  export const ResourceDetails = (props: ResourceDetailsProps) => {
    38      const {selectedNode, updateApp, application, isAppSelected, tree} = {...props};
    39      const [activeContainer, setActiveContainer] = useState();
    40      const appContext = React.useContext(Context);
    41      const tab = new URLSearchParams(appContext.history.location.search).get('tab');
    42      const selectedNodeInfo = NodeInfo(new URLSearchParams(appContext.history.location.search).get('node'));
    43      const selectedNodeKey = selectedNodeInfo.key;
    44      const [pageNumber, setPageNumber] = React.useState(0);
    45      const [collapsedSources, setCollapsedSources] = React.useState(new Array<boolean>()); // For Sources tab to save collapse states
    46      const handleCollapse = (i: number, isCollapsed: boolean) => {
    47          const v = collapsedSources.slice();
    48          v[i] = isCollapsed;
    49          setCollapsedSources(v);
    50      };
    51  
    52      const getResourceTabs = (
    53          node: ResourceNode,
    54          state: State,
    55          podState: State,
    56          events: Event[],
    57          extensionTabs: ResourceTabExtension[],
    58          tabs: Tab[],
    59          execEnabled: boolean,
    60          execAllowed: boolean,
    61          logsAllowed: boolean
    62      ) => {
    63          if (!node || node === undefined) {
    64              return [];
    65          }
    66          if (state) {
    67              const numErrors = events.filter(event => event.type !== 'Normal').reduce((total, event) => total + event.count, 0);
    68              tabs.push({
    69                  title: 'EVENTS',
    70                  icon: 'fa fa-calendar-alt',
    71                  badge: (numErrors > 0 && numErrors) || null,
    72                  key: 'events',
    73                  content: (
    74                      <div className='application-resource-events'>
    75                          <EventsList events={events} />
    76                      </div>
    77                  )
    78              });
    79          }
    80          if (podState && podState.metadata && podState.spec) {
    81              const containerGroups = [
    82                  {
    83                      offset: 0,
    84                      title: 'CONTAINERS',
    85                      containers: podState.spec.containers || []
    86                  }
    87              ];
    88              if (podState.spec.initContainers?.length > 0) {
    89                  containerGroups.push({
    90                      offset: (podState.spec.containers || []).length,
    91                      title: 'INIT CONTAINERS',
    92                      containers: podState.spec.initContainers || []
    93                  });
    94              }
    95  
    96              const onClickContainer = (group: any, i: number, activeTab: string) => {
    97                  setActiveContainer(group.offset + i);
    98                  SelectNode(selectedNodeKey, activeContainer, activeTab, appContext);
    99              };
   100  
   101              if (logsAllowed) {
   102                  tabs = tabs.concat([
   103                      {
   104                          key: 'logs',
   105                          icon: 'fa fa-align-left',
   106                          title: 'LOGS',
   107                          content: (
   108                              <div className='application-details__tab-content-full-height'>
   109                                  <PodsLogsViewer
   110                                      podName={(state.kind === 'Pod' && state.metadata.name) || ''}
   111                                      group={node.group}
   112                                      kind={node.kind}
   113                                      name={node.name}
   114                                      namespace={podState.metadata.namespace}
   115                                      applicationName={application.metadata.name}
   116                                      applicationNamespace={application.metadata.namespace}
   117                                      containerName={AppUtils.getContainerName(podState, activeContainer)}
   118                                      containerGroups={containerGroups}
   119                                      onClickContainer={onClickContainer}
   120                                  />
   121                              </div>
   122                          )
   123                      }
   124                  ]);
   125              }
   126              if (selectedNode?.kind === 'Pod' && execEnabled && execAllowed) {
   127                  tabs = tabs.concat([
   128                      {
   129                          key: 'exec',
   130                          icon: 'fa fa-terminal',
   131                          title: 'Terminal',
   132                          content: (
   133                              <PodTerminalViewer
   134                                  applicationName={application.metadata.name}
   135                                  applicationNamespace={application.metadata.namespace}
   136                                  projectName={application.spec.project}
   137                                  podState={podState}
   138                                  selectedNode={selectedNode}
   139                                  containerName={AppUtils.getContainerName(podState, activeContainer)}
   140                                  onClickContainer={onClickContainer}
   141                              />
   142                          )
   143                      }
   144                  ]);
   145              }
   146          }
   147          if (state) {
   148              extensionTabs.forEach((tabExtensions, i) => {
   149                  tabs.push({
   150                      title: tabExtensions.title,
   151                      key: `extension-${i}`,
   152                      content: (
   153                          <ErrorBoundary message={`Something went wrong with Extension for ${state?.kind || 'resource of unknown kind'}`}>
   154                              <tabExtensions.component tree={tree} resource={state} application={application} />
   155                          </ErrorBoundary>
   156                      ),
   157                      icon: tabExtensions.icon
   158                  });
   159              });
   160          }
   161          return tabs;
   162      };
   163  
   164      const getApplicationTabs = () => {
   165          const tabs: Tab[] = [
   166              {
   167                  title: 'SUMMARY',
   168                  key: 'summary',
   169                  content: <ApplicationSummary app={application} updateApp={(app, query: {validate?: boolean}) => updateApp(app, query)} />
   170              },
   171              {
   172                  title: application.spec.sources === undefined ? 'PARAMETERS' : 'SOURCES',
   173                  key: 'parameters',
   174                  content: (
   175                      <ApplicationParameters
   176                          save={(app: models.Application, query: {validate?: boolean}) => updateApp(app, query)}
   177                          application={application}
   178                          pageNumber={pageNumber}
   179                          setPageNumber={setPageNumber}
   180                          collapsedSources={collapsedSources}
   181                          handleCollapse={handleCollapse}
   182                          appContext={props.appCxt}
   183                      />
   184                  )
   185              },
   186              {
   187                  title: 'MANIFEST',
   188                  key: 'manifest',
   189                  content: (
   190                      <YamlEditor
   191                          minHeight={800}
   192                          input={application.spec}
   193                          onSave={async patch => {
   194                              const spec = JSON.parse(JSON.stringify(application.spec));
   195                              return services.applications.updateSpec(application.metadata.name, application.metadata.namespace, jsonMergePatch.apply(spec, JSON.parse(patch)));
   196                          }}
   197                      />
   198                  )
   199              }
   200          ];
   201  
   202          if (application.status.sync.status !== SyncStatuses.Synced) {
   203              tabs.push({
   204                  icon: 'fa fa-file-medical',
   205                  title: 'DIFF',
   206                  key: 'diff',
   207                  content: (
   208                      <DataLoader
   209                          key='diff'
   210                          load={async () =>
   211                              await services.applications.managedResources(application.metadata.name, application.metadata.namespace, {
   212                                  fields: ['items.normalizedLiveState', 'items.predictedLiveState', 'items.group', 'items.kind', 'items.namespace', 'items.name']
   213                              })
   214                          }>
   215                          {managedResources => <ApplicationResourcesDiff states={managedResources} />}
   216                      </DataLoader>
   217                  )
   218              });
   219          }
   220  
   221          tabs.push({
   222              title: 'EVENTS',
   223              key: 'event',
   224              content: <ApplicationResourceEvents applicationName={application.metadata.name} applicationNamespace={application.metadata.namespace} />
   225          });
   226  
   227          const extensionTabs = services.extensions.getResourceTabs('argoproj.io', 'Application').map((ext, i) => ({
   228              title: ext.title,
   229              key: `extension-${i}`,
   230              content: <ext.component resource={application} tree={tree} application={application} />,
   231              icon: ext.icon
   232          }));
   233  
   234          return tabs.concat(extensionTabs);
   235      };
   236  
   237      const extensions = selectedNode?.kind ? services.extensions.getResourceTabs(selectedNode?.group || '', selectedNode?.kind) : [];
   238  
   239      return (
   240          <div style={{width: '100%', height: '100%'}}>
   241              {selectedNode && (
   242                  <DataLoader
   243                      noLoaderOnInputChange={true}
   244                      input={selectedNode.resourceVersion}
   245                      load={async () => {
   246                          const managedResources = await services.applications.managedResources(application.metadata.name, application.metadata.namespace, {
   247                              id: {
   248                                  name: selectedNode.name,
   249                                  namespace: selectedNode.namespace,
   250                                  kind: selectedNode.kind,
   251                                  group: selectedNode.group
   252                              }
   253                          });
   254                          const controlled = managedResources.find(item => AppUtils.isSameNode(selectedNode, item));
   255                          const summary = application.status.resources.find(item => AppUtils.isSameNode(selectedNode, item));
   256                          const controlledState = (controlled && summary && {summary, state: controlled}) || null;
   257                          const resQuery = {...selectedNode};
   258                          if (controlled && controlled.targetState) {
   259                              resQuery.version = AppUtils.parseApiVersion(controlled.targetState.apiVersion).version;
   260                          }
   261                          const liveState = await services.applications.getResource(application.metadata.name, application.metadata.namespace, resQuery).catch(() => null);
   262                          const events =
   263                              (liveState &&
   264                                  (await services.applications.resourceEvents(application.metadata.name, application.metadata.namespace, {
   265                                      name: liveState.metadata.name,
   266                                      namespace: liveState.metadata.namespace,
   267                                      uid: liveState.metadata.uid
   268                                  }))) ||
   269                              [];
   270                          let podState: State;
   271                          let childResources: models.ResourceNode[] = [];
   272                          if (selectedNode.kind === 'Pod') {
   273                              podState = liveState;
   274                          } else {
   275                              const childPod = AppUtils.findChildPod(selectedNode, tree);
   276                              if (childPod) {
   277                                  podState = await services.applications.getResource(application.metadata.name, application.metadata.namespace, childPod).catch(() => null);
   278                              }
   279                              childResources = AppUtils.findChildResources(selectedNode, tree);
   280                          }
   281  
   282                          const settings = await services.authService.settings();
   283                          const execEnabled = settings.execEnabled;
   284                          const logsAllowed = await services.accounts.canI('logs', 'get', application.spec.project + '/' + application.metadata.name);
   285                          const execAllowed = execEnabled && (await services.accounts.canI('exec', 'create', application.spec.project + '/' + application.metadata.name));
   286                          const links = await services.applications.getResourceLinks(application.metadata.name, application.metadata.namespace, selectedNode).catch(() => null);
   287                          const resourceActionsMenuItems = await AppUtils.getResourceActionsMenuItems(selectedNode, application.metadata, appContext);
   288                          return {controlledState, liveState, events, podState, execEnabled, execAllowed, logsAllowed, links, childResources, resourceActionsMenuItems};
   289                      }}>
   290                      {data => (
   291                          <React.Fragment>
   292                              <div className='resource-details__header'>
   293                                  <div style={{display: 'flex', flexDirection: 'column', marginRight: '15px', alignItems: 'center', fontSize: '12px'}}>
   294                                      <ResourceIcon kind={selectedNode.kind} />
   295                                      {ResourceLabel({kind: selectedNode.kind})}
   296                                  </div>
   297                                  <h1>{selectedNode.name}</h1>
   298                                  {data.controlledState && (
   299                                      <span style={{marginRight: '5px'}}>
   300                                          <AppUtils.ComparisonStatusIcon status={data.controlledState.summary.status} resource={data.controlledState.summary} />
   301                                      </span>
   302                                  )}
   303                                  {(selectedNode as ResourceTreeNode).health && <AppUtils.HealthStatusIcon state={(selectedNode as ResourceTreeNode).health} />}
   304                                  <button
   305                                      onClick={() => appContext.navigation.goto('.', {deploy: AppUtils.nodeKey(selectedNode)}, {replace: true})}
   306                                      style={{marginLeft: 'auto', marginRight: '5px'}}
   307                                      className='argo-button argo-button--base'>
   308                                      <i className='fa fa-sync-alt' /> <span className='show-for-large'>SYNC</span>
   309                                  </button>
   310                                  <button
   311                                      onClick={() => AppUtils.deletePopup(appContext, selectedNode, application, !!data.controlledState, data.childResources)}
   312                                      style={{marginRight: '5px'}}
   313                                      className='argo-button argo-button--base'>
   314                                      <i className='fa fa-trash' /> <span className='show-for-large'>DELETE</span>
   315                                  </button>
   316                                  {data.resourceActionsMenuItems?.length > 0 && (
   317                                      <DropDown
   318                                          isMenu={true}
   319                                          anchor={() => (
   320                                              <button className='argo-button argo-button--light argo-button--lg argo-button--short'>
   321                                                  <i className='fa fa-ellipsis-v' />
   322                                              </button>
   323                                          )}>
   324                                          {() => AppUtils.renderResourceActionMenu(data.resourceActionsMenuItems)}
   325                                      </DropDown>
   326                                  )}
   327                              </div>
   328                              <Tabs
   329                                  navTransparent={true}
   330                                  tabs={getResourceTabs(
   331                                      selectedNode,
   332                                      data.liveState,
   333                                      data.podState,
   334                                      data.events,
   335                                      extensions,
   336                                      [
   337                                          {
   338                                              title: 'SUMMARY',
   339                                              icon: 'fa fa-file-alt',
   340                                              key: 'summary',
   341                                              content: (
   342                                                  <ApplicationNodeInfo
   343                                                      application={application}
   344                                                      live={data.liveState}
   345                                                      controlled={data.controlledState}
   346                                                      node={selectedNode}
   347                                                      links={data.links}
   348                                                  />
   349                                              )
   350                                          }
   351                                      ],
   352                                      data.execEnabled,
   353                                      data.execAllowed,
   354                                      data.logsAllowed
   355                                  )}
   356                                  selectedTabKey={props.tab}
   357                                  onTabSelected={selected => appContext.navigation.goto('.', {tab: selected}, {replace: true})}
   358                              />
   359                          </React.Fragment>
   360                      )}
   361                  </DataLoader>
   362              )}
   363              {isAppSelected && (
   364                  <Tabs
   365                      navTransparent={true}
   366                      tabs={getApplicationTabs()}
   367                      selectedTabKey={tab}
   368                      onTabSelected={selected => appContext.navigation.goto('.', {tab: selected}, {replace: true})}
   369                  />
   370              )}
   371          </div>
   372      );
   373  };