github.com/freiheit-com/kuberpult@v1.24.2-0.20240328135542-315d5630abe6/services/frontend-service/src/ui/components/ReleaseDialog/ReleaseDialog.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, { ReactElement, useCallback } from 'react';
    18  import { Environment, Environment_Application, EnvironmentGroup, Lock, LockBehavior, Release } from '../../../api/api';
    19  import {
    20      addAction,
    21      useCloseReleaseDialog,
    22      useEnvironmentGroups,
    23      useReleaseOptional,
    24      useReleaseOrThrow,
    25      useRolloutStatus,
    26      useTeamFromApplication,
    27  } from '../../utils/store';
    28  import { Button } from '../button';
    29  import { Close, Locks } from '../../../images';
    30  import { EnvironmentChip } from '../chip/EnvironmentGroupChip';
    31  import { FormattedDate } from '../FormattedDate/FormattedDate';
    32  import {
    33      ArgoAppLink,
    34      ArgoTeamLink,
    35      DisplayManifestLink,
    36      DisplaySourceLink,
    37      DisplayCommitHistoryLink,
    38  } from '../../utils/Links';
    39  import { ReleaseVersion } from '../ReleaseVersion/ReleaseVersion';
    40  import { PlainDialog } from '../dialog/ConfirmationDialog';
    41  import { ExpandButton } from '../button/ExpandButton';
    42  import { RolloutStatusDescription } from '../RolloutStatusDescription/RolloutStatusDescription';
    43  
    44  export type ReleaseDialogProps = {
    45      className?: string;
    46      app: string;
    47      version: number;
    48  };
    49  
    50  export const AppLock: React.FC<{
    51      env: Environment;
    52      app: string;
    53      lock: Lock;
    54  }> = ({ env, app, lock }) => {
    55      const deleteAppLock = useCallback(() => {
    56          addAction({
    57              action: {
    58                  $case: 'deleteEnvironmentApplicationLock',
    59                  deleteEnvironmentApplicationLock: { environment: env.name, application: app, lockId: lock.lockId },
    60              },
    61          });
    62      }, [app, env.name, lock.lockId]);
    63      return (
    64          <div
    65              title={'App Lock Message: "' + lock.message + '" | ID: "' + lock.lockId + '"  | Click to unlock. '}
    66              onClick={deleteAppLock}>
    67              <Button icon={<Locks className="env-card-app-lock" />} className={'button-lock'} />
    68          </div>
    69      );
    70  };
    71  
    72  export type EnvironmentListItemProps = {
    73      env: Environment;
    74      envGroup: EnvironmentGroup;
    75      app: string;
    76      release: Release;
    77      queuedVersion: number;
    78      className?: string;
    79  };
    80  
    81  type CommitIdProps = {
    82      application: Environment_Application;
    83      app: string;
    84      env: Environment;
    85      otherRelease?: Release;
    86  };
    87  
    88  const DeployedVersion: React.FC<CommitIdProps> = ({ application, app, env, otherRelease }): ReactElement => {
    89      if (!application || !otherRelease) {
    90          return (
    91              <span>
    92                  "{app}" has no version deployed on "{env.name}"
    93              </span>
    94          );
    95      }
    96      const firstLine = otherRelease.sourceMessage.split('\n')[0];
    97      return (
    98          <span>
    99              <ReleaseVersion release={otherRelease} />
   100              {firstLine}
   101          </span>
   102      );
   103  };
   104  
   105  export const EnvironmentListItem: React.FC<EnvironmentListItemProps> = ({
   106      env,
   107      envGroup,
   108      app,
   109      release,
   110      queuedVersion,
   111      className,
   112  }) => {
   113      const createAppLock = useCallback(() => {
   114          addAction({
   115              action: {
   116                  $case: 'createEnvironmentApplicationLock',
   117                  createEnvironmentApplicationLock: {
   118                      environment: env.name,
   119                      application: app,
   120                      lockId: '',
   121                      message: '',
   122                  },
   123              },
   124          });
   125      }, [app, env.name]);
   126      const deployAndLockClick = useCallback(
   127          (shouldLockToo: boolean) => {
   128              if (release.version) {
   129                  addAction({
   130                      action: {
   131                          $case: 'deploy',
   132                          deploy: {
   133                              environment: env.name,
   134                              application: app,
   135                              version: release.version,
   136                              ignoreAllLocks: false,
   137                              lockBehavior: LockBehavior.IGNORE,
   138                          },
   139                      },
   140                  });
   141                  if (shouldLockToo) {
   142                      createAppLock();
   143                  }
   144              }
   145          },
   146          [release.version, app, env.name, createAppLock]
   147      );
   148  
   149      const queueInfo =
   150          queuedVersion === 0 ? null : (
   151              <div
   152                  className={classNames('env-card-data env-card-data-queue', className)}
   153                  title={
   154                      'An attempt was made to deploy version ' +
   155                      queuedVersion +
   156                      ' either by a release train, or when a new version was created. However, there was a lock present at the time, so kuberpult did not deploy this version. '
   157                  }>
   158                  Version {queuedVersion} was not deployed, because of a lock.
   159              </div>
   160          );
   161      const otherRelease = useReleaseOptional(app, env);
   162      const application = env.applications[app];
   163      const getDeploymentMetadata = (): [String, JSX.Element] => {
   164          if (!application) {
   165              return ['', <></>];
   166          }
   167          if (application.deploymentMetaData === null) {
   168              return ['', <></>];
   169          }
   170          const deployedBy = application.deploymentMetaData?.deployAuthor ?? 'unknown';
   171          const deployedUNIX = application.deploymentMetaData?.deployTime ?? '';
   172          if (deployedUNIX === '') {
   173              return ['Deployed by &nbsp;' + deployedBy, <></>];
   174          }
   175          const deployedDate = new Date(+deployedUNIX * 1000);
   176          const returnString = 'Deployed by ' + deployedBy + ' ';
   177          const time = (
   178              <FormattedDate createdAt={deployedDate} className={classNames('release-dialog-createdAt', className)} />
   179          );
   180  
   181          return [returnString, time];
   182      };
   183      const appRolloutStatus = useRolloutStatus((getter) => getter.getAppStatus(app, application?.version, env.name));
   184      return (
   185          <li key={env.name} className={classNames('env-card', className)}>
   186              <div className="env-card-header">
   187                  <EnvironmentChip
   188                      env={env}
   189                      app={app}
   190                      envGroup={envGroup}
   191                      className={'release-environment'}
   192                      key={env.name}
   193                      groupNameOverride={undefined}
   194                      numberEnvsDeployed={undefined}
   195                      numberEnvsInGroup={undefined}
   196                  />
   197                  <div className={classNames('env-card-app-locks')}>
   198                      {Object.values(env.applications)
   199                          .filter((application) => application.name === app)
   200                          .map((app) => app.locks)
   201                          .map((locks) =>
   202                              Object.values(locks).map((lock) => (
   203                                  <AppLock key={lock.lockId} env={env} app={app} lock={lock} />
   204                              ))
   205                          )}
   206                      {appRolloutStatus && <RolloutStatusDescription status={appRolloutStatus} />}
   207                  </div>
   208              </div>
   209              <div className="content-area">
   210                  <div className="content-left">
   211                      <div
   212                          className={classNames('env-card-data', className)}
   213                          title={
   214                              'Shows the version that is currently deployed on ' +
   215                              env.name +
   216                              '. ' +
   217                              (release.undeployVersion ? undeployTooltipExplanation : '')
   218                          }>
   219                          <DeployedVersion app={app} env={env} application={application} otherRelease={otherRelease} />
   220                      </div>
   221                      {queueInfo}
   222                      <div className={classNames('env-card-data', className)}>
   223                          {getDeploymentMetadata().flatMap((metadata, i) => (
   224                              <div key={i}>{metadata}&nbsp;</div>
   225                          ))}
   226                      </div>
   227                  </div>
   228                  <div className="content-right">
   229                      <div className="env-card-buttons">
   230                          <Button
   231                              className="env-card-add-lock-btn"
   232                              label="Add lock"
   233                              onClick={createAppLock}
   234                              icon={<Locks className="icon" />}
   235                          />
   236                          <div
   237                              title={
   238                                  'When doing manual deployments, it is usually best to also lock the app. If you omit the lock, an automatic release train or another person may deploy an unintended version. If you do not want a lock, click the arrow.'
   239                              }>
   240                              <ExpandButton onClickSubmit={deployAndLockClick} defaultButtonLabel={'Deploy & Lock'} />
   241                          </div>
   242                      </div>
   243                  </div>
   244              </div>
   245          </li>
   246      );
   247  };
   248  
   249  export const EnvironmentList: React.FC<{
   250      release: Release;
   251      app: string;
   252      version: number;
   253      className?: string;
   254  }> = ({ release, app, version, className }) => {
   255      const allEnvGroups: EnvironmentGroup[] = useEnvironmentGroups();
   256      return (
   257          <div className="release-env-group-list">
   258              {allEnvGroups.map((envGroup) => (
   259                  <ul className={classNames('release-env-list', className)} key={envGroup.environmentGroupName}>
   260                      {envGroup.environments.map((env) => (
   261                          <EnvironmentListItem
   262                              key={env.name}
   263                              env={env}
   264                              envGroup={envGroup}
   265                              app={app}
   266                              release={release}
   267                              className={className}
   268                              queuedVersion={env.applications[app] ? env.applications[app].queuedVersion : 0}
   269                          />
   270                      ))}
   271                  </ul>
   272              ))}
   273          </div>
   274      );
   275  };
   276  
   277  export const undeployTooltipExplanation =
   278      'This is the "undeploy" version. It is essentially an empty manifest. Deploying this means removing all kubernetes entities like deployments from the given environment. You must deploy this to all environments before kuberpult allows to delete the app entirely.';
   279  
   280  export const ReleaseDialog: React.FC<ReleaseDialogProps> = (props) => {
   281      const { app, className, version } = props;
   282      // the ReleaseDialog is only opened when there is a release, so we can assume that it exists here:
   283      const release = useReleaseOrThrow(app, version);
   284      const team = useTeamFromApplication(app);
   285      const closeReleaseDialog = useCloseReleaseDialog();
   286  
   287      const dialog: JSX.Element | '' = (
   288          <PlainDialog
   289              open={app !== ''}
   290              onClose={closeReleaseDialog}
   291              classNames={'release-dialog'}
   292              disableBackground={true}
   293              center={true}>
   294              <>
   295                  <div className={classNames('release-dialog-app-bar', className)}>
   296                      <div className={classNames('release-dialog-app-bar-data')}>
   297                          <div className={classNames('release-dialog-message', className)}>
   298                              <span className={classNames('release-dialog-commitMessage', className)}>
   299                                  {release?.sourceMessage}
   300                              </span>
   301                          </div>
   302                          <div className="source">
   303                              <span>
   304                                  {'Created '}
   305                                  {release?.createdAt ? (
   306                                      <FormattedDate
   307                                          createdAt={release.createdAt}
   308                                          className={classNames('release-dialog-createdAt', className)}
   309                                      />
   310                                  ) : (
   311                                      'at an unknown date'
   312                                  )}
   313                                  {' by '}
   314                                  {release?.sourceAuthor ? release?.sourceAuthor : 'an unknown author'}{' '}
   315                              </span>
   316                              <span className="links">
   317                                  <DisplaySourceLink commitId={release.sourceCommitId} displayString={'Source'} />
   318                                  &nbsp;
   319                                  <DisplayManifestLink app={app} version={release.version} displayString="Manifest" />
   320                                  &nbsp;
   321                                  <DisplayCommitHistoryLink
   322                                      commitId={release.sourceCommitId}
   323                                      displayString={'Commit History'}
   324                                  />
   325                              </span>
   326                          </div>
   327                          <div className={classNames('release-dialog-app', className)}>
   328                              {'App: '}
   329                              <ArgoAppLink app={app} />
   330                              <ArgoTeamLink team={team} />
   331                          </div>
   332                      </div>
   333                      <Button
   334                          onClick={closeReleaseDialog}
   335                          className={classNames('release-dialog-close', className)}
   336                          icon={<Close />}
   337                      />
   338                  </div>
   339                  <EnvironmentList app={app} className={className} release={release} version={version} />
   340              </>
   341          </PlainDialog>
   342      );
   343      return <div>{dialog}</div>;
   344  };