github.com/argoproj/argo-cd/v2@v2.10.9/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 {Context} from '../../../shared/context';
     8  import {Application, ApplicationTree, AppSourceType, Event, RepoAppDetails, 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  }
    35  
    36  export const ResourceDetails = (props: ResourceDetailsProps) => {
    37      const {selectedNode, updateApp, application, isAppSelected, tree} = {...props};
    38      const [activeContainer, setActiveContainer] = useState();
    39      const appContext = React.useContext(Context);
    40      const tab = new URLSearchParams(appContext.history.location.search).get('tab');
    41      const selectedNodeInfo = NodeInfo(new URLSearchParams(appContext.history.location.search).get('node'));
    42      const selectedNodeKey = selectedNodeInfo.key;
    43  
    44      const getResourceTabs = (
    45          node: ResourceNode,
    46          state: State,
    47          podState: State,
    48          events: Event[],
    49          extensionTabs: ResourceTabExtension[],
    50          tabs: Tab[],
    51          execEnabled: boolean,
    52          execAllowed: boolean,
    53          logsAllowed: boolean
    54      ) => {
    55          if (!node || node === undefined) {
    56              return [];
    57          }
    58          if (state) {
    59              const numErrors = events.filter(event => event.type !== 'Normal').reduce((total, event) => total + event.count, 0);
    60              tabs.push({
    61                  title: 'EVENTS',
    62                  icon: 'fa fa-calendar-alt',
    63                  badge: (numErrors > 0 && numErrors) || null,
    64                  key: 'events',
    65                  content: (
    66                      <div className='application-resource-events'>
    67                          <EventsList events={events} />
    68                      </div>
    69                  )
    70              });
    71          }
    72          if (podState && podState.metadata && podState.spec) {
    73              const containerGroups = [
    74                  {
    75                      offset: 0,
    76                      title: 'CONTAINERS',
    77                      containers: podState.spec.containers || []
    78                  }
    79              ];
    80              if (podState.spec.initContainers?.length > 0) {
    81                  containerGroups.push({
    82                      offset: (podState.spec.containers || []).length,
    83                      title: 'INIT CONTAINERS',
    84                      containers: podState.spec.initContainers || []
    85                  });
    86              }
    87  
    88              const onClickContainer = (group: any, i: number, activeTab: string) => {
    89                  setActiveContainer(group.offset + i);
    90                  SelectNode(selectedNodeKey, activeContainer, activeTab, appContext);
    91              };
    92  
    93              if (logsAllowed) {
    94                  tabs = tabs.concat([
    95                      {
    96                          key: 'logs',
    97                          icon: 'fa fa-align-left',
    98                          title: 'LOGS',
    99                          content: (
   100                              <div className='application-details__tab-content-full-height'>
   101                                  <PodsLogsViewer
   102                                      podName={(state.kind === 'Pod' && state.metadata.name) || ''}
   103                                      group={node.group}
   104                                      kind={node.kind}
   105                                      name={node.name}
   106                                      namespace={podState.metadata.namespace}
   107                                      applicationName={application.metadata.name}
   108                                      applicationNamespace={application.metadata.namespace}
   109                                      containerName={AppUtils.getContainerName(podState, activeContainer)}
   110                                      containerGroups={containerGroups}
   111                                      onClickContainer={onClickContainer}
   112                                  />
   113                              </div>
   114                          )
   115                      }
   116                  ]);
   117              }
   118              if (selectedNode.kind === 'Pod' && execEnabled && execAllowed) {
   119                  tabs = tabs.concat([
   120                      {
   121                          key: 'exec',
   122                          icon: 'fa fa-terminal',
   123                          title: 'Terminal',
   124                          content: (
   125                              <PodTerminalViewer
   126                                  applicationName={application.metadata.name}
   127                                  applicationNamespace={application.metadata.namespace}
   128                                  projectName={application.spec.project}
   129                                  podState={podState}
   130                                  selectedNode={selectedNode}
   131                                  containerName={AppUtils.getContainerName(podState, activeContainer)}
   132                                  onClickContainer={onClickContainer}
   133                              />
   134                          )
   135                      }
   136                  ]);
   137              }
   138          }
   139          if (state) {
   140              extensionTabs.forEach((tabExtensions, i) => {
   141                  tabs.push({
   142                      title: tabExtensions.title,
   143                      key: `extension-${i}`,
   144                      content: (
   145                          <ErrorBoundary message={`Something went wrong with Extension for ${state.kind}`}>
   146                              <tabExtensions.component tree={tree} resource={state} application={application} />
   147                          </ErrorBoundary>
   148                      ),
   149                      icon: tabExtensions.icon
   150                  });
   151              });
   152          }
   153          return tabs;
   154      };
   155  
   156      const getApplicationTabs = () => {
   157          const tabs: Tab[] = [
   158              {
   159                  title: 'SUMMARY',
   160                  key: 'summary',
   161                  content: <ApplicationSummary app={application} updateApp={(app, query: {validate?: boolean}) => updateApp(app, query)} />
   162              },
   163              {
   164                  title: 'PARAMETERS',
   165                  key: 'parameters',
   166                  content: (
   167                      <DataLoader
   168                          key='appDetails'
   169                          input={application}
   170                          load={app =>
   171                              services.repos.appDetails(AppUtils.getAppDefaultSource(app), app.metadata.name, app.spec.project).catch(() => ({
   172                                  type: 'Directory' as AppSourceType,
   173                                  path: AppUtils.getAppDefaultSource(app).path
   174                              }))
   175                          }>
   176                          {(details: RepoAppDetails) => (
   177                              <ApplicationParameters
   178                                  save={(app: models.Application, query: {validate?: boolean}) => updateApp(app, query)}
   179                                  application={application}
   180                                  details={details}
   181                              />
   182                          )}
   183                      </DataLoader>
   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                          if (selectedNode.kind === 'Pod') {
   272                              podState = liveState;
   273                          } else {
   274                              const childPod = AppUtils.findChildPod(selectedNode, tree);
   275                              if (childPod) {
   276                                  podState = await services.applications.getResource(application.metadata.name, application.metadata.namespace, childPod).catch(() => null);
   277                              }
   278                          }
   279  
   280                          const settings = await services.authService.settings();
   281                          const execEnabled = settings.execEnabled;
   282                          const logsAllowed = await services.accounts.canI('logs', 'get', application.spec.project + '/' + application.metadata.name);
   283                          const execAllowed = execEnabled && (await services.accounts.canI('exec', 'create', application.spec.project + '/' + application.metadata.name));
   284                          const links = await services.applications.getResourceLinks(application.metadata.name, application.metadata.namespace, selectedNode).catch(() => null);
   285                          return {controlledState, liveState, events, podState, execEnabled, execAllowed, logsAllowed, links};
   286                      }}>
   287                      {data => (
   288                          <React.Fragment>
   289                              <div className='resource-details__header'>
   290                                  <div style={{display: 'flex', flexDirection: 'column', marginRight: '15px', alignItems: 'center', fontSize: '12px'}}>
   291                                      <ResourceIcon kind={selectedNode.kind} />
   292                                      {ResourceLabel({kind: selectedNode.kind})}
   293                                  </div>
   294                                  <h1>{selectedNode.name}</h1>
   295                                  {data.controlledState && (
   296                                      <React.Fragment>
   297                                          <span style={{marginRight: '5px'}}>
   298                                              <AppUtils.ComparisonStatusIcon status={data.controlledState.summary.status} resource={data.controlledState.summary} />
   299                                          </span>
   300                                      </React.Fragment>
   301                                  )}
   302                                  {(selectedNode as ResourceTreeNode).health && <AppUtils.HealthStatusIcon state={(selectedNode as ResourceTreeNode).health} />}
   303                                  <button
   304                                      onClick={() => appContext.navigation.goto('.', {deploy: AppUtils.nodeKey(selectedNode)}, {replace: true})}
   305                                      style={{marginLeft: 'auto', marginRight: '5px'}}
   306                                      className='argo-button argo-button--base'>
   307                                      <i className='fa fa-sync-alt' /> <span className='show-for-large'>SYNC</span>
   308                                  </button>
   309                                  <button
   310                                      onClick={() => AppUtils.deletePopup(appContext, selectedNode, application)}
   311                                      style={{marginRight: '5px'}}
   312                                      className='argo-button argo-button--base'>
   313                                      <i className='fa fa-trash' /> <span className='show-for-large'>DELETE</span>
   314                                  </button>
   315                                  <DropDown
   316                                      isMenu={true}
   317                                      anchor={() => (
   318                                          <button className='argo-button argo-button--light argo-button--lg argo-button--short'>
   319                                              <i className='fa fa-ellipsis-v' />
   320                                          </button>
   321                                      )}>
   322                                      {() => AppUtils.renderResourceActionMenu(selectedNode, application, appContext)}
   323                                  </DropDown>
   324                              </div>
   325                              <Tabs
   326                                  navTransparent={true}
   327                                  tabs={getResourceTabs(
   328                                      selectedNode,
   329                                      data.liveState,
   330                                      data.podState,
   331                                      data.events,
   332                                      extensions,
   333                                      [
   334                                          {
   335                                              title: 'SUMMARY',
   336                                              icon: 'fa fa-file-alt',
   337                                              key: 'summary',
   338                                              content: (
   339                                                  <ApplicationNodeInfo
   340                                                      application={application}
   341                                                      live={data.liveState}
   342                                                      controlled={data.controlledState}
   343                                                      node={selectedNode}
   344                                                      links={data.links}
   345                                                  />
   346                                              )
   347                                          }
   348                                      ],
   349                                      data.execEnabled,
   350                                      data.execAllowed,
   351                                      data.logsAllowed
   352                                  )}
   353                                  selectedTabKey={props.tab}
   354                                  onTabSelected={selected => appContext.navigation.goto('.', {tab: selected}, {replace: true})}
   355                              />
   356                          </React.Fragment>
   357                      )}
   358                  </DataLoader>
   359              )}
   360              {isAppSelected && (
   361                  <Tabs
   362                      navTransparent={true}
   363                      tabs={getApplicationTabs()}
   364                      selectedTabKey={tab}
   365                      onTabSelected={selected => appContext.navigation.goto('.', {tab: selected}, {replace: true})}
   366                  />
   367              )}
   368          </div>
   369      );
   370  };