github.com/freiheit-com/kuberpult@v1.24.2-0.20240328135542-315d5630abe6/services/frontend-service/src/ui/components/ServiceLane/ServiceLane.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 {
    17      addAction,
    18      EnvironmentGroupExtended,
    19      showSnackbarError,
    20      showSnackbarWarn,
    21      useCurrentlyExistsAtGroup,
    22      useDeployedReleases,
    23      useFilteredApplicationLocks,
    24      useNavigateWithSearchParams,
    25      useVersionsForApp,
    26  } from '../../utils/store';
    27  import { ReleaseCard } from '../ReleaseCard/ReleaseCard';
    28  import { DeleteWhite, HistoryWhite } from '../../../images';
    29  import { Application, Environment, UndeploySummary } from '../../../api/api';
    30  import * as React from 'react';
    31  import { AppLockSummary } from '../chip/EnvironmentGroupChip';
    32  import { WarningBoxes } from './Warnings';
    33  import { DotsMenu, DotsMenuButton } from './DotsMenu';
    34  import { useCallback, useState } from 'react';
    35  import { EnvSelectionDialog } from './EnvSelectionDialog';
    36  
    37  // number of releases on home. based on design
    38  // we could update this dynamically based on viewport width
    39  const numberOfDisplayedReleasesOnHome = 6;
    40  
    41  const getReleasesToDisplay = (deployedReleases: number[], allReleases: number[]): number[] => {
    42      // all deployed releases are important and the latest release is also important
    43      const importantReleases = deployedReleases.includes(allReleases[0])
    44          ? deployedReleases
    45          : [allReleases[0], ...deployedReleases];
    46      // number of remaining releases to get from history
    47      const numOfTrailingReleases = numberOfDisplayedReleasesOnHome - importantReleases.length;
    48      // find the index of the last deployed release e.g. Prod (or -1 when there's no deployed releases)
    49      const oldestImportantReleaseIndex = importantReleases.length
    50          ? allReleases.findIndex((version) => version === importantReleases.slice(-1)[0])
    51          : -1;
    52      // take the deployed releases + a slice from the oldest element (or very first, see above) with total length 6
    53      return [
    54          ...importantReleases,
    55          ...allReleases.slice(oldestImportantReleaseIndex + 1, oldestImportantReleaseIndex + numOfTrailingReleases + 1),
    56      ];
    57  };
    58  
    59  function getNumberOfReleasesBetween(releases: number[], higherVersion: number, lowerVersion: number): number {
    60      // diff = index of lower version (older release) - index of higher version (newer release) - 1
    61      return releases.findIndex((ver) => ver === lowerVersion) - releases.findIndex((ver) => ver === higherVersion) - 1;
    62  }
    63  
    64  const DiffElement: React.FC<{ diff: number; title: string }> = ({ diff, title }) => (
    65      <div className="service-lane__diff--container" title={title}>
    66          <div className="service-lane__diff--dot" />
    67          <div className="service-lane__diff--dot" />
    68          <div className="service-lane__diff--number">{diff}</div>
    69          <div className="service-lane__diff--dot" />
    70          <div className="service-lane__diff--dot" />
    71      </div>
    72  );
    73  
    74  const deriveUndeployMessage = (undeploySummary: UndeploySummary): string | undefined => {
    75      switch (undeploySummary) {
    76          case UndeploySummary.UNDEPLOY:
    77              return 'Delete Forever';
    78          case UndeploySummary.NORMAL:
    79              return 'Prepare Undeploy Release';
    80          case UndeploySummary.MIXED:
    81              return undefined;
    82          default:
    83              return undefined;
    84      }
    85  };
    86  
    87  export const ServiceLane: React.FC<{ application: Application }> = (props) => {
    88      const { application } = props;
    89      const deployedReleases = useDeployedReleases(application.name);
    90      const allReleases = useVersionsForApp(application.name);
    91      const { navCallback } = useNavigateWithSearchParams('releases/' + application.name);
    92      const prepareUndeployOrUndeployText = deriveUndeployMessage(application.undeploySummary);
    93  
    94      const prepareUndeployOrUndeploy = React.useCallback(() => {
    95          switch (application.undeploySummary) {
    96              case UndeploySummary.UNDEPLOY:
    97                  addAction({
    98                      action: {
    99                          $case: 'undeploy',
   100                          undeploy: { application: application.name },
   101                      },
   102                  });
   103                  break;
   104              case UndeploySummary.NORMAL:
   105                  addAction({
   106                      action: {
   107                          $case: 'prepareUndeploy',
   108                          prepareUndeploy: { application: application.name },
   109                      },
   110                  });
   111                  break;
   112              case UndeploySummary.MIXED:
   113                  showSnackbarError('Internal Error: Cannot prepare to undeploy or actual undeploy in mixed state.');
   114                  break;
   115              default:
   116                  showSnackbarError('Internal Error: Cannot prepare to undeploy or actual undeploy in unknown state.');
   117                  break;
   118          }
   119      }, [application.name, application.undeploySummary]);
   120      const releases = getReleasesToDisplay(deployedReleases, allReleases);
   121  
   122      const releases_lane =
   123          !!releases &&
   124          releases.map((rel, index) => {
   125              // diff is releases between current card and the next.
   126              // for the last card, diff is number of remaining releases in history
   127              const diff =
   128                  index < releases.length - 1
   129                      ? getNumberOfReleasesBetween(allReleases, rel, releases[index + 1])
   130                      : getNumberOfReleasesBetween(allReleases, rel, allReleases.slice(-1)[0]) + 1;
   131              return (
   132                  <div key={application.name + '-' + rel} className="service-lane__diff">
   133                      <ReleaseCard app={application.name} version={rel} key={application.name + '-' + rel} />
   134                      {!!diff && (
   135                          <DiffElement
   136                              diff={diff}
   137                              title={'There are ' + diff + ' more releases hidden. Click on History to view more'}
   138                          />
   139                      )}
   140                  </div>
   141              );
   142          });
   143  
   144      const envs: Environment[] = useCurrentlyExistsAtGroup(application.name).flatMap(
   145          (envGroup: EnvironmentGroupExtended) => envGroup.environments
   146      );
   147  
   148      const [showEnvSelectionDialog, setShowEnvSelectionDialog] = useState(false);
   149  
   150      const handleClose = useCallback(() => {
   151          setShowEnvSelectionDialog(false);
   152      }, []);
   153      const confirmEnvAppDelete = useCallback(
   154          (selectedEnvs: string[]) => {
   155              if (selectedEnvs.length === envs.length) {
   156                  showSnackbarWarn("If you want to delete all environments, use 'prepare undeploy'");
   157                  setShowEnvSelectionDialog(false);
   158                  return;
   159              }
   160              selectedEnvs.forEach((env) => {
   161                  addAction({
   162                      action: {
   163                          $case: 'deleteEnvFromApp',
   164                          deleteEnvFromApp: { application: application.name, environment: env },
   165                      },
   166                  });
   167              });
   168              setShowEnvSelectionDialog(false);
   169          },
   170          [application.name, envs]
   171      );
   172      const buttons: DotsMenuButton[] = [
   173          {
   174              label: 'View History',
   175              icon: <HistoryWhite />,
   176              onClick: (): void => {
   177                  navCallback();
   178              },
   179          },
   180          {
   181              label: 'Remove environment from app',
   182              icon: <DeleteWhite />,
   183              onClick: (): void => {
   184                  setShowEnvSelectionDialog(true);
   185              },
   186          },
   187      ];
   188      if (prepareUndeployOrUndeployText) {
   189          buttons.push({
   190              label: prepareUndeployOrUndeployText,
   191              onClick: prepareUndeployOrUndeploy,
   192              icon: <DeleteWhite />,
   193          });
   194      }
   195  
   196      const dotsMenu = <DotsMenu buttons={buttons} />;
   197      const appLocks = useFilteredApplicationLocks(application.name);
   198      const dialog = (
   199          <EnvSelectionDialog
   200              environments={envs.map((e) => e.name)}
   201              open={showEnvSelectionDialog}
   202              onSubmit={confirmEnvAppDelete}
   203              onCancel={handleClose}
   204              envSelectionDialog={true}
   205          />
   206      );
   207  
   208      return (
   209          <div className="service-lane">
   210              {dialog}
   211              <div className="service-lane__header">
   212                  <div className="service__name">
   213                      {(application.team ? application.team + ' | ' : '<No Team> | ') + application.name}
   214                      {appLocks.length >= 1 && (
   215                          <div className={'test-app-lock-summary'}>
   216                              <AppLockSummary app={application.name} numLocks={appLocks.length} />
   217                          </div>
   218                      )}
   219                  </div>
   220                  <div className="service__actions__">{dotsMenu}</div>
   221              </div>
   222              <div className="service__warnings">
   223                  <WarningBoxes application={application} />
   224              </div>
   225              <div className="service__releases">{releases_lane}</div>
   226          </div>
   227      );
   228  };