github.com/freiheit-com/kuberpult@v1.24.2-0.20240328135542-315d5630abe6/services/frontend-service/src/ui/components/ReleaseCard/ReleaseCard.tsx (about)

     1  /*This file is part of kuberpult.
     2  
     3  Kuberpult is free software: you can redistribute it and/or modify
     4  it under the terms of the Expat(MIT) License as published by
     5  the Free Software Foundation.
     6  
     7  Kuberpult is distributed in the hope that it will be useful,
     8  but WITHOUT ANY WARRANTY; without even the implied warranty of
     9  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    10  MIT License for more details.
    11  
    12  You should have received a copy of the MIT License
    13  along with kuberpult. If not, see <https://directory.fsf.org/wiki/License:Expat>.
    14  
    15  Copyright 2023 freiheit.com*/
    16  import classNames from 'classnames';
    17  import React from 'react';
    18  import {
    19      useCurrentlyDeployedAtGroup,
    20      useOpenReleaseDialog,
    21      useReleaseOrThrow,
    22      useRolloutStatus,
    23      EnvironmentGroupExtended,
    24  } from '../../utils/store';
    25  import { Tooltip } from '../tooltip/tooltip';
    26  import { EnvironmentGroupChipList } from '../chip/EnvironmentGroupChip';
    27  import { FormattedDate } from '../FormattedDate/FormattedDate';
    28  import { RolloutStatus } from '../../../api/api';
    29  import { ReleaseVersion } from '../ReleaseVersion/ReleaseVersion';
    30  import { RolloutStatusDescription } from '../RolloutStatusDescription/RolloutStatusDescription';
    31  
    32  export type ReleaseCardProps = {
    33      className?: string;
    34      version: number;
    35      app: string;
    36  };
    37  
    38  const RolloutStatusIcon: React.FC<{ status: RolloutStatus }> = (props) => {
    39      const { status } = props;
    40      switch (status) {
    41          case RolloutStatus.ROLLOUT_STATUS_SUCCESFUL:
    42              return <span className="rollout__icon_successful">✓</span>;
    43          case RolloutStatus.ROLLOUT_STATUS_PROGRESSING:
    44              return <span className="rollout__icon_progressing">↻</span>;
    45          case RolloutStatus.ROLLOUT_STATUS_PENDING:
    46              return <span className="rollout__icon_pending">⧖</span>;
    47          case RolloutStatus.ROLLOUT_STATUS_ERROR:
    48              return <span className="rollout__icon_error">!</span>;
    49          case RolloutStatus.ROLLOUT_STATUS_UNHEALTHY:
    50              return <span className="rollout__icon_unhealthy">⚠</span>;
    51      }
    52      return <span className="rollout__icon_unknown">?</span>;
    53  };
    54  
    55  // note that the order is important here.
    56  // "most interesting" must come first.
    57  // see `calculateDeploymentStatus`
    58  // The same priority list is also implemented in pkg/service/broadcast.go.
    59  const rolloutStatusPriority = [
    60      // Error is not recoverable by waiting and requires manual intervention
    61      RolloutStatus.ROLLOUT_STATUS_ERROR,
    62  
    63      // These states may resolve by waiting longer
    64      RolloutStatus.ROLLOUT_STATUS_PROGRESSING,
    65      RolloutStatus.ROLLOUT_STATUS_UNHEALTHY,
    66      RolloutStatus.ROLLOUT_STATUS_PENDING,
    67      RolloutStatus.ROLLOUT_STATUS_UNKNOWN,
    68  
    69      // This is the only successful state
    70      RolloutStatus.ROLLOUT_STATUS_SUCCESFUL,
    71  ];
    72  
    73  const getRolloutStatusPriority = (status: RolloutStatus): number => {
    74      const idx = rolloutStatusPriority.indexOf(status);
    75      if (idx === -1) {
    76          return rolloutStatusPriority.length;
    77      }
    78      return idx;
    79  };
    80  
    81  type DeploymentStatus = {
    82      environmentGroup: string;
    83      rolloutStatus: RolloutStatus;
    84  };
    85  
    86  const useDeploymentStatus = (
    87      app: string,
    88      deployedAt: EnvironmentGroupExtended[]
    89  ): [Array<DeploymentStatus>, RolloutStatus?] => {
    90      const rolloutEnvGroups = useRolloutStatus((getter) => {
    91          const groups: { [envGroup: string]: RolloutStatus } = {};
    92          deployedAt.forEach((envGroup) => {
    93              const status = envGroup.environments.reduce((cur: RolloutStatus | undefined, env) => {
    94                  const appVersion: number | undefined = env.applications[app]?.version;
    95                  const status = getter.getAppStatus(app, appVersion, env.name);
    96                  if (cur === undefined) {
    97                      return status;
    98                  }
    99                  if (status === undefined) {
   100                      return cur;
   101                  }
   102                  if (getRolloutStatusPriority(status) < getRolloutStatusPriority(cur)) {
   103                      return status;
   104                  }
   105                  return cur;
   106              }, undefined);
   107              groups[envGroup.environmentGroupName] = status ?? RolloutStatus.ROLLOUT_STATUS_UNKNOWN;
   108          });
   109          return groups;
   110      });
   111      const rolloutEnvGroupsArray = Object.entries(rolloutEnvGroups).map((e) => ({
   112          environmentGroup: e[0],
   113          rolloutStatus: e[1],
   114      }));
   115      rolloutEnvGroupsArray.sort((a, b) => {
   116          if (a.environmentGroup < b.environmentGroup) {
   117              return -1;
   118          } else if (a.environmentGroup > b.environmentGroup) {
   119              return 1;
   120          }
   121          return 0;
   122      });
   123      // Calculates the most interesting rollout status according to the `rolloutStatusPriority`.
   124      const mostInteresting = rolloutEnvGroupsArray.reduce(
   125          (cur: RolloutStatus | undefined, item) =>
   126              cur === undefined
   127                  ? item.rolloutStatus
   128                  : getRolloutStatusPriority(item.rolloutStatus) < getRolloutStatusPriority(cur)
   129                    ? item.rolloutStatus
   130                    : cur,
   131          undefined
   132      );
   133      return [rolloutEnvGroupsArray, mostInteresting];
   134  };
   135  
   136  export const ReleaseCard: React.FC<ReleaseCardProps> = (props) => {
   137      const { className, app, version } = props;
   138      // the ReleaseCard only displays actual releases, so we can assume that it exists here:
   139      const release = useReleaseOrThrow(app, version);
   140      const { createdAt, sourceMessage, sourceAuthor, undeployVersion } = release;
   141      const openReleaseDialog = useOpenReleaseDialog(app, version);
   142      const deployedAt = useCurrentlyDeployedAtGroup(app, version);
   143  
   144      const [rolloutEnvs, mostInteresting] = useDeploymentStatus(app, deployedAt);
   145  
   146      const tooltipContents = (
   147          <div className="mdc-tooltip__title_ release__details">
   148              {!!sourceMessage && <b>{sourceMessage}</b>}
   149              {!!sourceAuthor && (
   150                  <div>
   151                      <span>Author:</span> {sourceAuthor}
   152                  </div>
   153              )}
   154              {!!createdAt && (
   155                  <div className="release__metadata">
   156                      <span>Created </span>
   157                      <FormattedDate className={'date'} createdAt={createdAt} />
   158                  </div>
   159              )}
   160              {rolloutEnvs.length > 0 && (
   161                  <table className="release__environment_status">
   162                      <thead>
   163                          <tr>
   164                              <th>Environment group</th>
   165                              <th>Rollout</th>
   166                          </tr>
   167                      </thead>
   168                      <tbody>
   169                          {rolloutEnvs.map((env) => (
   170                              <tr key={env.environmentGroup}>
   171                                  <td>{env.environmentGroup}</td>
   172                                  <td>
   173                                      <RolloutStatusDescription status={env.rolloutStatus} />
   174                                  </td>
   175                              </tr>
   176                          ))}
   177                      </tbody>
   178                  </table>
   179              )}
   180          </div>
   181      );
   182  
   183      const firstLine = sourceMessage.split('\n')[0];
   184      return (
   185          <Tooltip id={app + version} tooltipContent={tooltipContents}>
   186              <div className="release-card__container">
   187                  <div className="release__environments">
   188                      <EnvironmentGroupChipList app={props.app} version={props.version} smallEnvChip />
   189                  </div>
   190                  <div className={classNames('mdc-card release-card', className)}>
   191                      <div
   192                          className="mdc-card__primary-action release-card__description"
   193                          // ref={control}
   194                          tabIndex={0}
   195                          onClick={openReleaseDialog}>
   196                          <div className="release-card__header">
   197                              <div className="release__title">{undeployVersion ? 'Undeploy Version' : firstLine}</div>
   198                              <ReleaseVersion release={release} />
   199                          </div>
   200                          {mostInteresting !== undefined && (
   201                              <div className="release__status">
   202                                  <RolloutStatusIcon status={mostInteresting} />
   203                              </div>
   204                          )}
   205                          <div className="mdc-card__ripple" />
   206                      </div>
   207                  </div>
   208              </div>
   209          </Tooltip>
   210      );
   211  };