github.com/freiheit-com/kuberpult@v1.24.2-0.20240328135542-315d5630abe6/services/frontend-service/src/ui/components/ProductVersion/ProductVersion.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 './ProductVersion.scss';
    17  import * as React from 'react';
    18  import {
    19      refreshTags,
    20      useTags,
    21      useEnvironmentGroups,
    22      useEnvironments,
    23      addAction,
    24      showSnackbarError,
    25  } from '../../utils/store';
    26  import { DisplayManifestLink, DisplaySourceLink } from '../../utils/Links';
    27  import { Spinner } from '../Spinner/Spinner';
    28  import { EnvironmentGroup, GetProductSummaryResponse, ProductSummary } from '../../../api/api';
    29  import { useSearchParams } from 'react-router-dom';
    30  import { Button } from '../button';
    31  import { EnvSelectionDialog } from '../ServiceLane/EnvSelectionDialog';
    32  import { useState } from 'react';
    33  import { useApi } from '../../utils/GrpcApi';
    34  
    35  export type TableProps = {
    36      productSummary: ProductSummary[];
    37      teams: string[];
    38  };
    39  
    40  export const TableFiltered: React.FC<TableProps> = (props) => {
    41      const versionToDisplay = (app: ProductSummary): string => {
    42          if (app.displayVersion !== '') {
    43              return app.displayVersion;
    44          }
    45          if (app.commitId !== '') {
    46              return app.commitId;
    47          }
    48          return app.version;
    49      };
    50      const displayTeams = props.teams;
    51      if (displayTeams.includes('<No Team>')) {
    52          displayTeams.filter((team, index) => team !== '<No Team>');
    53          displayTeams.push('');
    54      }
    55      return (
    56          <table className="table">
    57              <tbody>
    58                  <tr className="table_title">
    59                      <th>App Name</th>
    60                      <th>Version</th>
    61                      <th>Environment</th>
    62                      <th>Team</th>
    63                      <th>ManifestRepoLink</th>
    64                      <th>SourceRepoLink</th>
    65                  </tr>
    66                  {props.productSummary
    67                      .filter((row, index) => props.teams.length <= 0 || displayTeams.includes(row.team))
    68                      .map((sum) => (
    69                          <tr key={sum.app + sum.environment} className="table_data">
    70                              <td>{sum.app}</td>
    71                              <td>{versionToDisplay(sum)}</td>
    72                              <td>{sum.environment}</td>
    73                              <td>{sum.team}</td>
    74                              <td>
    75                                  <DisplayManifestLink
    76                                      app={sum.app}
    77                                      version={Number(sum.version)}
    78                                      displayString="Manifest Link"
    79                                  />
    80                              </td>
    81                              <td>
    82                                  <DisplaySourceLink commitId={sum.commitId} displayString={'Source Link'} />
    83                              </td>
    84                          </tr>
    85                      ))}
    86              </tbody>
    87          </table>
    88      );
    89  };
    90  
    91  // splits up a string like "dev:dev-de" into ["dev", "dev-de"]
    92  const splitCombinedGroupName = (envName: string): string[] => {
    93      const splitter = envName.split('/');
    94      if (splitter.length === 1) {
    95          return ['', splitter[0]];
    96      }
    97      return [splitter[1], ''];
    98  };
    99  
   100  const useEnvironmentGroupCombinations = (envGroupResponse: EnvironmentGroup[]): string[] => {
   101      const envList: string[] = [];
   102      for (let i = 0; i < envGroupResponse.length; i++) {
   103          envList.push(envGroupResponse[i].environmentGroupName);
   104          for (let j = 0; j < envGroupResponse[i].environments.length; j++) {
   105              envList.push(envGroupResponse[i].environmentGroupName + '/' + envGroupResponse[i].environments[j].name);
   106          }
   107      }
   108      return envList;
   109  };
   110  
   111  export const ProductVersion: React.FC = () => {
   112      React.useEffect(() => {
   113          refreshTags();
   114      }, []);
   115      const envGroupResponse = useEnvironmentGroups();
   116      const envList = useEnvironmentGroupCombinations(envGroupResponse);
   117      const [searchParams, setSearchParams] = useSearchParams();
   118      const [environment, setEnvironment] = React.useState(searchParams.get('env') || envList[0]);
   119      const [showSummary, setShowSummary] = useState(false);
   120      const [summaryLoading, setSummaryLoading] = useState(false);
   121      const [productSummaries, setProductSummaries] = useState(Array<ProductSummary>());
   122      const teams = (searchParams.get('teams') || '').split(',').filter((val) => val !== '');
   123      const [selectedTag, setSelectedTag] = React.useState('');
   124      const envsList = useEnvironments();
   125      const tagsResponse = useTags();
   126  
   127      const onChangeTag = React.useCallback(
   128          (e: React.ChangeEvent<HTMLSelectElement>) => {
   129              setSelectedTag(e.target.value);
   130              searchParams.set('tag', e.target.value);
   131              setSearchParams(searchParams);
   132          },
   133          [searchParams, setSearchParams]
   134      );
   135      React.useEffect(() => {
   136          let tag = searchParams.get('tag');
   137          if (tag === null) {
   138              if (tagsResponse.response.tagData.length === 0) return;
   139              tag = tagsResponse.response.tagData[0].commitId;
   140              if (tag === null) return;
   141              setSelectedTag(tag);
   142              searchParams.set('tag', tag);
   143              setSearchParams(searchParams);
   144          }
   145          const env = splitCombinedGroupName(environment);
   146          setShowSummary(true);
   147          setSummaryLoading(true);
   148          useApi
   149              .gitService()
   150              .GetProductSummary({ commitHash: tag, environment: env[0], environmentGroup: env[1] })
   151              .then((result: GetProductSummaryResponse) => {
   152                  setProductSummaries(result.productSummary);
   153              })
   154              .catch((e) => {
   155                  showSnackbarError(e.message);
   156              });
   157          setSummaryLoading(false);
   158      }, [tagsResponse, envGroupResponse, environment, searchParams, setSearchParams]);
   159  
   160      const changeEnv = React.useCallback(
   161          (e: React.ChangeEvent<HTMLSelectElement>) => {
   162              searchParams.set('env', e.target.value);
   163              setEnvironment(e.target.value);
   164              setSearchParams(searchParams);
   165          },
   166          [setSearchParams, searchParams]
   167      );
   168      const [showReleaseTrainEnvs, setShowReleaseTrainEnvs] = React.useState(false);
   169      const handleClose = React.useCallback(() => {
   170          setShowReleaseTrainEnvs(false);
   171      }, []);
   172      const openDialog = React.useCallback(() => {
   173          setShowReleaseTrainEnvs(true);
   174      }, []);
   175      const confirmReleaseTrainFunction = React.useCallback(
   176          (selectedEnvs: string[]) => {
   177              if (teams.length < 1) {
   178                  selectedEnvs.forEach((env) => {
   179                      addAction({
   180                          action: {
   181                              $case: 'releaseTrain',
   182                              releaseTrain: { target: env, commitHash: selectedTag, team: '' },
   183                          },
   184                      });
   185                  });
   186                  return;
   187              }
   188              if (teams.length > 1) {
   189                  showSnackbarError('Can only run one release train action at a time, should only select one team');
   190                  return;
   191              }
   192              selectedEnvs.forEach((env) => {
   193                  addAction({
   194                      action: {
   195                          $case: 'releaseTrain',
   196                          releaseTrain: { target: env, commitHash: selectedTag, team: teams[0] },
   197                      },
   198                  });
   199              });
   200              return;
   201          },
   202          [selectedTag, teams]
   203      );
   204  
   205      if (!tagsResponse.tagsReady) {
   206          return <Spinner message="Loading Git Tags" />;
   207      }
   208      if (summaryLoading) {
   209          return <Spinner message="Loading Production Version" />;
   210      }
   211  
   212      const dialog = (
   213          <EnvSelectionDialog
   214              environments={envsList
   215                  .filter((env, index) => splitCombinedGroupName(environment)[0] === env.config?.upstream?.environment)
   216                  .map((env) => env.name)}
   217              open={showReleaseTrainEnvs}
   218              onCancel={handleClose}
   219              onSubmit={confirmReleaseTrainFunction}
   220              envSelectionDialog={false}
   221          />
   222      );
   223  
   224      return (
   225          <div className="product_version">
   226              <h1 className="environment_name">{'Product Version Page'}</h1>
   227              {dialog}
   228              {tagsResponse.response.tagData.length > 0 ? (
   229                  <div className="space_apart_row">
   230                      <div className="dropdown_div">
   231                          <select
   232                              onChange={onChangeTag}
   233                              className="drop_down"
   234                              data-testid="drop_down"
   235                              value={selectedTag}>
   236                              <option value="default" disabled>
   237                                  Select a Tag
   238                              </option>
   239                              {tagsResponse.response.tagData.map((tag) => (
   240                                  <option value={tag.commitId} key={tag.tag}>
   241                                      {tag.tag.slice(10)}
   242                                  </option>
   243                              ))}
   244                          </select>
   245                          <select className="env_drop_down" onChange={changeEnv} value={environment}>
   246                              <option value="default" disabled>
   247                                  Select an Environment or Environment Group
   248                              </option>
   249                              {envList.map((env) => (
   250                                  <option value={env} key={env}>
   251                                      {env}
   252                                  </option>
   253                              ))}
   254                          </select>
   255                      </div>
   256                      <Button label={'Run Release Train'} className="release_train_button" onClick={openDialog} />
   257                  </div>
   258              ) : (
   259                  <div />
   260              )}
   261              <div>
   262                  {showSummary ? (
   263                      <div className="table_padding">
   264                          <TableFiltered productSummary={productSummaries} teams={teams} />
   265                      </div>
   266                  ) : (
   267                      <div className="page_description">
   268                          {
   269                              'This page shows the version of the product for the selected environment based on tags to the repository. If there are no tags, then no data can be shown.'
   270                          }
   271                      </div>
   272                  )}
   273              </div>
   274          </div>
   275      );
   276  };