github.com/freiheit-com/kuberpult@v1.24.2-0.20240328135542-315d5630abe6/services/frontend-service/src/ui/utils/store.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 { createStore } from 'react-use-sub';
    17  import {
    18      Application,
    19      BatchAction,
    20      BatchRequest,
    21      Environment,
    22      EnvironmentGroup,
    23      GetFrontendConfigResponse,
    24      GetOverviewResponse,
    25      Priority,
    26      Release,
    27      StreamStatusResponse,
    28      Warning,
    29      GetGitTagsResponse,
    30      RolloutStatus,
    31      GetCommitInfoResponse,
    32      GetEnvironmentConfigResponse,
    33  } from '../../api/api';
    34  import * as React from 'react';
    35  import { useCallback, useMemo } from 'react';
    36  import { useLocation, useNavigate, useSearchParams } from 'react-router-dom';
    37  import { useIsAuthenticated } from '@azure/msal-react';
    38  import { useApi } from './GrpcApi';
    39  
    40  // see maxBatchActions in batch.go
    41  export const maxBatchActions = 100;
    42  
    43  export interface DisplayLock {
    44      date?: Date;
    45      environment: string;
    46      application?: string;
    47      message: string;
    48      lockId: string;
    49      authorName?: string;
    50      authorEmail?: string;
    51  }
    52  
    53  export const displayLockUniqueId = (displayLock: DisplayLock): string =>
    54      'dl-' + displayLock.lockId + '-' + displayLock.environment + '-' + displayLock.application;
    55  
    56  type EnhancedOverview = GetOverviewResponse & { loaded: boolean };
    57  
    58  const emptyOverview: EnhancedOverview = {
    59      applications: {},
    60      environmentGroups: [],
    61      gitRevision: '',
    62      loaded: false,
    63      branch: '',
    64      manifestRepoUrl: '',
    65  };
    66  const [useOverview, UpdateOverview_] = createStore(emptyOverview);
    67  export const UpdateOverview = UpdateOverview_; // we do not want to export "useOverview". The store.tsx should act like a facade to the data.
    68  
    69  export const useOverviewLoaded = (): boolean => useOverview(({ loaded }) => loaded);
    70  type TagsResponse = {
    71      response: GetGitTagsResponse;
    72      tagsReady: boolean;
    73  };
    74  
    75  export enum CommitInfoState {
    76      LOADING,
    77      READY,
    78      ERROR,
    79      NOTFOUND,
    80  }
    81  export type CommitInfoResponse = {
    82      response: GetCommitInfoResponse | undefined;
    83      commitInfoReady: CommitInfoState;
    84  };
    85  
    86  const emptyBatch: BatchRequest = { actions: [] };
    87  export const [useAction, UpdateAction] = createStore(emptyBatch);
    88  const tagsResponse: GetGitTagsResponse = { tagData: [] };
    89  export const refreshTags = (): void => {
    90      const api = useApi;
    91      api.gitService()
    92          .GetGitTags({})
    93          .then((result: GetGitTagsResponse) => {
    94              updateTag.set({ response: result, tagsReady: true });
    95          })
    96          .catch((e) => {
    97              showSnackbarError(e.message);
    98          });
    99  };
   100  export const [useTag, updateTag] = createStore<TagsResponse>({ response: tagsResponse, tagsReady: false });
   101  
   102  export const getCommitInfo = (commitHash: string): void => {
   103      const api = useApi;
   104      api.gitService()
   105          .GetCommitInfo({ commitHash: commitHash })
   106          .then((result: GetCommitInfoResponse) => {
   107              updateCommitInfo.set({ response: result, commitInfoReady: CommitInfoState.READY });
   108          })
   109          .catch((e) => {
   110              const GrpcErrorNotFound = 5;
   111              if (e.code === GrpcErrorNotFound) {
   112                  updateCommitInfo.set({ response: undefined, commitInfoReady: CommitInfoState.NOTFOUND });
   113              } else {
   114                  showSnackbarError(e.message);
   115                  updateCommitInfo.set({ response: undefined, commitInfoReady: CommitInfoState.ERROR });
   116              }
   117          });
   118  };
   119  export const [useCommitInfo, updateCommitInfo] = createStore<CommitInfoResponse>({
   120      response: undefined,
   121      commitInfoReady: CommitInfoState.LOADING,
   122  });
   123  
   124  export const [_, PanicOverview] = createStore({ error: '' });
   125  
   126  const randBase36 = (): string => Math.random().toString(36).substring(7);
   127  export const randomLockId = (): string => 'ui-v2-' + randBase36();
   128  
   129  export const useActions = (): BatchAction[] => useAction(({ actions }) => actions);
   130  export const useTags = (): TagsResponse => useTag((res) => res);
   131  
   132  export const [useSidebar, UpdateSidebar] = createStore({ shown: false });
   133  
   134  export enum SnackbarStatus {
   135      SUCCESS,
   136      WARN,
   137      ERROR,
   138  }
   139  
   140  export const [useSnackbar, UpdateSnackbar] = createStore({ show: false, status: SnackbarStatus.SUCCESS, content: '' });
   141  export const showSnackbarSuccess = (content: string): void =>
   142      UpdateSnackbar.set({ show: true, status: SnackbarStatus.SUCCESS, content: content });
   143  export const showSnackbarError = (content: string): void =>
   144      UpdateSnackbar.set({ show: true, status: SnackbarStatus.ERROR, content: content });
   145  export const showSnackbarWarn = (content: string): void =>
   146      UpdateSnackbar.set({ show: true, status: SnackbarStatus.WARN, content: content });
   147  export const useSidebarShown = (): boolean => useSidebar(({ shown }) => shown);
   148  
   149  export const useNumberOfActions = (): number => useAction(({ actions }) => actions.length);
   150  
   151  export const updateActions = (actions: BatchAction[]): void => {
   152      deleteAllActions();
   153      actions.forEach((action) => addAction(action));
   154  };
   155  
   156  export const appendAction = (actions: BatchAction[]): void => {
   157      actions.forEach((action) => addAction(action));
   158  };
   159  
   160  export const addAction = (action: BatchAction): void => {
   161      const actions = UpdateAction.get().actions;
   162      if (actions.length + 1 > maxBatchActions) {
   163          showSnackbarError('Maximum number of actions is ' + String(maxBatchActions));
   164          return;
   165      }
   166      // checking for duplicates
   167      switch (action.action?.$case) {
   168          case 'createEnvironmentLock':
   169              if (
   170                  actions.some(
   171                      (act) =>
   172                          act.action?.$case === 'createEnvironmentLock' &&
   173                          action.action?.$case === 'createEnvironmentLock' &&
   174                          act.action.createEnvironmentLock.environment === action.action.createEnvironmentLock.environment
   175                      // lockId and message are ignored
   176                  )
   177              )
   178                  return;
   179              break;
   180          case 'deleteEnvironmentLock':
   181              if (
   182                  actions.some(
   183                      (act) =>
   184                          act.action?.$case === 'deleteEnvironmentLock' &&
   185                          action.action?.$case === 'deleteEnvironmentLock' &&
   186                          act.action.deleteEnvironmentLock.environment ===
   187                              action.action.deleteEnvironmentLock.environment &&
   188                          act.action.deleteEnvironmentLock.lockId === action.action.deleteEnvironmentLock.lockId
   189                  )
   190              )
   191                  return;
   192              break;
   193          case 'createEnvironmentApplicationLock':
   194              if (
   195                  actions.some(
   196                      (act) =>
   197                          act.action?.$case === 'createEnvironmentApplicationLock' &&
   198                          action.action?.$case === 'createEnvironmentApplicationLock' &&
   199                          act.action.createEnvironmentApplicationLock.application ===
   200                              action.action.createEnvironmentApplicationLock.application &&
   201                          act.action.createEnvironmentApplicationLock.environment ===
   202                              action.action.createEnvironmentApplicationLock.environment
   203                      // lockId and message are ignored
   204                  )
   205              )
   206                  return;
   207              break;
   208          case 'deleteEnvironmentApplicationLock':
   209              if (
   210                  actions.some(
   211                      (act) =>
   212                          act.action?.$case === 'deleteEnvironmentApplicationLock' &&
   213                          action.action?.$case === 'deleteEnvironmentApplicationLock' &&
   214                          act.action.deleteEnvironmentApplicationLock.environment ===
   215                              action.action.deleteEnvironmentApplicationLock.environment &&
   216                          act.action.deleteEnvironmentApplicationLock.lockId ===
   217                              action.action.deleteEnvironmentApplicationLock.lockId &&
   218                          act.action.deleteEnvironmentApplicationLock.application ===
   219                              action.action.deleteEnvironmentApplicationLock.application
   220                  )
   221              )
   222                  return;
   223              break;
   224          case 'deploy':
   225              if (
   226                  actions.some(
   227                      (act) =>
   228                          (act.action?.$case === 'deploy' &&
   229                              action.action?.$case === 'deploy' &&
   230                              act.action.deploy.application === action.action.deploy.application &&
   231                              act.action.deploy.environment === action.action.deploy.environment) ||
   232                          act.action?.$case === 'releaseTrain'
   233                      // version, lockBehavior and ignoreAllLocks are ignored
   234                  )
   235              )
   236                  return;
   237              break;
   238          case 'undeploy':
   239              if (
   240                  actions.some(
   241                      (act) =>
   242                          act.action?.$case === 'undeploy' &&
   243                          action.action?.$case === 'undeploy' &&
   244                          act.action.undeploy.application === action.action.undeploy.application
   245                  )
   246              )
   247                  return;
   248              break;
   249          case 'prepareUndeploy':
   250              if (
   251                  actions.some(
   252                      (act) =>
   253                          act.action?.$case === 'prepareUndeploy' &&
   254                          action.action?.$case === 'prepareUndeploy' &&
   255                          act.action.prepareUndeploy.application === action.action.prepareUndeploy.application
   256                  )
   257              )
   258                  return;
   259              break;
   260          case 'releaseTrain':
   261              // only allow one release train at a time to avoid conflicts or if there are existing deploy actions
   262              if (actions.some((act) => act.action?.$case === 'releaseTrain' || act.action?.$case === 'deploy')) {
   263                  showSnackbarError(
   264                      'Can only have one release train action at a time and can not have deploy actions in parrallel'
   265                  );
   266                  return;
   267              }
   268  
   269              break;
   270      }
   271      UpdateAction.set({ actions: [...UpdateAction.get().actions, action] });
   272      UpdateSidebar.set({ shown: true });
   273  };
   274  
   275  export const useOpenReleaseDialog = (app: string, version: number): (() => void) => {
   276      const [params, setParams] = useSearchParams();
   277      return useCallback(() => {
   278          params.set('dialog-app', app);
   279          params.set('dialog-version', version.toString());
   280          setParams(params);
   281      }, [app, params, setParams, version]);
   282  };
   283  
   284  export const useCloseReleaseDialog = (): (() => void) => {
   285      const [params, setParams] = useSearchParams();
   286      return useCallback(() => {
   287          params.delete('dialog-app');
   288          params.delete('dialog-version');
   289          setParams(params);
   290      }, [params, setParams]);
   291  };
   292  
   293  export const useReleaseDialogParams = (): { app: string | null; version: number | null } => {
   294      const [params] = useSearchParams();
   295      const app = params.get('dialog-app') ?? '';
   296      const version = +(params.get('dialog-version') ?? '');
   297      const valid = useOverview(({ applications }) =>
   298          applications[app] ? !!applications[app].releases.find((r) => r.version === version) : false
   299      );
   300      return valid ? { app, version } : { app: null, version: null };
   301  };
   302  
   303  export const deleteAllActions = (): void => {
   304      UpdateAction.set({ actions: [] });
   305  };
   306  
   307  export const deleteAction = (action: BatchAction): void => {
   308      UpdateAction.set(({ actions }) => ({
   309          // create comparison function
   310          actions: actions.filter((act) => JSON.stringify(act).localeCompare(JSON.stringify(action))),
   311      }));
   312  };
   313  // returns all application names
   314  // doesn't return empty team names (i.e.: '')
   315  // doesn't return repeated team names
   316  export const useTeamNames = (): string[] =>
   317      useOverview(({ applications }) => [
   318          ...new Set(
   319              Object.values(applications)
   320                  .map((app: Application) => app.team.trim() || '<No Team>')
   321                  .sort((a, b) => a.localeCompare(b))
   322          ),
   323      ]);
   324  
   325  export const useTeamFromApplication = (app: string): string | undefined =>
   326      useOverview(({ applications }) => applications[app]?.team?.trim());
   327  
   328  // returns warnings from all apps
   329  export const useAllWarnings = (): Warning[] =>
   330      useOverview(({ applications }) => Object.values(applications).flatMap((app) => app.warnings));
   331  
   332  // return warnings from all apps matching the given filtering criteria
   333  export const useShownWarnings = (teams: string[], nameIncludes: string): Warning[] => {
   334      const shownApps = useApplicationsFilteredAndSorted(teams, true, nameIncludes);
   335      return shownApps.flatMap((app) => app.warnings);
   336  };
   337  
   338  export const useEnvironmentGroups = (): EnvironmentGroup[] => useOverview(({ environmentGroups }) => environmentGroups);
   339  
   340  /**
   341   * returns all environments
   342   */
   343  export const useEnvironments = (): Environment[] =>
   344      useOverview(({ environmentGroups }) => environmentGroups.flatMap((envGroup) => envGroup.environments));
   345  
   346  /**
   347   * returns all environment names
   348   */
   349  export const useEnvironmentNames = (): string[] => useEnvironments().map((env) => env.name);
   350  
   351  /**
   352   * returns the classname according to the priority of an environment, used to color environments
   353   */
   354  export const getPriorityClassName = (envOrGroup: Environment | EnvironmentGroup): string =>
   355      'environment-priority-' + String(Priority[envOrGroup?.priority ?? Priority.UNRECOGNIZED]).toLowerCase();
   356  
   357  // filter for apps included in the selected teams
   358  const applicationsMatchingTeam = (applications: Application[], teams: string[]): Application[] =>
   359      applications.filter((app) => teams.length === 0 || teams.includes(app.team.trim() || '<No Team>'));
   360  
   361  // filter for all application names that have warnings
   362  const applicationsWithWarnings = (applications: Application[]): Application[] =>
   363      applications.filter((app) => app.warnings.length > 0);
   364  
   365  // filters given apps with the search terms or all for the empty string
   366  const applicationsMatchingName = (applications: Application[], appNameParam: string): Application[] =>
   367      applications.filter((app) => appNameParam === '' || app.name.includes(appNameParam));
   368  
   369  // sorts given apps by team
   370  const applicationsSortedByTeam = (applications: Application[]): Application[] =>
   371      applications.sort((a, b) => (a.team === b.team ? a.name?.localeCompare(b.name) : a.team?.localeCompare(b.team)));
   372  
   373  // returns applications to show on the home page
   374  export const useApplicationsFilteredAndSorted = (
   375      teams: string[],
   376      withWarningsOnly: boolean,
   377      nameIncludes: string
   378  ): Application[] => {
   379      const all = useOverview(({ applications }) => Object.values(applications));
   380      const allMatchingTeam = applicationsMatchingTeam(all, teams);
   381      const allMatchingTeamAndWarnings = withWarningsOnly ? applicationsWithWarnings(allMatchingTeam) : allMatchingTeam;
   382      const allMatchingTeamAndWarningsAndName = applicationsMatchingName(allMatchingTeamAndWarnings, nameIncludes);
   383      return applicationsSortedByTeam(allMatchingTeamAndWarningsAndName);
   384  };
   385  
   386  // return all applications locks
   387  export const useFilteredApplicationLocks = (appNameParam: string | null): DisplayLock[] => {
   388      const finalLocks: DisplayLock[] = [];
   389      Object.values(useEnvironments())
   390          .map((environment) => ({ envName: environment.name, apps: environment.applications }))
   391          .forEach((app) => {
   392              Object.values(app.apps)
   393                  .map((myApp) => ({ environment: app.envName, appName: myApp.name, locks: myApp.locks }))
   394                  .forEach((lock) => {
   395                      Object.values(lock.locks).forEach((cena) =>
   396                          finalLocks.push({
   397                              date: cena.createdAt,
   398                              application: lock.appName,
   399                              environment: lock.environment,
   400                              lockId: cena.lockId,
   401                              message: cena.message,
   402                              authorName: cena.createdBy?.name,
   403                              authorEmail: cena.createdBy?.email,
   404                          })
   405                      );
   406                  });
   407          });
   408      const filteredLocks = finalLocks.filter((val) => appNameParam === val.application);
   409      return sortLocks(filteredLocks, 'newestToOldest');
   410  };
   411  
   412  export const useLocksConflictingWithActions = (): AllLocks => {
   413      const allActions = useActions();
   414      const locks = useAllLocks();
   415      return {
   416          environmentLocks: locks.environmentLocks.filter((envLock: DisplayLock) => {
   417              const actions = allActions.filter((action) => {
   418                  if (action.action?.$case === 'deploy') {
   419                      const env = action.action.deploy.environment;
   420                      if (envLock.environment === env) {
   421                          // found an env lock that matches
   422                          return true;
   423                      }
   424                  }
   425                  return false;
   426              });
   427              return actions.length > 0;
   428          }),
   429          appLocks: locks.appLocks.filter((envLock: DisplayLock) => {
   430              const actions = allActions.filter((action) => {
   431                  if (action.action?.$case === 'deploy') {
   432                      const app = action.action.deploy.application;
   433                      const env = action.action.deploy.environment;
   434                      if (envLock.environment === env && envLock.application === app) {
   435                          // found an app lock that matches
   436                          return true;
   437                      }
   438                  }
   439                  return false;
   440              });
   441              return actions.length > 0;
   442          }),
   443      };
   444  };
   445  
   446  // return env lock IDs from given env
   447  export const useFilteredEnvironmentLockIDs = (envName: string): string[] =>
   448      useEnvironments()
   449          .filter((env) => envName === '' || env.name === envName)
   450          .map((env) => Object.values(env.locks))
   451          .flat()
   452          .map((lock) => lock.lockId);
   453  
   454  export const useEnvironmentLock = (lockId: string): DisplayLock => {
   455      const envs = useEnvironments();
   456      for (let i = 0; i < envs.length; i++) {
   457          const env = envs[i];
   458          for (const locksKey in env.locks) {
   459              const lock = env.locks[locksKey];
   460              if (lock.lockId === lockId) {
   461                  return {
   462                      date: lock.createdAt,
   463                      message: lock.message,
   464                      lockId: lock.lockId,
   465                      authorName: lock.createdBy?.name,
   466                      authorEmail: lock.createdBy?.email,
   467                      environment: env.name,
   468                  };
   469              }
   470          }
   471      }
   472      throw new Error('env lock with id not found: ' + lockId);
   473  };
   474  
   475  export const searchCustomFilter = (queryContent: string | null, val: string | undefined): string => {
   476      if (!!val && !!queryContent) {
   477          if (val.includes(queryContent)) {
   478              return val;
   479          }
   480          return '';
   481      } else {
   482          return val || '';
   483      }
   484  };
   485  
   486  export type AllLocks = {
   487      environmentLocks: DisplayLock[];
   488      appLocks: DisplayLock[];
   489  };
   490  
   491  export const useAllLocks = (): AllLocks => {
   492      const envs = useEnvironments();
   493      const environmentLocks: DisplayLock[] = [];
   494      const appLocks: DisplayLock[] = [];
   495      envs.forEach((env: Environment) => {
   496          for (const locksKey in env.locks) {
   497              const lock = env.locks[locksKey];
   498              const displayLock: DisplayLock = {
   499                  lockId: lock.lockId,
   500                  date: lock.createdAt,
   501                  environment: env.name,
   502                  message: lock.message,
   503                  authorName: lock.createdBy?.name,
   504                  authorEmail: lock.createdBy?.email,
   505              };
   506              environmentLocks.push(displayLock);
   507          }
   508          for (const applicationsKey in env.applications) {
   509              const app = env.applications[applicationsKey];
   510              for (const locksKey in app.locks) {
   511                  const lock = app.locks[locksKey];
   512                  const displayLock: DisplayLock = {
   513                      lockId: lock.lockId,
   514                      application: app.name,
   515                      date: lock.createdAt,
   516                      environment: env.name,
   517                      message: lock.message,
   518                      authorName: lock.createdBy?.name,
   519                      authorEmail: lock.createdBy?.email,
   520                  };
   521                  appLocks.push(displayLock);
   522              }
   523          }
   524      });
   525      return {
   526          environmentLocks,
   527          appLocks,
   528      };
   529  };
   530  
   531  type DeleteActionData = {
   532      env: string;
   533      app: string | undefined;
   534      lockId: string;
   535  };
   536  
   537  const extractDeleteActionData = (batchAction: BatchAction): DeleteActionData | undefined => {
   538      if (batchAction.action?.$case === 'deleteEnvironmentApplicationLock') {
   539          return {
   540              env: batchAction.action.deleteEnvironmentApplicationLock.environment,
   541              app: batchAction.action.deleteEnvironmentApplicationLock.application,
   542              lockId: batchAction.action.deleteEnvironmentApplicationLock.lockId,
   543          };
   544      }
   545      if (batchAction.action?.$case === 'deleteEnvironmentLock') {
   546          return {
   547              env: batchAction.action.deleteEnvironmentLock.environment,
   548              app: undefined,
   549              lockId: batchAction.action.deleteEnvironmentLock.lockId,
   550          };
   551      }
   552      return undefined;
   553  };
   554  
   555  // returns all locks with the same ID
   556  // that are not already in the cart
   557  export const useLocksSimilarTo = (cartItemAction: BatchAction | undefined): AllLocks => {
   558      const allLocks = useAllLocks();
   559      const actions = useActions();
   560  
   561      if (!cartItemAction) {
   562          return { appLocks: [], environmentLocks: [] };
   563      }
   564      const data = extractDeleteActionData(cartItemAction);
   565      if (!data) {
   566          return {
   567              appLocks: [],
   568              environmentLocks: [],
   569          };
   570      }
   571      const isInCart = (lock: DisplayLock): boolean =>
   572          actions.find((cartAction: BatchAction): boolean => {
   573              const data = extractDeleteActionData(cartAction);
   574              if (!data) {
   575                  return false;
   576              }
   577              return lock.lockId === data.lockId && lock.application === data.app && lock.environment === data.env;
   578          }) !== undefined;
   579  
   580      const resultLocks: AllLocks = {
   581          environmentLocks: [],
   582          appLocks: [],
   583      };
   584      allLocks.environmentLocks.forEach((envLock: DisplayLock) => {
   585          if (isInCart(envLock)) {
   586              return;
   587          }
   588          // if the id is the same, but we are on a different environment, or it's an app lock:
   589          if (envLock.lockId === data.lockId && (envLock.environment !== data.env || data.app !== undefined)) {
   590              resultLocks.environmentLocks.push(envLock);
   591          }
   592      });
   593      allLocks.appLocks.forEach((appLock: DisplayLock) => {
   594          if (isInCart(appLock)) {
   595              return;
   596          }
   597          // if the id is the same, but we are on a different environment or different app:
   598          if (appLock.lockId === data.lockId && (appLock.environment !== data.env || appLock.application !== data.app)) {
   599              resultLocks.appLocks.push(appLock);
   600          }
   601      });
   602      return resultLocks;
   603  };
   604  
   605  export const sortLocks = (displayLocks: DisplayLock[], sorting: 'oldestToNewest' | 'newestToOldest'): DisplayLock[] => {
   606      const sortMethod = sorting === 'newestToOldest' ? -1 : 1;
   607      displayLocks.sort((a: DisplayLock, b: DisplayLock) => {
   608          const aValues: (Date | string)[] = [];
   609          const bValues: (Date | string)[] = [];
   610          Object.values(a).forEach((val) => aValues.push(val));
   611          Object.values(b).forEach((val) => bValues.push(val));
   612          for (let i = 0; i < aValues.length; i++) {
   613              if (aValues[i] < bValues[i]) {
   614                  if (aValues[i] instanceof Date) return -sortMethod;
   615                  return sortMethod;
   616              } else if (aValues[i] > bValues[i]) {
   617                  if (aValues[i] instanceof Date) return sortMethod;
   618                  return -sortMethod;
   619              }
   620              if (aValues[aValues.length - 1] === bValues[aValues.length - 1]) {
   621                  return 0;
   622              }
   623          }
   624          return 0;
   625      });
   626      return displayLocks;
   627  };
   628  
   629  // returns the release number {$version} of {$application}
   630  export const useRelease = (application: string, version: number): Release | undefined =>
   631      useOverview(({ applications }) => applications[application]?.releases?.find((r) => r.version === version));
   632  
   633  export const useReleaseOrThrow = (application: string, version: number): Release => {
   634      const release = useRelease(application, version);
   635      if (!release) {
   636          throw new Error('Release cannot be found for app ' + application + ' version ' + version);
   637      }
   638      return release;
   639  };
   640  
   641  export const useReleaseOptional = (application: string, env: Environment): Release | undefined => {
   642      const x = env.applications[application];
   643      return useOverview(({ applications }) => {
   644          const version = x ? x.version : 0;
   645          const res = applications[application].releases.find((r) => r.version === version);
   646          if (!x) {
   647              return undefined;
   648          }
   649          return res;
   650      });
   651  };
   652  
   653  // returns the release versions that are currently deployed to at least one environment
   654  export const useDeployedReleases = (application: string): number[] =>
   655      [
   656          ...new Set(
   657              Object.values(useEnvironments())
   658                  .filter((env) => env.applications[application])
   659                  .map((env) => env.applications[application].version)
   660          ),
   661      ]
   662          .sort((a, b) => b - a)
   663          .filter((version) => version !== 0); // 0 means "not deployed", so we filter those out
   664  
   665  export type EnvironmentGroupExtended = EnvironmentGroup & { numberOfEnvsInGroup: number };
   666  
   667  /**
   668   * returns the environments where a release is currently deployed
   669   */
   670  export const useCurrentlyDeployedAtGroup = (application: string, version: number): EnvironmentGroupExtended[] => {
   671      const environmentGroups: EnvironmentGroup[] = useEnvironmentGroups();
   672      return useMemo(() => {
   673          const envGroups: EnvironmentGroupExtended[] = [];
   674          environmentGroups.forEach((group: EnvironmentGroup) => {
   675              const envs = group.environments.filter(
   676                  (env) => env.applications[application] && env.applications[application].version === version
   677              );
   678              if (envs.length > 0) {
   679                  // we need to make a copy of the group here, because we want to remove some envs.
   680                  // but that should not have any effect on the group saved in the store.
   681                  const groupCopy: EnvironmentGroupExtended = {
   682                      environmentGroupName: group.environmentGroupName,
   683                      environments: envs,
   684                      distanceToUpstream: group.distanceToUpstream,
   685                      numberOfEnvsInGroup: group.environments.length,
   686                      priority: group.priority,
   687                  };
   688                  envGroups.push(groupCopy);
   689              }
   690          });
   691          return envGroups;
   692      }, [environmentGroups, application, version]);
   693  };
   694  
   695  /**
   696   * returns the environments where an application is currently deployed
   697   */
   698  export const useCurrentlyExistsAtGroup = (application: string): EnvironmentGroupExtended[] => {
   699      const environmentGroups: EnvironmentGroup[] = useEnvironmentGroups();
   700      return useMemo(() => {
   701          const envGroups: EnvironmentGroupExtended[] = [];
   702          environmentGroups.forEach((group: EnvironmentGroup) => {
   703              const envs = group.environments.filter((env) => env.applications[application]);
   704              if (envs.length > 0) {
   705                  // we need to make a copy of the group here, because we want to remove some envs.
   706                  // but that should not have any effect on the group saved in the store.
   707                  const groupCopy: EnvironmentGroupExtended = {
   708                      environmentGroupName: group.environmentGroupName,
   709                      environments: envs,
   710                      distanceToUpstream: group.distanceToUpstream,
   711                      numberOfEnvsInGroup: group.environments.length,
   712                      priority: group.priority,
   713                  };
   714                  envGroups.push(groupCopy);
   715              }
   716          });
   717          return envGroups;
   718      }, [environmentGroups, application]);
   719  };
   720  
   721  // Get all releases for an app
   722  export const useReleasesForApp = (app: string): Release[] =>
   723      useOverview(({ applications }) => applications[app]?.releases?.sort((a, b) => b.version - a.version));
   724  
   725  // Get all release versions for an app
   726  export const useVersionsForApp = (app: string): number[] => useReleasesForApp(app).map((rel) => rel.version);
   727  
   728  // Navigate while keeping search params, returns new navigation url, and a callback function to navigate
   729  export const useNavigateWithSearchParams = (to: string): { navURL: string; navCallback: () => void } => {
   730      const location = useLocation();
   731      const navigate = useNavigate();
   732      const queryParams = location?.search ?? '';
   733      const navURL = `${to}${queryParams}`;
   734      return {
   735          navURL: navURL,
   736          navCallback: React.useCallback(() => {
   737              navigate(navURL);
   738          }, [navURL, navigate]),
   739      };
   740  };
   741  
   742  type FrontendConfig = {
   743      configs: GetFrontendConfigResponse;
   744      configReady: boolean;
   745  };
   746  
   747  export const [useFrontendConfig, UpdateFrontendConfig] = createStore<FrontendConfig>({
   748      configs: {
   749          sourceRepoUrl: '',
   750          manifestRepoUrl: '',
   751          branch: '',
   752          kuberpultVersion: '0',
   753      },
   754      configReady: false,
   755  });
   756  
   757  export type GlobalLoadingState = {
   758      configReady: boolean;
   759      isAuthenticated: boolean;
   760      azureAuthEnabled: boolean;
   761      overviewLoaded: boolean;
   762  };
   763  
   764  // returns one loading state for all the calls done on startup, in order to render a spinner with details
   765  export const useGlobalLoadingState = (): [boolean, GlobalLoadingState] => {
   766      const { configs, configReady } = useFrontendConfig((c) => c);
   767      const isAuthenticated = useIsAuthenticated();
   768      const azureAuthEnabled = configs.authConfig?.azureAuth?.enabled || false;
   769      const overviewLoaded = useOverviewLoaded();
   770      const everythingLoaded = overviewLoaded && configReady && (isAuthenticated || !azureAuthEnabled);
   771      return [
   772          everythingLoaded,
   773          {
   774              configReady,
   775              isAuthenticated,
   776              azureAuthEnabled,
   777              overviewLoaded,
   778          },
   779      ];
   780  };
   781  
   782  export const useKuberpultVersion = (): string => useFrontendConfig((configs) => configs.configs.kuberpultVersion);
   783  export const useArgoCdBaseUrl = (): string | undefined =>
   784      useFrontendConfig((configs) => configs.configs.argoCd?.baseUrl);
   785  export const useSourceRepoUrl = (): string | undefined => useFrontendConfig((configs) => configs.configs.sourceRepoUrl);
   786  export const useManifestRepoUrl = (): string | undefined =>
   787      useFrontendConfig((configs) => configs.configs.manifestRepoUrl);
   788  export const useBranch = (): string | undefined => useFrontendConfig((configs) => configs.configs.branch);
   789  
   790  export type RolloutStatusApplication = {
   791      [environment: string]: StreamStatusResponse;
   792  };
   793  
   794  type RolloutStatusStore = {
   795      enabled: boolean;
   796      applications: {
   797          [application: string]: RolloutStatusApplication;
   798      };
   799  };
   800  
   801  const [useEntireRolloutStatus, rolloutStatus] = createStore<RolloutStatusStore>({ enabled: false, applications: {} });
   802  
   803  class RolloutStatusGetter {
   804      private readonly store: RolloutStatusStore;
   805  
   806      constructor(store: RolloutStatusStore) {
   807          this.store = store;
   808      }
   809  
   810      getAppStatus(
   811          application: string,
   812          applicationVersion: number | undefined,
   813          environment: string
   814      ): RolloutStatus | undefined {
   815          if (!this.store.enabled) {
   816              return undefined;
   817          }
   818          const statusPerEnv = this.store.applications[application];
   819          if (statusPerEnv === undefined) {
   820              return undefined;
   821          }
   822          const status = statusPerEnv[environment];
   823          if (status === undefined) {
   824              return undefined;
   825          }
   826          if (status.rolloutStatus === RolloutStatus.ROLLOUT_STATUS_SUCCESFUL && status.version !== applicationVersion) {
   827              // The rollout service might be sligthly behind the UI.
   828              return RolloutStatus.ROLLOUT_STATUS_PENDING;
   829          }
   830          return status.rolloutStatus;
   831      }
   832  }
   833  
   834  export const useRolloutStatus = <T,>(f: (getter: RolloutStatusGetter) => T): T =>
   835      useEntireRolloutStatus((data) => f(new RolloutStatusGetter(data)));
   836  
   837  export const UpdateRolloutStatus = (ev: StreamStatusResponse): void => {
   838      rolloutStatus.set((data: RolloutStatusStore) => ({
   839          enabled: true,
   840          applications: {
   841              ...data.applications,
   842              [ev.application]: {
   843                  ...(data.applications[ev.application] ?? {}),
   844                  [ev.environment]: ev,
   845              },
   846          },
   847      }));
   848  };
   849  
   850  export const EnableRolloutStatus = (): void => {
   851      rolloutStatus.set({ enabled: true });
   852  };
   853  
   854  export const FlushRolloutStatus = (): void => {
   855      rolloutStatus.set({ enabled: false, applications: {} });
   856  };
   857  
   858  export const GetEnvironmentConfigPretty = (environmentName: string): Promise<string> =>
   859      useApi
   860          .environmentService()
   861          .GetEnvironmentConfig({ environment: environmentName })
   862          .then((res: GetEnvironmentConfigResponse) => {
   863              if (!res.config) {
   864                  return Promise.reject(new Error('empty response.'));
   865              }
   866              return JSON.stringify(res.config, null, ' ');
   867          });
   868  
   869  export const useArgoCDNamespace = (): string | undefined => useFrontendConfig((c) => c.configs.argoCd?.namespace);