From 51d74ae9a612341322337b9c274f1f88013a7021 Mon Sep 17 00:00:00 2001 From: Diogo Nogueira Date: Mon, 21 Oct 2024 10:18:22 +0100 Subject: [PATCH 1/3] fix(db): fixed select query that ignored eslversions with deleted flag (#2053) In the DeleteEnvFromApp a query was made which selected the most recent eslversions for each release of the app. This query ignored rows that has the deleted flag set to true. When the transformer later updated each release if the lated eslversion was deleted then an error would occur. This query was fixed and, in the process, altered to be more efficient. Ref: SRX-9MM6SC --- pkg/db/db.go | 48 +++++++++++++++---- pkg/db/db_test.go | 31 +++++++++++- .../cd-service/pkg/repository/transformer.go | 4 +- .../pkg/repository/transformer_db_test.go | 7 +-- tests/integration-tests/release_test.go | 2 +- 5 files changed, 75 insertions(+), 17 deletions(-) diff --git a/pkg/db/db.go b/pkg/db/db.go index 734723c04..e2246b9a2 100644 --- a/pkg/db/db.go +++ b/pkg/db/db.go @@ -789,27 +789,55 @@ func (h *DBHandler) processReleaseManifestRows(ctx context.Context, err error, r return result, nil } -func (h *DBHandler) DBSelectReleasesByApp(ctx context.Context, tx *sql.Tx, app string, deleted bool, ignorePrepublishes bool) ([]*DBReleaseWithMetaData, error) { - span, ctx := tracer.StartSpanFromContext(ctx, "DBSelectReleasesByApp") +// DBSelectReleasesByAppLatestEslVersion returns the latest eslversion +// for each release of an app. It includes deleted releases and loads manifests. +func (h *DBHandler) DBSelectReleasesByAppLatestEslVersion(ctx context.Context, tx *sql.Tx, app string, ignorePrepublishes bool) ([]*DBReleaseWithMetaData, error) { + span, ctx := tracer.StartSpanFromContext(ctx, "DBSelectReleasesByAppLatestEslVersion") defer span.Finish() - selectQuery := h.AdaptQuery(fmt.Sprintf( - "SELECT eslVersion, created, appName, metadata, manifests, releaseVersion, deleted, environments " + - " FROM releases " + - " WHERE appName=? AND deleted=?" + - " ORDER BY releaseVersion DESC, eslVersion DESC, created DESC;")) + selectQuery := h.AdaptQuery( + `SELECT + releases.eslVersion, + releases.created, + releases.appName, + releases.metadata, + releases.manifests, + releases.releaseVersion, + releases.deleted, + releases.environments + FROM ( + SELECT + MAX(eslVersion) AS latestEslVersion, + appname, + releaseversion + FROM + releases + WHERE + appname=? + GROUP BY + appname, releaseversion + ) as currentEslReleases + JOIN + releases + ON + currentEslReleases.appname = releases.appname + AND + currentEslReleases.latesteslversion = releases.eslversion + AND + currentEslReleases.releaseversion = releases.releaseversion + ORDER BY currentEslReleases.releaseversion DESC;`, + ) span.SetTag("query", selectQuery) rows, err := tx.QueryContext( ctx, selectQuery, app, - deleted, ) return h.processReleaseRows(ctx, err, rows, ignorePrepublishes, true) } -func (h *DBHandler) DBSelectReleasesByAppLatestEslVersion(ctx context.Context, tx *sql.Tx, app string, deleted bool, ignorePrepublishes bool) ([]*DBReleaseWithMetaData, error) { - span, ctx := tracer.StartSpanFromContext(ctx, "DBSelectReleasesByApp") +func (h *DBHandler) DBSelectReleasesByAppOrderedByEslVersion(ctx context.Context, tx *sql.Tx, app string, deleted bool, ignorePrepublishes bool) ([]*DBReleaseWithMetaData, error) { + span, ctx := tracer.StartSpanFromContext(ctx, "DBSelectReleasesByAppOrderedByEslVersion") defer span.Finish() selectQuery := h.AdaptQuery(fmt.Sprintf( "SELECT eslVersion, created, appName, metadata, releaseVersion, deleted, environments " + diff --git a/pkg/db/db_test.go b/pkg/db/db_test.go index 7221dffef..35563e9e6 100644 --- a/pkg/db/db_test.go +++ b/pkg/db/db_test.go @@ -2311,6 +2311,35 @@ func TestReadReleasesByApp(t *testing.T) { }, }, }, + { + Name: "Retrieve deleted release", + Releases: []DBReleaseWithMetaData{ + { + EslVersion: 1, + ReleaseNumber: 10, + App: "app1", + Manifests: DBReleaseManifests{Manifests: map[string]string{"dev": "manifest1"}}, + }, + { + EslVersion: 2, + ReleaseNumber: 10, + App: "app1", + Deleted: true, + Manifests: DBReleaseManifests{Manifests: map[string]string{"dev": "manifest1"}}, + }, + }, + AppName: "app1", + Expected: []*DBReleaseWithMetaData{ + { + EslVersion: 2, + ReleaseNumber: 10, + Deleted: true, + App: "app1", + Manifests: DBReleaseManifests{Manifests: map[string]string{"dev": "manifest1"}}, + Environments: []string{"dev"}, + }, + }, + }, { Name: "Retrieve multiple releases", Releases: []DBReleaseWithMetaData{ @@ -2525,7 +2554,7 @@ func TestReadReleasesByApp(t *testing.T) { return fmt.Errorf("error while writing release, error: %w", err) } } - releases, err := dbHandler.DBSelectReleasesByApp(ctx, transaction, tc.AppName, false, !tc.RetrievePrepublishes) + releases, err := dbHandler.DBSelectReleasesByAppLatestEslVersion(ctx, transaction, tc.AppName, !tc.RetrievePrepublishes) if err != nil { return fmt.Errorf("error while selecting release, error: %w", err) } diff --git a/services/cd-service/pkg/repository/transformer.go b/services/cd-service/pkg/repository/transformer.go index c308054ab..c684bb02c 100644 --- a/services/cd-service/pkg/repository/transformer.go +++ b/services/cd-service/pkg/repository/transformer.go @@ -672,7 +672,7 @@ func (c *CreateApplicationVersion) Transform( sortedKeys := sorting.SortKeys(c.Manifests) if state.DBHandler.ShouldUseOtherTables() { - prevRelease, err := state.DBHandler.DBSelectReleasesByAppLatestEslVersion(ctx, transaction, c.Application, false, false) + prevRelease, err := state.DBHandler.DBSelectReleasesByAppOrderedByEslVersion(ctx, transaction, c.Application, false, false) if err != nil { return "", err } @@ -1711,7 +1711,7 @@ func (u *DeleteEnvFromApp) Transform( return "", err } if state.DBHandler.ShouldUseOtherTables() { - releases, err := state.DBHandler.DBSelectReleasesByApp(ctx, transaction, u.Application, false, true) + releases, err := state.DBHandler.DBSelectReleasesByAppLatestEslVersion(ctx, transaction, u.Application, true) if err != nil { return "", err } diff --git a/services/cd-service/pkg/repository/transformer_db_test.go b/services/cd-service/pkg/repository/transformer_db_test.go index 16b87918d..973c987e8 100644 --- a/services/cd-service/pkg/repository/transformer_db_test.go +++ b/services/cd-service/pkg/repository/transformer_db_test.go @@ -22,11 +22,12 @@ import ( "encoding/json" "errors" "fmt" - "github.com/lib/pq" "regexp" "testing" gotime "time" + "github.com/lib/pq" + "github.com/freiheit-com/kuberpult/pkg/api/v1" "github.com/freiheit-com/kuberpult/pkg/event" @@ -2264,7 +2265,7 @@ func TestDeleteEnvFromAppWithDB(t *testing.T) { if err != nil { return fmt.Errorf("error: %v", err) } - releases, err2 := state.DBHandler.DBSelectReleasesByApp(ctx, transaction, appName, false, true) + releases, err2 := state.DBHandler.DBSelectReleasesByAppLatestEslVersion(ctx, transaction, appName, true) if err2 != nil { return fmt.Errorf("error retrieving release: %v", err2) } @@ -3649,7 +3650,7 @@ func TestTimestampConsistency(t *testing.T) { t.Fatalf("error mismatch on envAcceptance(-want, +got):\n%s", diff) } //Release - releases, err := state.DBHandler.DBSelectReleasesByApp(ctx, transaction, testAppName, false, true) + releases, err := state.DBHandler.DBSelectReleasesByAppLatestEslVersion(ctx, transaction, testAppName, true) if err != nil { return err } diff --git a/tests/integration-tests/release_test.go b/tests/integration-tests/release_test.go index 81809fe43..513dcb3a6 100644 --- a/tests/integration-tests/release_test.go +++ b/tests/integration-tests/release_test.go @@ -472,7 +472,7 @@ func callDBForLock(t *testing.T, dbHandler *db.DBHandler, ctx context.Context, e func callDBForReleases(t *testing.T, dbHandler *db.DBHandler, ctx context.Context, appName string) []*db.DBReleaseWithMetaData { release, err := db.WithTransactionMultipleEntriesT(dbHandler, ctx, true, func(ctx context.Context, transaction *sql.Tx) ([]*db.DBReleaseWithMetaData, error) { - return dbHandler.DBSelectReleasesByApp(ctx, transaction, appName, false, true) + return dbHandler.DBSelectReleasesByAppLatestEslVersion(ctx, transaction, appName, true) }) if err != nil { t.Fatalf("DBSelectReleasesByApp failed %s", err) From 0a0d0ac12bfa37cdfe3a5596b8acdf9f68d5161b Mon Sep 17 00:00:00 2001 From: Diogo Nogueira Date: Mon, 21 Oct 2024 16:57:35 +0100 Subject: [PATCH 2/3] fix(manifest-repo-export-service): Changed fetch from HEAD to direct reference (#2055) The manifest repo export service fetches the reference to the current commit directly in all of the code base. Updated this fetch the HEAD to get the specific reference for consistency. Ref: SRX-B66T2N --- .../pkg/cmd/server.go | 4 ++-- .../pkg/repository/repository.go | 16 ++++++---------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/services/manifest-repo-export-service/pkg/cmd/server.go b/services/manifest-repo-export-service/pkg/cmd/server.go index 9f2e29bad..ac10c4b97 100755 --- a/services/manifest-repo-export-service/pkg/cmd/server.go +++ b/services/manifest-repo-export-service/pkg/cmd/server.go @@ -344,11 +344,11 @@ func processEsls(ctx context.Context, repo repository.Repository, dbHandler *db. } //Get latest commit. Write esl timestamp and commit hash. - commit, err := repo.GetHeadCommit() + commitId, err := repo.GetHeadCommitId() if err != nil { return err } - return dbHandler.DBWriteCommitTransactionTimestamp(ctx, transaction, commit.Id().String(), esl.Created) + return dbHandler.DBWriteCommitTransactionTimestamp(ctx, transaction, commitId.String(), esl.Created) }) if err != nil { err3 := repo.FetchAndReset(ctx) diff --git a/services/manifest-repo-export-service/pkg/repository/repository.go b/services/manifest-repo-export-service/pkg/repository/repository.go index 50faac7e4..77d7e82a3 100644 --- a/services/manifest-repo-export-service/pkg/repository/repository.go +++ b/services/manifest-repo-export-service/pkg/repository/repository.go @@ -62,7 +62,7 @@ type Repository interface { StateAt(oid *git.Oid) (*State, error) FetchAndReset(ctx context.Context) error PushRepo(ctx context.Context) error - GetHeadCommit() (*git.Commit, error) + GetHeadCommitId() (*git.Oid, error) } type TransformerBatchApplyError struct { @@ -418,17 +418,13 @@ func (r *repository) PushRepo(ctx context.Context) error { return nil } -func (r *repository) GetHeadCommit() (*git.Commit, error) { - ref, err := r.repository.Head() +func (r *repository) GetHeadCommitId() (*git.Oid, error) { + branchHead := fmt.Sprintf("refs/heads/%s", r.config.Branch) + ref, err := r.repository.References.Lookup(branchHead) if err != nil { - return nil, fmt.Errorf("Error fetching HEAD: %v", err) + return nil, fmt.Errorf("Error fetching reference \"%s\": %v", branchHead, err) } - commit, err := r.repository.LookupCommit(ref.Target()) - if err != nil { - return nil, fmt.Errorf("Error transalting into commit: %v", err) - } - return commit, nil - + return ref.Target(), nil } func (r *repository) ApplyTransformersInternal(ctx context.Context, transaction *sql.Tx, transformer Transformer) ([]string, *State, []*TransformerResult, *TransformerBatchApplyError) { From 0c5737760af60430ec2d057a27e83af52806e29c Mon Sep 17 00:00:00 2001 From: Miguel Crespo <162333021+miguel-crespo-fdc@users.noreply.github.com> Date: Tue, 22 Oct 2024 08:41:32 +0100 Subject: [PATCH 3/3] feat(UI): application data is now loaded in UI through the new GetAppDetails endpoint (#2020) Ref: SRX-FIC65Q --------- Co-authored-by: Sven Urbanski --- pkg/api/v1/api.proto | 7 + pkg/db/db.go | 1 + .../cd-service/pkg/repository/repository.go | 12 +- .../pkg/repository/repository_test.go | 19 + .../cd-service/pkg/repository/transformer.go | 1 + .../pkg/repository/transformer_db_test.go | 6 + services/cd-service/pkg/service/overview.go | 129 +++- .../cd-service/pkg/service/overview_test.go | 6 + services/frontend-service/pkg/cmd/server.go | 12 +- .../pkg/handler/commit_deployments_test.go | 2 +- .../frontend-service/src/ui/App/index.tsx | 2 + .../src/ui/Pages/Home/Home.test.tsx | 677 +++++++++++++----- .../ReleaseHistoryPage.test.tsx | 12 +- .../ReleaseHistory/ReleaseHistoryPage.tsx | 18 +- .../ReleaseCard/ReleaseCard.test.tsx | 244 ++++++- .../ReleaseCardMini/ReleaseCardMini.test.tsx | 24 +- .../ReleaseDialog/ReleaseDialog.test.tsx | 195 ++++- .../ReleaseDialog/ReleaseDialog.tsx | 10 +- .../__snapshots__/ReleaseDialog.test.tsx.snap | 35 +- .../ReleaseTrainPrognosis.test.tsx | 60 +- .../ui/components/Releases/Releases.test.tsx | 152 +++- .../src/ui/components/Releases/Releases.tsx | 6 +- .../components/ServiceLane/ServiceLane.scss | 1 + .../ServiceLane/ServiceLane.test.tsx | 400 +++++++---- .../ui/components/ServiceLane/ServiceLane.tsx | 89 ++- .../ui/components/ServiceLane/Warnings.tsx | 10 +- .../src/ui/components/Spinner/Spinner.scss | 9 + .../src/ui/components/Spinner/Spinner.tsx | 14 + .../src/ui/components/TopAppBar/TopAppBar.tsx | 19 +- .../src/ui/utils/store.test.tsx | 308 ++++---- .../frontend-service/src/ui/utils/store.tsx | 176 +++-- 31 files changed, 2028 insertions(+), 628 deletions(-) diff --git a/pkg/api/v1/api.proto b/pkg/api/v1/api.proto index 30a9a75ff..f5d8c6506 100644 --- a/pkg/api/v1/api.proto +++ b/pkg/api/v1/api.proto @@ -434,12 +434,19 @@ message GetOverviewRequest { string git_revision = 1; } +//Lightweight version of application. Only contains name and team. +message OverviewApplication { + string name = 1; + string team = 2; +} + message GetOverviewResponse { map applications = 2; repeated EnvironmentGroup environment_groups = 3; string git_revision = 4; string branch = 5; string manifest_repo_url = 6; + repeated OverviewApplication lightweight_apps = 7; } message EnvironmentGroup { diff --git a/pkg/db/db.go b/pkg/db/db.go index e2246b9a2..752505632 100644 --- a/pkg/db/db.go +++ b/pkg/db/db.go @@ -5662,6 +5662,7 @@ func (h *DBHandler) ReadLatestOverviewCache(ctx context.Context, transaction *sq Branch: "", ManifestRepoUrl: "", Applications: map[string]*api.Application{}, + LightweightApps: []*api.OverviewApplication{}, EnvironmentGroups: []*api.EnvironmentGroup{}, GitRevision: "", } diff --git a/services/cd-service/pkg/repository/repository.go b/services/cd-service/pkg/repository/repository.go index 12a347ea1..b1f992fdc 100644 --- a/services/cd-service/pkg/repository/repository.go +++ b/services/cd-service/pkg/repository/repository.go @@ -2466,6 +2466,16 @@ func (s *State) DBInsertApplicationWithOverview(ctx context.Context, transaction } } } + if shouldDelete { + lApps := make([]*api.OverviewApplication, len(cache.LightweightApps)-1) + + for _, curr := range cache.LightweightApps { + if curr.Name != appName { + lApps = append(lApps, curr) + } + } + cache.LightweightApps = lApps + } err = h.WriteOverviewCache(ctx, transaction, cache) if err != nil { @@ -2528,7 +2538,6 @@ func (s *State) UpdateTopLevelAppInOverview(ctx context.Context, transaction *sq } rels = retrievedReleasesOfApp } - if releasesInDb, err := s.GetApplicationReleasesDB(ctx, transaction, appName, rels); err != nil { return err } else { @@ -2556,6 +2565,7 @@ func (s *State) UpdateTopLevelAppInOverview(ctx context.Context, transaction *sq result.Applications = map[string]*api.Application{} } result.Applications[appName] = &app + result.LightweightApps = append(result.LightweightApps, &api.OverviewApplication{Name: appName, Team: app.Team}) return nil } diff --git a/services/cd-service/pkg/repository/repository_test.go b/services/cd-service/pkg/repository/repository_test.go index 3c2968d39..29c74fc0f 100644 --- a/services/cd-service/pkg/repository/repository_test.go +++ b/services/cd-service/pkg/repository/repository_test.go @@ -2188,6 +2188,12 @@ func TestUpdateOverviewCache(t *testing.T) { Warnings: nil, }, }, + LightweightApps: []*api.OverviewApplication{ + { + Name: "app1", + Team: "", + }, + }, EnvironmentGroups: []*api.EnvironmentGroup{}, }, }, @@ -2228,6 +2234,12 @@ func TestUpdateOverviewCache(t *testing.T) { Warnings: nil, }, }, + LightweightApps: []*api.OverviewApplication{ + { + Name: "app1", + Team: "", + }, + }, EnvironmentGroups: []*api.EnvironmentGroup{ { EnvironmentGroupName: "dev", @@ -2311,6 +2323,12 @@ func TestUpdateOverviewCache(t *testing.T) { Priority: 0, }, }, + LightweightApps: []*api.OverviewApplication{ + { + Name: "app1", + Team: "", + }, + }, GitRevision: "123", Branch: "main", ManifestRepoUrl: "https://example.com", @@ -2337,6 +2355,7 @@ func TestUpdateOverviewCache(t *testing.T) { GitRevision: "123", Branch: "main", ManifestRepoUrl: "https://example.com", + LightweightApps: []*api.OverviewApplication{}, }, }, } diff --git a/services/cd-service/pkg/repository/transformer.go b/services/cd-service/pkg/repository/transformer.go index c684bb02c..0651a83fb 100644 --- a/services/cd-service/pkg/repository/transformer.go +++ b/services/cd-service/pkg/repository/transformer.go @@ -2779,6 +2779,7 @@ func (c *CreateEnvironment) Transform( Applications: map[string]*api.Application{}, EnvironmentGroups: []*api.EnvironmentGroup{}, GitRevision: "0000000000000000000000000000000000000000", + LightweightApps: make([]*api.OverviewApplication, 0), } } if err != nil { diff --git a/services/cd-service/pkg/repository/transformer_db_test.go b/services/cd-service/pkg/repository/transformer_db_test.go index 973c987e8..305b19c3e 100644 --- a/services/cd-service/pkg/repository/transformer_db_test.go +++ b/services/cd-service/pkg/repository/transformer_db_test.go @@ -1735,6 +1735,12 @@ func TestCreateEnvironmentUpdatesOverview(t *testing.T) { }, }, }, + LightweightApps: []*api.OverviewApplication{ + { + Name: "app", + Team: "", + }, + }, EnvironmentGroups: []*api.EnvironmentGroup{ &api.EnvironmentGroup{ EnvironmentGroupName: "development", diff --git a/services/cd-service/pkg/service/overview.go b/services/cd-service/pkg/service/overview.go index 7c4795871..fc29526c7 100644 --- a/services/cd-service/pkg/service/overview.go +++ b/services/cd-service/pkg/service/overview.go @@ -21,24 +21,22 @@ import ( "database/sql" "errors" "fmt" - "github.com/freiheit-com/kuberpult/pkg/mapper" - "google.golang.org/protobuf/types/known/timestamppb" - "os" - "sync" - "sync/atomic" - + api "github.com/freiheit-com/kuberpult/pkg/api/v1" + "github.com/freiheit-com/kuberpult/pkg/db" "github.com/freiheit-com/kuberpult/pkg/grpc" "github.com/freiheit-com/kuberpult/pkg/logger" - "go.uber.org/zap" - + "github.com/freiheit-com/kuberpult/pkg/mapper" + "github.com/freiheit-com/kuberpult/services/cd-service/pkg/notify" + "github.com/freiheit-com/kuberpult/services/cd-service/pkg/repository" git "github.com/libgit2/git2go/v34" + "go.uber.org/zap" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - - api "github.com/freiheit-com/kuberpult/pkg/api/v1" - "github.com/freiheit-com/kuberpult/pkg/db" - "github.com/freiheit-com/kuberpult/services/cd-service/pkg/notify" - "github.com/freiheit-com/kuberpult/services/cd-service/pkg/repository" + "google.golang.org/protobuf/types/known/timestamppb" + "os" + "sort" + "sync" + "sync/atomic" ) type OverviewServiceServer struct { @@ -96,7 +94,10 @@ func (o *OverviewServiceServer) GetAppDetails( if retrievedReleasesOfApp != nil { rels = retrievedReleasesOfApp.Metadata.Releases } - + //Highest to lowest + sort.Slice(rels, func(i, j int) bool { + return rels[j] < rels[i] + }) for _, id := range rels { uid := uint64(id) // we could optimize this by making one query that does return multiples: @@ -142,9 +143,6 @@ func (o *OverviewServiceServer) GetAppDetails( } envGroups := mapper.MapEnvironmentsToGroups(envConfigs) - result.UndeploySummary = deriveUndeploySummary(appName, envGroups) - result.Warnings = db.CalculateWarnings(ctx, appName, envGroups) - // App Locks appLocks, err := o.DBHandler.DBSelectAllActiveAppLocksForApp(ctx, transaction, appName) if err != nil { @@ -217,6 +215,12 @@ func (o *OverviewServiceServer) GetAppDetails( } response.Deployments[envName] = deployment } + result.UndeploySummary = deriveUndeploySummary(appName, response.Deployments) + warnings, err := CalculateWarnings(ctx, transaction, o.Repository.State(), appName, envGroups) + if err != nil { + return nil, err + } + result.Warnings = warnings return result, nil }) if err != nil { @@ -224,7 +228,6 @@ func (o *OverviewServiceServer) GetAppDetails( } response.Application = resultApp return response, nil - } func (o *OverviewServiceServer) GetOverview( @@ -302,6 +305,7 @@ func (o *OverviewServiceServer) getOverview( Applications: map[string]*api.Application{}, EnvironmentGroups: []*api.EnvironmentGroup{}, GitRevision: rev, + LightweightApps: make([]*api.OverviewApplication, 0), } result.ManifestRepoUrl = o.RepositoryConfig.URL result.Branch = o.RepositoryConfig.Branch @@ -453,24 +457,14 @@ func (o *OverviewServiceServer) update(s *repository.State) { o.notify.Notify() } -func deriveUndeploySummary(appName string, groups []*api.EnvironmentGroup) api.UndeploySummary { +func deriveUndeploySummary(appName string, deployments map[string]*api.Deployment) api.UndeploySummary { var allNormal = true var allUndeploy = true - for _, group := range groups { - for _, environment := range group.Environments { - var app, exists = environment.Applications[appName] - if !exists { - continue - } - if app.Version == 0 { - // if the app exists but nothing is deployed, we ignore this - continue - } - if app.UndeployVersion { - allNormal = false - } else { - allUndeploy = false - } + for _, currentDeployment := range deployments { + if currentDeployment.UndeployVersion { + allNormal = false + } else { + allUndeploy = false } } if allUndeploy { @@ -481,3 +475,70 @@ func deriveUndeploySummary(appName string, groups []*api.EnvironmentGroup) api.U } return api.UndeploySummary_MIXED } + +func CalculateWarnings(ctx context.Context, transaction *sql.Tx, state *repository.State, appName string, groups []*api.EnvironmentGroup) ([]*api.Warning, error) { + result := make([]*api.Warning, 0) + for e := 0; e < len(groups); e++ { + group := groups[e] + for i := 0; i < len(groups[e].Environments); i++ { + env := group.Environments[i] + if env.Config.Upstream == nil || env.Config.Upstream.Environment == nil { + // if the env has no upstream, there's nothing to warn about + continue + } + upstreamEnvName := env.Config.GetUpstream().Environment + if upstreamEnvName == nil { + // this is already checked on startup and therefore shouldn't happen here + continue + } + + versionInEnv, err := state.GetEnvironmentApplicationVersion(ctx, transaction, env.Name, appName) + if err != nil { + return nil, err + } + + if versionInEnv == nil { + // appName is not deployed here, ignore it + continue + } + + versionInUpstreamEnv, err := state.GetEnvironmentApplicationVersion(ctx, transaction, *upstreamEnvName, appName) + if err != nil { + return nil, err + } + if versionInUpstreamEnv == nil { + // appName is not deployed upstream... that's unusual! + var warning = api.Warning{ + WarningType: &api.Warning_UpstreamNotDeployed{ + UpstreamNotDeployed: &api.UpstreamNotDeployed{ + UpstreamEnvironment: *upstreamEnvName, + ThisVersion: *versionInEnv, + ThisEnvironment: env.Name, + }, + }, + } + result = append(result, &warning) + continue + } + + appLocks, err := state.GetEnvironmentApplicationLocks(ctx, transaction, env.Name, appName) + if err != nil { + return nil, err + } + if *versionInEnv > *versionInUpstreamEnv && len(appLocks) == 0 { + var warning = api.Warning{ + WarningType: &api.Warning_UnusualDeploymentOrder{ + UnusualDeploymentOrder: &api.UnusualDeploymentOrder{ + UpstreamVersion: *versionInUpstreamEnv, + UpstreamEnvironment: *upstreamEnvName, + ThisVersion: *versionInEnv, + ThisEnvironment: env.Name, + }, + }, + } + result = append(result, &warning) + } + } + } + return result, nil +} diff --git a/services/cd-service/pkg/service/overview_test.go b/services/cd-service/pkg/service/overview_test.go index d949f1018..f3a6363a0 100644 --- a/services/cd-service/pkg/service/overview_test.go +++ b/services/cd-service/pkg/service/overview_test.go @@ -492,6 +492,12 @@ func TestOverviewService(t *testing.T) { Team: "team-123", }, }, + LightweightApps: []*api.OverviewApplication{ + { + Name: "test", + Team: "team-123", + }, + }, GitRevision: "0", }, Setup: []repository.Transformer{ diff --git a/services/frontend-service/pkg/cmd/server.go b/services/frontend-service/pkg/cmd/server.go index 21041b308..535521d8e 100644 --- a/services/frontend-service/pkg/cmd/server.go +++ b/services/frontend-service/pkg/cmd/server.go @@ -650,18 +650,18 @@ func (p *GrpcProxy) GetFailedEsls( return p.EslServiceClient.GetFailedEsls(ctx, in) } -func (p *GrpcProxy) GetAppDetails( - ctx context.Context, - in *api.GetAppDetailsRequest) (*api.GetAppDetailsResponse, error) { - return p.OverviewClient.GetAppDetails(ctx, in) -} - func (p *GrpcProxy) GetOverview( ctx context.Context, in *api.GetOverviewRequest) (*api.GetOverviewResponse, error) { return p.OverviewClient.GetOverview(ctx, in) } +func (p *GrpcProxy) GetAppDetails( + ctx context.Context, + in *api.GetAppDetailsRequest) (*api.GetAppDetailsResponse, error) { + return p.OverviewClient.GetAppDetails(ctx, in) +} + func (p *GrpcProxy) GetGitTags( ctx context.Context, in *api.GetGitTagsRequest) (*api.GetGitTagsResponse, error) { diff --git a/services/frontend-service/pkg/handler/commit_deployments_test.go b/services/frontend-service/pkg/handler/commit_deployments_test.go index 6c6ee3e12..9ca276235 100644 --- a/services/frontend-service/pkg/handler/commit_deployments_test.go +++ b/services/frontend-service/pkg/handler/commit_deployments_test.go @@ -83,7 +83,7 @@ func TestHandleCommitDeployments(t *testing.T) { inputTail: "123456/", failGrpcCall: false, expectedStatusCode: http.StatusOK, - expectedResponse: "{\"deploymentStatus\":{\"app1\":{\"deploymentStatus\":{\"dev\":\"DEPLOYED\",\"prod\":\"UNKNOWN\",\"stage\":\"PENDING\"}}}}\n", + expectedResponse: "{\"deploymentStatus\":{\"app1\":{\"deploymentStatus\":{\"dev\":\"DEPLOYED\", \"prod\":\"UNKNOWN\", \"stage\":\"PENDING\"}}}}\n", }, } for _, tc := range tcs { diff --git a/services/frontend-service/src/ui/App/index.tsx b/services/frontend-service/src/ui/App/index.tsx index 63775fd48..90b0e98c3 100644 --- a/services/frontend-service/src/ui/App/index.tsx +++ b/services/frontend-service/src/ui/App/index.tsx @@ -23,6 +23,7 @@ import { FlushRolloutStatus, PanicOverview, showSnackbarWarn, + updateAppDetails, UpdateFrontendConfig, UpdateOverview, UpdateRolloutStatus, @@ -99,6 +100,7 @@ export const App: React.FC = () => { UpdateOverview.set(result); UpdateOverview.set({ loaded: true }); PanicOverview.set({ error: '' }); + updateAppDetails.set({}); }, (error) => { PanicOverview.set({ error: JSON.stringify({ msg: 'error in streamoverview', error }) }); diff --git a/services/frontend-service/src/ui/Pages/Home/Home.test.tsx b/services/frontend-service/src/ui/Pages/Home/Home.test.tsx index 594572a7d..e782fc394 100644 --- a/services/frontend-service/src/ui/Pages/Home/Home.test.tsx +++ b/services/frontend-service/src/ui/Pages/Home/Home.test.tsx @@ -15,10 +15,16 @@ along with kuberpult. If not, see Copyright freiheit.com*/ import { render, renderHook } from '@testing-library/react'; import { Home } from './Home'; -import { searchCustomFilter, UpdateOverview, useApplicationsFilteredAndSorted, useTeamNames } from '../../utils/store'; +import { + searchCustomFilter, + updateAppDetails, + UpdateOverview, + useApplicationsFilteredAndSorted, + useTeamNames, +} from '../../utils/store'; import { Spy } from 'spy4js'; import { MemoryRouter } from 'react-router-dom'; -import { Application, UndeploySummary } from '../../../api/api'; +import { Application, GetAppDetailsResponse, GetOverviewResponse, UndeploySummary } from '../../../api/api'; import { fakeLoadEverything, enableDexAuth } from '../../../setupTests'; const mock_ServiceLane = Spy.mockReactComponents('../../components/ServiceLane/ServiceLane', 'ServiceLane'); @@ -47,21 +53,55 @@ describe('App', () => { }; UpdateOverview.set({ applications: sampleApps, + lightweightApps: [ + { + name: sampleApps.app1.name, + team: sampleApps.app1.team, + }, + { + name: sampleApps.app2.name, + team: sampleApps.app2.team, + }, + { + name: sampleApps.app3.name, + team: sampleApps.app3.team, + }, + ], + }); + updateAppDetails.set({ + [sampleApps.app1.name]: { + application: sampleApps.app1, + deployments: {}, + appLocks: {}, + teamLocks: {}, + }, + [sampleApps.app2.name]: { + application: sampleApps.app2, + deployments: {}, + appLocks: {}, + teamLocks: {}, + }, + [sampleApps.app2.name]: { + application: sampleApps.app2, + deployments: {}, + appLocks: {}, + teamLocks: {}, + }, }); fakeLoadEverything(true); getWrapper(); // then apps are sorted and Service Lane is called expect(mock_ServiceLane.ServiceLane.getCallArgument(0, 0)).toStrictEqual({ - application: sampleApps.app1, + application: { name: sampleApps.app1.name, team: sampleApps.app1.team }, hideMinors: false, }); expect(mock_ServiceLane.ServiceLane.getCallArgument(1, 0)).toStrictEqual({ - application: sampleApps.app2, + application: { name: sampleApps.app2.name, team: sampleApps.app2.team }, hideMinors: false, }); expect(mock_ServiceLane.ServiceLane.getCallArgument(2, 0)).toStrictEqual({ - application: sampleApps.app3, + application: { name: sampleApps.app3.name, team: sampleApps.app3.team }, hideMinors: false, }); }); @@ -98,6 +138,40 @@ describe('App', () => { }; UpdateOverview.set({ applications: sampleApps, + lightweightApps: [ + { + name: sampleApps.app1.name, + team: sampleApps.app1.team, + }, + { + name: sampleApps.app2.name, + team: sampleApps.app2.team, + }, + { + name: sampleApps.app3.name, + team: sampleApps.app3.team, + }, + ], + }); + updateAppDetails.set({ + [sampleApps.app1.name]: { + application: sampleApps.app1, + deployments: {}, + appLocks: {}, + teamLocks: {}, + }, + [sampleApps.app2.name]: { + application: sampleApps.app2, + deployments: {}, + appLocks: {}, + teamLocks: {}, + }, + [sampleApps.app2.name]: { + application: sampleApps.app2, + deployments: {}, + appLocks: {}, + teamLocks: {}, + }, }); fakeLoadEverything(true); enableDexAuth(true); @@ -105,15 +179,15 @@ describe('App', () => { // then apps are sorted and Service Lane is called expect(mock_ServiceLane.ServiceLane.getCallArgument(0, 0)).toStrictEqual({ - application: sampleApps.app1, + application: { name: sampleApps.app1.name, team: sampleApps.app1.team }, hideMinors: false, }); expect(mock_ServiceLane.ServiceLane.getCallArgument(1, 0)).toStrictEqual({ - application: sampleApps.app2, + application: { name: sampleApps.app2.name, team: sampleApps.app2.team }, hideMinors: false, }); expect(mock_ServiceLane.ServiceLane.getCallArgument(2, 0)).toStrictEqual({ - application: sampleApps.app3, + application: { name: sampleApps.app3.name, team: sampleApps.app3.team }, hideMinors: false, }); }); @@ -122,113 +196,241 @@ describe('App', () => { describe('Get teams from application list (useTeamNames)', () => { interface dataT { name: string; - applications: { [key: string]: Application }; + appDetails: { [key: string]: GetAppDetailsResponse }; + overview: GetOverviewResponse; expectedTeams: string[]; } const data: dataT[] = [ { name: 'right amount of teams - 4 sorted results', - applications: { + + overview: { + applications: {}, + lightweightApps: [ + { + name: 'foo', + team: 'dummy', + }, + { + name: 'bar', + team: 'test', + }, + { + name: 'example', + team: 'test2', + }, + { + name: 'team', + team: 'foo', + }, + ], + environmentGroups: [], + gitRevision: '', + branch: '', + manifestRepoUrl: '', + }, + appDetails: { foo: { - name: 'foo', - releases: [], - sourceRepoUrl: 'http://foo.com', - team: 'dummy', - undeploySummary: UndeploySummary.NORMAL, - warnings: [], + application: { + name: 'foo', + releases: [], + sourceRepoUrl: 'http://foo.com', + team: 'dummy', + undeploySummary: UndeploySummary.NORMAL, + warnings: [], + }, + appLocks: {}, + teamLocks: {}, + deployments: {}, }, bar: { - name: 'bar', - releases: [], - sourceRepoUrl: 'http://bar.com', - team: 'test', - undeploySummary: UndeploySummary.NORMAL, - warnings: [], + application: { + name: 'bar', + releases: [], + sourceRepoUrl: 'http://bar.com', + team: 'test', + undeploySummary: UndeploySummary.NORMAL, + warnings: [], + }, + appLocks: {}, + teamLocks: {}, + deployments: {}, }, example: { - name: 'example', - releases: [], - sourceRepoUrl: 'http://example.com', - team: 'test2', - undeploySummary: UndeploySummary.NORMAL, - warnings: [], + application: { + name: 'example', + releases: [], + sourceRepoUrl: 'http://example.com', + team: 'test2', + undeploySummary: UndeploySummary.NORMAL, + warnings: [], + }, + appLocks: {}, + teamLocks: {}, + deployments: {}, }, team: { - name: 'team', - releases: [], - sourceRepoUrl: 'http://team.com', - team: 'foo', - undeploySummary: UndeploySummary.NORMAL, - warnings: [], + application: { + name: 'team', + releases: [], + sourceRepoUrl: 'http://team.com', + team: 'foo', + undeploySummary: UndeploySummary.NORMAL, + warnings: [], + }, + appLocks: {}, + teamLocks: {}, + deployments: {}, }, }, expectedTeams: ['dummy', 'foo', 'test', 'test2'], }, { name: "doesn't collect duplicate team names - 2 sorted results", - applications: { + overview: { + applications: {}, + lightweightApps: [ + { + name: 'foo', + team: 'dummy', + }, + { + name: 'bar', + team: 'dummy', + }, + { + name: 'team', + team: 'foo', + }, + ], + environmentGroups: [], + gitRevision: '', + branch: '', + manifestRepoUrl: '', + }, + appDetails: { foo: { - name: 'foo', - releases: [], - sourceRepoUrl: 'http://foo.com', - team: 'dummy', - undeploySummary: UndeploySummary.NORMAL, - warnings: [], + application: { + name: 'foo', + releases: [], + sourceRepoUrl: 'http://foo.com', + team: 'dummy', + undeploySummary: UndeploySummary.NORMAL, + warnings: [], + }, + appLocks: {}, + teamLocks: {}, + deployments: {}, }, bar: { - name: 'bar', - releases: [], - sourceRepoUrl: 'http://bar.com', - team: 'dummy', - undeploySummary: UndeploySummary.NORMAL, - warnings: [], + application: { + name: 'bar', + releases: [], + sourceRepoUrl: 'http://bar.com', + team: 'test', + undeploySummary: UndeploySummary.NORMAL, + warnings: [], + }, + appLocks: {}, + teamLocks: {}, + deployments: {}, }, team: { - name: 'team', - releases: [], - sourceRepoUrl: 'http://team.com', - team: 'foo', - undeploySummary: UndeploySummary.NORMAL, - warnings: [], + application: { + name: 'team', + releases: [], + sourceRepoUrl: 'http://team.com', + team: 'foo', + undeploySummary: UndeploySummary.NORMAL, + warnings: [], + }, + appLocks: {}, + teamLocks: {}, + deployments: {}, }, }, expectedTeams: ['dummy', 'foo'], }, { name: "doesn't collect empty team names and adds option to dropdown - 2 sorted results", - applications: { + overview: { + applications: {}, + lightweightApps: [ + { + name: 'foo', + team: '', + }, + { + name: 'bar', + team: 'test', + }, + { + name: 'example', + team: '', + }, + { + name: 'team', + team: 'foo', + }, + ], + environmentGroups: [], + gitRevision: '', + branch: '', + manifestRepoUrl: '', + }, + appDetails: { foo: { - name: 'foo', - releases: [], - sourceRepoUrl: 'http://foo.com', - team: '', - undeploySummary: UndeploySummary.NORMAL, - warnings: [], + application: { + name: 'foo', + releases: [], + sourceRepoUrl: 'http://foo.com', + team: '', + undeploySummary: UndeploySummary.NORMAL, + warnings: [], + }, + appLocks: {}, + teamLocks: {}, + deployments: {}, }, bar: { - name: 'bar', - releases: [], - sourceRepoUrl: 'http://bar.com', - team: 'test', - undeploySummary: UndeploySummary.NORMAL, - warnings: [], + application: { + name: 'bar', + releases: [], + sourceRepoUrl: 'http://bar.com', + team: 'test', + undeploySummary: UndeploySummary.NORMAL, + warnings: [], + }, + appLocks: {}, + teamLocks: {}, + deployments: {}, }, example: { - name: 'example', - releases: [], - sourceRepoUrl: 'http://example.com', - team: '', - undeploySummary: UndeploySummary.NORMAL, - warnings: [], + application: { + name: 'example', + releases: [], + sourceRepoUrl: 'http://example.com', + team: '', + undeploySummary: UndeploySummary.NORMAL, + warnings: [], + }, + appLocks: {}, + teamLocks: {}, + deployments: {}, }, team: { - name: 'team', - releases: [], - sourceRepoUrl: 'http://team.com', - team: 'foo', - undeploySummary: UndeploySummary.NORMAL, - warnings: [], + application: { + name: 'team', + releases: [], + sourceRepoUrl: 'http://team.com', + team: 'foo', + undeploySummary: UndeploySummary.NORMAL, + warnings: [], + }, + appLocks: {}, + teamLocks: {}, + deployments: {}, }, }, expectedTeams: ['', 'foo', 'test'], @@ -238,7 +440,8 @@ describe('Get teams from application list (useTeamNames)', () => { describe.each(data)(`Renders an Application Card`, (testcase) => { it(testcase.name, () => { // given - UpdateOverview.set({ applications: testcase.applications }); + UpdateOverview.set(testcase.overview); + UpdateOverview.set(testcase.appDetails); // when const teamNames = renderHook(() => useTeamNames()).result.current; expect(teamNames).toStrictEqual(testcase.expectedTeams); @@ -250,46 +453,92 @@ describe('Get applications from selected teams (useApplicationsFilteredAndSorted interface dataT { name: string; selectedTeams: string[]; - applications: { [key: string]: Application }; + Overview: GetOverviewResponse; expectedNumOfTeams: number; + appDetails: { [key: string]: GetAppDetailsResponse }; } const data: dataT[] = [ { name: 'gets filtered apps by team - 2 results', selectedTeams: ['dummy', 'foo'], - applications: { + Overview: { + applications: {}, + environmentGroups: [], + gitRevision: '', + branch: '', + manifestRepoUrl: '', + lightweightApps: [ + { + name: 'foo', + team: 'dummy', + }, + { + name: 'bar', + team: 'test', + }, + { + name: 'example', + team: 'test2', + }, + { + name: 'team', + team: 'foo', + }, + ], + }, + appDetails: { foo: { - name: 'foo', - releases: [], - sourceRepoUrl: 'http://foo.com', - team: 'dummy', - undeploySummary: UndeploySummary.NORMAL, - warnings: [], + application: { + name: 'foo', + releases: [], + sourceRepoUrl: 'http://foo.com', + team: 'dummy', + undeploySummary: UndeploySummary.NORMAL, + warnings: [], + }, + appLocks: {}, + teamLocks: {}, + deployments: {}, }, bar: { - name: 'bar', - releases: [], - sourceRepoUrl: 'http://bar.com', - team: 'test', - undeploySummary: UndeploySummary.NORMAL, - warnings: [], + application: { + name: 'bar', + releases: [], + sourceRepoUrl: 'http://bar.com', + team: 'test', + undeploySummary: UndeploySummary.NORMAL, + warnings: [], + }, + appLocks: {}, + teamLocks: {}, + deployments: {}, }, example: { - name: 'example', - releases: [], - sourceRepoUrl: 'http://example.com', - team: 'test2', - undeploySummary: UndeploySummary.NORMAL, - warnings: [], + application: { + name: 'example', + releases: [], + sourceRepoUrl: 'http://example.com', + team: 'test2', + undeploySummary: UndeploySummary.NORMAL, + warnings: [], + }, + appLocks: {}, + teamLocks: {}, + deployments: {}, }, team: { - name: 'team', - releases: [], - sourceRepoUrl: 'http://team.com', - team: 'foo', - undeploySummary: UndeploySummary.NORMAL, - warnings: [], + application: { + name: 'team', + releases: [], + sourceRepoUrl: 'http://team.com', + team: 'foo', + undeploySummary: UndeploySummary.NORMAL, + warnings: [], + }, + appLocks: {}, + teamLocks: {}, + deployments: {}, }, }, expectedNumOfTeams: 2, @@ -297,69 +546,137 @@ describe('Get applications from selected teams (useApplicationsFilteredAndSorted { name: 'shows both applications of the selected team - 2 results', selectedTeams: ['dummy'], - applications: { + Overview: { + applications: {}, + environmentGroups: [], + gitRevision: '', + branch: '', + manifestRepoUrl: '', + lightweightApps: [ + { + name: 'foo', + team: 'dummy', + }, + { + name: 'bar', + team: 'dummy', + }, + { + name: 'team', + team: 'foo', + }, + ], + }, + expectedNumOfTeams: 2, + appDetails: { foo: { - name: 'foo', - releases: [], - sourceRepoUrl: 'http://foo.com', - team: 'dummy', - undeploySummary: UndeploySummary.NORMAL, - warnings: [], + application: { + name: 'foo', + releases: [], + sourceRepoUrl: 'http://foo.com', + team: 'dummy', + undeploySummary: UndeploySummary.NORMAL, + warnings: [], + }, + appLocks: {}, + teamLocks: {}, + deployments: {}, }, bar: { - name: 'bar', - releases: [], - sourceRepoUrl: 'http://bar.com', - team: 'dummy', - undeploySummary: UndeploySummary.NORMAL, - warnings: [], + application: { + name: 'bar', + releases: [], + sourceRepoUrl: 'http://bar.com', + team: 'test', + undeploySummary: UndeploySummary.NORMAL, + warnings: [], + }, + appLocks: {}, + teamLocks: {}, + deployments: {}, }, team: { - name: 'team', - releases: [], - sourceRepoUrl: 'http://team.com', - team: 'foo', - undeploySummary: UndeploySummary.NORMAL, - warnings: [], + application: { + name: 'team', + releases: [], + sourceRepoUrl: 'http://team.com', + team: 'foo', + undeploySummary: UndeploySummary.NORMAL, + warnings: [], + }, + appLocks: {}, + teamLocks: {}, + deployments: {}, }, }, - expectedNumOfTeams: 2, }, { name: 'no teams selected (shows every application) - 4 results', selectedTeams: [], - applications: { + Overview: { + applications: {}, + environmentGroups: [], + gitRevision: '', + branch: '', + manifestRepoUrl: '', + lightweightApps: [ + { + name: 'foo', + team: 'dummy', + }, + { + name: 'bar', + team: 'test', + }, + { + name: 'team', + team: 'foo', + }, + { + name: 'example', + team: 'test2', + }, + ], + }, + appDetails: { foo: { - name: 'foo', - releases: [], - sourceRepoUrl: 'http://foo.com', - team: '', - undeploySummary: UndeploySummary.NORMAL, - warnings: [], + application: { + name: 'foo', + releases: [], + sourceRepoUrl: 'http://foo.com', + team: 'dummy', + undeploySummary: UndeploySummary.NORMAL, + warnings: [], + }, + appLocks: {}, + teamLocks: {}, + deployments: {}, }, bar: { - name: 'bar', - releases: [], - sourceRepoUrl: 'http://bar.com', - team: 'test', - undeploySummary: UndeploySummary.NORMAL, - warnings: [], - }, - example: { - name: 'example', - releases: [], - sourceRepoUrl: 'http://example.com', - team: '', - undeploySummary: UndeploySummary.NORMAL, - warnings: [], + application: { + name: 'bar', + releases: [], + sourceRepoUrl: 'http://bar.com', + team: 'test', + undeploySummary: UndeploySummary.NORMAL, + warnings: [], + }, + appLocks: {}, + teamLocks: {}, + deployments: {}, }, team: { - name: 'team', - releases: [], - sourceRepoUrl: 'http://team.com', - team: 'foo', - undeploySummary: UndeploySummary.NORMAL, - warnings: [], + application: { + name: 'team', + releases: [], + sourceRepoUrl: 'http://team.com', + team: 'foo', + undeploySummary: UndeploySummary.NORMAL, + warnings: [], + }, + appLocks: {}, + teamLocks: {}, + deployments: {}, }, }, expectedNumOfTeams: 4, @@ -367,22 +684,49 @@ describe('Get applications from selected teams (useApplicationsFilteredAndSorted { name: 'selected team has no assigned applications - 0 results', selectedTeams: ['thisTeamDoesntExist'], - applications: { + Overview: { + applications: {}, + environmentGroups: [], + gitRevision: '', + branch: '', + manifestRepoUrl: '', + lightweightApps: [ + { + name: 'foo', + team: 'dummy', + }, + { + name: 'bar', + team: 'test', + }, + ], + }, + appDetails: { foo: { - name: 'foo', - releases: [], - sourceRepoUrl: 'http://foo.com', - team: 'dummy', - undeploySummary: UndeploySummary.NORMAL, - warnings: [], + application: { + name: 'foo', + releases: [], + sourceRepoUrl: 'http://foo.com', + team: 'dummy', + undeploySummary: UndeploySummary.NORMAL, + warnings: [], + }, + appLocks: {}, + teamLocks: {}, + deployments: {}, }, bar: { - name: 'bar', - releases: [], - sourceRepoUrl: 'http://bar.com', - team: 'test', - undeploySummary: UndeploySummary.NORMAL, - warnings: [], + application: { + name: 'bar', + releases: [], + sourceRepoUrl: 'http://bar.com', + team: 'test', + undeploySummary: UndeploySummary.NORMAL, + warnings: [], + }, + appLocks: {}, + teamLocks: {}, + deployments: {}, }, }, expectedNumOfTeams: 0, @@ -392,7 +736,8 @@ describe('Get applications from selected teams (useApplicationsFilteredAndSorted describe.each(data)(`Renders an Application Card`, (testcase) => { it(testcase.name, () => { // given - UpdateOverview.set({ applications: testcase.applications }); + UpdateOverview.set(testcase.Overview); + updateAppDetails.set(testcase.appDetails); // when const numOfTeams = renderHook(() => useApplicationsFilteredAndSorted(testcase.selectedTeams, false, '')) .result.current.length; diff --git a/services/frontend-service/src/ui/Pages/ReleaseHistory/ReleaseHistoryPage.test.tsx b/services/frontend-service/src/ui/Pages/ReleaseHistory/ReleaseHistoryPage.test.tsx index 7d2d6e8ed..faebd8f98 100644 --- a/services/frontend-service/src/ui/Pages/ReleaseHistory/ReleaseHistoryPage.test.tsx +++ b/services/frontend-service/src/ui/Pages/ReleaseHistory/ReleaseHistoryPage.test.tsx @@ -17,8 +17,9 @@ import { render } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import { ReleaseHistoryPage } from './ReleaseHistoryPage'; import { fakeLoadEverything, enableDexAuth } from '../../../setupTests'; +import { updateAppDetails } from '../../utils/store'; -describe('LocksPage', () => { +describe('ReleaseHistoryPage', () => { const getNode = (): JSX.Element | any => ( @@ -81,7 +82,16 @@ describe('LocksPage', () => { if (testcase.enableDex) { enableDexAuth(testcase.enableDexValidToken); } + + updateAppDetails.set({ + '': { + deployments: {}, + appLocks: {}, + teamLocks: {}, + }, + }); const { container } = getWrapper(); + expect(container.getElementsByClassName('main-content')).toHaveLength(testcase.expectedNumMainContent); expect(container.getElementsByClassName('spinner')).toHaveLength(testcase.expectedNumSpinner); expect( diff --git a/services/frontend-service/src/ui/Pages/ReleaseHistory/ReleaseHistoryPage.tsx b/services/frontend-service/src/ui/Pages/ReleaseHistory/ReleaseHistoryPage.tsx index 04388d200..7e84aecd5 100644 --- a/services/frontend-service/src/ui/Pages/ReleaseHistory/ReleaseHistoryPage.tsx +++ b/services/frontend-service/src/ui/Pages/ReleaseHistory/ReleaseHistoryPage.tsx @@ -14,19 +14,29 @@ along with kuberpult. If not, see Copyright freiheit.com*/ import { Releases } from '../../components/Releases/Releases'; -import { useGlobalLoadingState } from '../../utils/store'; -import React from 'react'; +import { getAppDetails, useAppDetailsForApp, useGlobalLoadingState } from '../../utils/store'; +import React, { useEffect } from 'react'; import { TopAppBar } from '../../components/TopAppBar/TopAppBar'; +import { useAzureAuthSub } from '../../utils/AzureAuthProvider'; +import { Spinner } from '../../components/Spinner/Spinner'; export const ReleaseHistoryPage: React.FC = () => { const url = window.location.pathname.split('/'); const app_name = url[url.length - 1]; - + const appDetails = useAppDetailsForApp(app_name); + const { authHeader } = useAzureAuthSub((auth) => auth); const element = useGlobalLoadingState(); + + useEffect(() => { + getAppDetails(app_name, authHeader); + }, [app_name, authHeader]); + if (element) { return element; } - + if (!appDetails || element) { + return ; + } return (
diff --git a/services/frontend-service/src/ui/components/ReleaseCard/ReleaseCard.test.tsx b/services/frontend-service/src/ui/components/ReleaseCard/ReleaseCard.test.tsx index d1a4ef2c7..c5c9ece79 100644 --- a/services/frontend-service/src/ui/components/ReleaseCard/ReleaseCard.test.tsx +++ b/services/frontend-service/src/ui/components/ReleaseCard/ReleaseCard.test.tsx @@ -15,11 +15,12 @@ along with kuberpult. If not, see Copyright freiheit.com*/ import { ReleaseCard, ReleaseCardProps } from './ReleaseCard'; import { render } from '@testing-library/react'; -import { UpdateOverview, UpdateRolloutStatus } from '../../utils/store'; +import { updateAppDetails, UpdateOverview, UpdateRolloutStatus } from '../../utils/store'; import { MemoryRouter } from 'react-router-dom'; import { Environment, EnvironmentGroup, + GetAppDetailsResponse, Priority, Release, RolloutStatus, @@ -46,11 +47,40 @@ describe('Release Card', () => { }; rels: Release[]; environments: { [key: string]: Environment }; + appDetails: { [key: string]: GetAppDetailsResponse }; }; const data: TestData[] = [ { name: 'using a sample release - useRelease hook', props: { app: 'test1', version: 2 }, + appDetails: { + test1: { + application: { + name: 'test1', + releases: [ + { + version: 2, + sourceMessage: 'test-rel', + undeployVersion: false, + sourceCommitId: 'commit123', + sourceAuthor: 'author', + prNumber: '666', + createdAt: new Date(2023, 6, 6), + displayVersion: '2', + isMinor: false, + isPrepublish: false, + }, + ], + team: 'test-team', + sourceRepoUrl: '', + undeploySummary: UndeploySummary.NORMAL, + warnings: [], + }, + deployments: {}, + appLocks: {}, + teamLocks: {}, + }, + }, rels: [ { version: 2, @@ -70,6 +100,34 @@ describe('Release Card', () => { { name: 'using a full release - component test', props: { app: 'test2', version: 2 }, + appDetails: { + test2: { + application: { + name: 'test2', + releases: [ + { + undeployVersion: false, + version: 2, + sourceMessage: 'test-rel', + sourceCommitId: '12s3', + sourceAuthor: 'test-author', + prNumber: '666', + createdAt: new Date(2002), + displayVersion: '2', + isMinor: true, + isPrepublish: false, + }, + ], + team: 'test-team', + sourceRepoUrl: '', + undeploySummary: UndeploySummary.NORMAL, + warnings: [], + }, + deployments: {}, + appLocks: {}, + teamLocks: {}, + }, + }, rels: [ { undeployVersion: false, @@ -89,6 +147,40 @@ describe('Release Card', () => { { name: 'using a deployed release - useDeployedAt test', props: { app: 'test2', version: 2 }, + appDetails: { + test2: { + application: { + name: 'test2', + releases: [ + { + undeployVersion: false, + version: 2, + sourceMessage: 'test-rel', + sourceCommitId: '12s3', + sourceAuthor: 'test-author', + prNumber: '666', + createdAt: new Date(2002), + displayVersion: '2', + isMinor: true, + isPrepublish: false, + }, + ], + team: 'test-team', + sourceRepoUrl: '', + undeploySummary: UndeploySummary.NORMAL, + warnings: [], + }, + deployments: { + foo: { + version: 2, + queuedVersion: 0, + undeployVersion: false, + }, + }, + appLocks: {}, + teamLocks: {}, + }, + }, rels: [ { version: 2, @@ -126,6 +218,40 @@ describe('Release Card', () => { { name: 'using an undeployed release - useDeployedAt test', props: { app: 'test2', version: 2 }, + appDetails: { + test2: { + application: { + name: 'test2', + releases: [ + { + undeployVersion: false, + version: 2, + sourceMessage: 'test-rel', + sourceCommitId: '12s3', + sourceAuthor: 'test-author', + prNumber: '666', + createdAt: new Date(2002), + displayVersion: '2', + isMinor: true, + isPrepublish: false, + }, + ], + team: 'test-team', + sourceRepoUrl: '', + undeploySummary: UndeploySummary.NORMAL, + warnings: [], + }, + deployments: { + foo: { + version: 2, + queuedVersion: 0, + undeployVersion: false, + }, + }, + appLocks: {}, + teamLocks: {}, + }, + }, rels: [ { version: 2, @@ -163,6 +289,40 @@ describe('Release Card', () => { { name: 'using another environment - useDeployedAt test', props: { app: 'test2', version: 2 }, + appDetails: { + test2: { + application: { + name: 'test2', + releases: [ + { + version: 2, + sourceMessage: 'test-rel', + sourceCommitId: 'commit123', + undeployVersion: false, + sourceAuthor: 'test-author', + prNumber: '666', + createdAt: new Date(2023, 6, 6), + displayVersion: '2', + isMinor: false, + isPrepublish: false, + }, + ], + team: 'test-team', + sourceRepoUrl: '', + undeploySummary: UndeploySummary.NORMAL, + warnings: [], + }, + deployments: { + foo: { + version: 2, + queuedVersion: 0, + undeployVersion: false, + }, + }, + appLocks: {}, + teamLocks: {}, + }, + }, rels: [ { version: 2, @@ -201,6 +361,40 @@ describe('Release Card', () => { { name: 'using a prepublished release', props: { app: 'test2', version: 2 }, + appDetails: { + test2: { + application: { + name: 'test2', + releases: [ + { + undeployVersion: false, + version: 2, + sourceMessage: 'test-rel', + sourceCommitId: '12s3', + sourceAuthor: 'test-author', + prNumber: '666', + createdAt: new Date(2002), + displayVersion: '2', + isMinor: true, + isPrepublish: true, + }, + ], + team: 'test-team', + sourceRepoUrl: '', + undeploySummary: UndeploySummary.NORMAL, + warnings: [], + }, + deployments: { + foo: { + version: 2, + queuedVersion: 0, + undeployVersion: false, + }, + }, + appLocks: {}, + teamLocks: {}, + }, + }, rels: [ { undeployVersion: false, @@ -237,6 +431,7 @@ describe('Release Card', () => { }, environmentGroups: [], }); + updateAppDetails.set(testcase.appDetails); const { container } = getWrapper(testcase.props); // then @@ -293,11 +488,56 @@ describe('Release Card Rollout Status', () => { rolloutStatus: StreamStatusResponse[]; expectedStatusIcon: RolloutStatus; expectedRolloutDetails: { [name: string]: RolloutStatus }; + appDetails: { [key: string]: GetAppDetailsResponse }; }; const data: TestData[] = [ { name: 'shows success when it is deployed', props: { app: 'test1', version: 2 }, + appDetails: { + test1: { + application: { + name: '', + releases: [ + { + version: 2, + sourceMessage: 'test-rel', + undeployVersion: false, + sourceCommitId: 'commit123', + sourceAuthor: 'author', + prNumber: '666', + createdAt: new Date(2023, 6, 6), + displayVersion: '2', + isMinor: false, + isPrepublish: false, + }, + ], + team: 'test-team', + sourceRepoUrl: '', + undeploySummary: UndeploySummary.NORMAL, + warnings: [], + }, + deployments: { + development: { + version: 2, + queuedVersion: 0, + undeployVersion: false, + }, + development2: { + version: 2, + queuedVersion: 0, + undeployVersion: false, + }, + staging: { + version: 2, + queuedVersion: 0, + undeployVersion: false, + }, + }, + appLocks: {}, + teamLocks: {}, + }, + }, rels: [ { version: 2, @@ -426,6 +666,8 @@ describe('Release Card Rollout Status', () => { }, environmentGroups: testcase.environmentGroups, }); + updateAppDetails.set(testcase.appDetails); + testcase.rolloutStatus.forEach(UpdateRolloutStatus); const { container } = getWrapper(testcase.props); // then diff --git a/services/frontend-service/src/ui/components/ReleaseCardMini/ReleaseCardMini.test.tsx b/services/frontend-service/src/ui/components/ReleaseCardMini/ReleaseCardMini.test.tsx index 22499c025..da3d48107 100644 --- a/services/frontend-service/src/ui/components/ReleaseCardMini/ReleaseCardMini.test.tsx +++ b/services/frontend-service/src/ui/components/ReleaseCardMini/ReleaseCardMini.test.tsx @@ -15,7 +15,7 @@ along with kuberpult. If not, see Copyright freiheit.com*/ import { ReleaseCardMini, ReleaseCardMiniProps } from './ReleaseCardMini'; import { render } from '@testing-library/react'; -import { UpdateOverview } from '../../utils/store'; +import { updateAppDetails, UpdateOverview } from '../../utils/store'; import { MemoryRouter } from 'react-router-dom'; import { Environment, Priority, Release, UndeploySummary } from '../../../api/api'; import { Spy } from 'spy4js'; @@ -146,6 +146,28 @@ describe('Release Card Mini', () => { }, ], }); + + updateAppDetails.set({ + test2: { + application: { + name: 'test2', + releases: testcase.rels, + sourceRepoUrl: 'http://test2.com', + team: 'example', + undeploySummary: UndeploySummary.NORMAL, + warnings: [], + }, + deployments: { + test2: { + version: 2, + queuedVersion: 0, + undeployVersion: false, + }, + }, + appLocks: {}, + teamLocks: {}, + }, + }); const { container } = getWrapper(testcase.props); expect(container.querySelector('.release__details-mini')?.textContent).toContain( testcase.rels[0].sourceAuthor diff --git a/services/frontend-service/src/ui/components/ReleaseDialog/ReleaseDialog.test.tsx b/services/frontend-service/src/ui/components/ReleaseDialog/ReleaseDialog.test.tsx index 8c8ec2882..eb6ddef9b 100644 --- a/services/frontend-service/src/ui/components/ReleaseDialog/ReleaseDialog.test.tsx +++ b/services/frontend-service/src/ui/components/ReleaseDialog/ReleaseDialog.test.tsx @@ -15,8 +15,16 @@ along with kuberpult. If not, see Copyright freiheit.com*/ import { EnvironmentListItem, ReleaseDialog, ReleaseDialogProps } from './ReleaseDialog'; import { fireEvent, render } from '@testing-library/react'; -import { UpdateAction, UpdateOverview, UpdateRolloutStatus } from '../../utils/store'; -import { Environment, EnvironmentGroup, Priority, Release, RolloutStatus, UndeploySummary } from '../../../api/api'; +import { UpdateAction, updateAppDetails, UpdateOverview, UpdateRolloutStatus } from '../../utils/store'; +import { + Environment, + EnvironmentGroup, + GetAppDetailsResponse, + Priority, + Release, + RolloutStatus, + UndeploySummary, +} from '../../../api/api'; import { Spy } from 'spy4js'; import { MemoryRouter } from 'react-router-dom'; @@ -33,6 +41,7 @@ describe('Release Dialog', () => { interface dataT { name: string; props: ReleaseDialogProps; + appDetails: { [p: string]: GetAppDetailsResponse }; rels: Release[]; envs: Environment[]; envGroups: EnvironmentGroup[]; @@ -52,6 +61,7 @@ describe('Release Dialog', () => { props: ReleaseDialogProps; rels: Release[]; envs: Environment[]; + appDetails: { [p: string]: GetAppDetailsResponse }; envGroups: EnvironmentGroup[]; expect_message: boolean; expect_queues: number; @@ -65,6 +75,7 @@ describe('Release Dialog', () => { app: 'test1', version: 2, }, + appDetails: {}, rels: [ { version: 2, @@ -118,6 +129,7 @@ describe('Release Dialog', () => { app: 'test1', version: 2, }, + appDetails: {}, rels: [ { version: 2, @@ -173,6 +185,44 @@ describe('Release Dialog', () => { app: 'test1', version: 2, }, + appDetails: { + test1: { + application: { + name: 'test1', + releases: [ + { + version: 2, + sourceMessage: 'test1', + sourceAuthor: 'test', + sourceCommitId: 'commit', + createdAt: new Date(2002), + undeployVersion: false, + prNumber: '#1337', + displayVersion: '2', + isMinor: false, + isPrepublish: false, + }, + ], + sourceRepoUrl: 'http://test2.com', + team: 'example', + undeploySummary: UndeploySummary.NORMAL, + warnings: [], + }, + appLocks: { + production: { + locks: [{ message: 'appLock', lockId: 'ui-applock' }], + }, + }, + teamLocks: {}, + deployments: { + dev: { + version: 1, + queuedVersion: 0, + undeployVersion: false, + }, + }, + }, + }, rels: [ { version: 2, @@ -226,6 +276,44 @@ describe('Release Dialog', () => { app: 'test1', version: 2, }, + appDetails: { + test1: { + application: { + name: 'test1', + releases: [ + { + version: 2, + sourceMessage: 'test1', + sourceAuthor: 'test', + sourceCommitId: 'commit', + createdAt: new Date(2002), + undeployVersion: false, + prNumber: '#1337', + displayVersion: '2', + isMinor: false, + isPrepublish: false, + }, + ], + sourceRepoUrl: 'http://test2.com', + team: 'example', + undeploySummary: UndeploySummary.NORMAL, + warnings: [], + }, + appLocks: { + production: { + locks: [{ message: 'appLock', lockId: 'ui-applock' }], + }, + }, + teamLocks: {}, + deployments: { + dev: { + version: 1, + queuedVersion: 0, + undeployVersion: false, + }, + }, + }, + }, rels: [ { version: 2, @@ -280,6 +368,68 @@ describe('Release Dialog', () => { app: 'test1', version: 2, }, + appDetails: { + test1: { + application: { + name: 'test1', + releases: [ + { + sourceCommitId: 'cafe', + sourceMessage: 'the other commit message 2', + version: 2, + createdAt: new Date(2002), + undeployVersion: false, + prNumber: 'PR123', + sourceAuthor: 'nobody', + displayVersion: '2', + isMinor: false, + isPrepublish: false, + }, + { + sourceCommitId: 'cafe', + sourceMessage: 'the other commit message 3', + version: 3, + createdAt: new Date(2002), + undeployVersion: false, + prNumber: 'PR123', + sourceAuthor: 'nobody', + displayVersion: '3', + isMinor: false, + isPrepublish: false, + }, + ], + sourceRepoUrl: 'http://test2.com', + team: 'example', + undeploySummary: UndeploySummary.NORMAL, + warnings: [], + }, + appLocks: { + production: { + locks: [{ message: 'appLock', lockId: 'ui-applock' }], + }, + dev: { + locks: [{ message: 'appLock', lockId: 'ui-applock' }], + }, + }, + teamLocks: { + dev: { + locks: [{ message: 'teamLock', lockId: 'ui-teamlock' }], + }, + }, + deployments: { + prod: { + version: 2, + queuedVersion: 0, + undeployVersion: false, + }, + dev: { + version: 3, + queuedVersion: 666, + undeployVersion: false, + }, + }, + }, + }, envs: [ { name: 'prod', @@ -328,25 +478,25 @@ describe('Release Dialog', () => { rels: [ { sourceCommitId: 'cafe', - sourceMessage: 'the other commit message 2', - version: 2, + sourceMessage: 'the other commit message 3', + version: 3, createdAt: new Date(2002), undeployVersion: false, prNumber: 'PR123', sourceAuthor: 'nobody', - displayVersion: '2', + displayVersion: '3', isMinor: false, isPrepublish: false, }, { sourceCommitId: 'cafe', - sourceMessage: 'the other commit message 3', - version: 3, + sourceMessage: 'the other commit message 2', + version: 2, createdAt: new Date(2002), undeployVersion: false, prNumber: 'PR123', sourceAuthor: 'nobody', - displayVersion: '3', + displayVersion: '2', isMinor: false, isPrepublish: false, }, @@ -376,6 +526,34 @@ describe('Release Dialog', () => { app: 'test1', version: 4, }, + appDetails: { + test1: { + application: { + name: 'test1', + releases: [ + { + version: 4, + sourceAuthor: 'test1', + sourceMessage: '', + sourceCommitId: '', + prNumber: '', + createdAt: new Date(2002), + undeployVersion: true, + displayVersion: '4', + isMinor: false, + isPrepublish: false, + }, + ], + sourceRepoUrl: 'http://test2.com', + team: 'example', + undeploySummary: UndeploySummary.NORMAL, + warnings: [], + }, + appLocks: {}, + teamLocks: {}, + deployments: {}, + }, + }, rels: [ { version: 4, @@ -424,6 +602,7 @@ describe('Release Dialog', () => { }, ], }); + updateAppDetails.set(testcase.appDetails); const status = testcase.rolloutStatus; if (status !== undefined) { for (const app of status) { diff --git a/services/frontend-service/src/ui/components/ReleaseDialog/ReleaseDialog.tsx b/services/frontend-service/src/ui/components/ReleaseDialog/ReleaseDialog.tsx index 772ef07d1..2027b786e 100644 --- a/services/frontend-service/src/ui/components/ReleaseDialog/ReleaseDialog.tsx +++ b/services/frontend-service/src/ui/components/ReleaseDialog/ReleaseDialog.tsx @@ -19,12 +19,12 @@ import { Environment, Environment_Application, EnvironmentGroup, Lock, LockBehav import { addAction, getPriorityClassName, + useAppDetailsForApp, useCloseReleaseDialog, useCurrentlyDeployedAtGroup, useEnvironmentGroups, useReleaseDifference, useReleaseOptional, - useReleaseOrLog, useRolloutStatus, useTeamFromApplication, } from '../../utils/store'; @@ -371,10 +371,14 @@ export const undeployTooltipExplanation = export const ReleaseDialog: React.FC = (props) => { const { app, className, version } = props; - // the ReleaseDialog is only opened when there is a release, so we can assume that it exists here: - const release = useReleaseOrLog(app, version); + const appDetails = useAppDetailsForApp(app); const team = useTeamFromApplication(app) || ''; const closeReleaseDialog = useCloseReleaseDialog(); + if (!appDetails) { + return null; + } + const release = appDetails.application?.releases.find((r) => r.version === version); + if (!release) { return null; } diff --git a/services/frontend-service/src/ui/components/ReleaseDialog/__snapshots__/ReleaseDialog.test.tsx.snap b/services/frontend-service/src/ui/components/ReleaseDialog/__snapshots__/ReleaseDialog.test.tsx.snap index 3d42c8033..678ad7703 100644 --- a/services/frontend-service/src/ui/components/ReleaseDialog/__snapshots__/ReleaseDialog.test.tsx.snap +++ b/services/frontend-service/src/ui/components/ReleaseDialog/__snapshots__/ReleaseDialog.test.tsx.snap @@ -252,13 +252,11 @@ exports[`Release Dialog Renders the environment locks normal release 1`] = ` title="Shows the version that is currently deployed on prod. " > - - 2 - + " test1 + " has no version deployed on " + prod + "
-
- same version -
+
- - 2 - + " test1 + " has no version deployed on " + prod + "
-
- same version -
+
test1 - - test me team -
); -const deriveUndeployMessage = (undeploySummary: UndeploySummary): string | undefined => { +const deriveUndeployMessage = (undeploySummary: UndeploySummary | undefined): string | undefined => { switch (undeploySummary) { case UndeploySummary.UNDEPLOY: return 'Delete Forever'; @@ -104,15 +104,59 @@ const deriveUndeployMessage = (undeploySummary: UndeploySummary): string | undef } }; -export const ServiceLane: React.FC<{ application: Application; hideMinors: boolean }> = (props) => { +export const ServiceLane: React.FC<{ application: OverviewApplication; hideMinors: boolean }> = (props) => { + const { application, hideMinors } = props; + const { authHeader } = useAzureAuthSub((auth) => auth); + + const appDetails = useAppDetailsForApp(application.name); + React.useEffect(() => { + getAppDetails(application.name, authHeader); + }, [application, authHeader]); + + if (!appDetails) { + return ( +
+
+
+
+ {application.team ? application.team : ' '} + {' | '} {application.name} +
+ +
+
+
+
+ ); + } + + return ( + + ); +}; + +export const ReadyServiceLane: React.FC<{ + application: OverviewApplication; + hideMinors: boolean; + appDetails: GetAppDetailsResponse; +}> = (props) => { const { application, hideMinors } = props; - const deployedReleases = useDeployedReleases(application.name); - const allReleases = useVersionsForApp(application.name); const { navCallback } = useNavigateWithSearchParams('releasehistory/' + application.name); - const prepareUndeployOrUndeployText = deriveUndeployMessage(application.undeploySummary); + + const allReleases = [...new Set(props.appDetails?.application?.releases.map((d) => d.version))]; + const deployments = props.appDetails?.deployments; + const allDeployedReleaseNumbers = []; + for (const prop in deployments) { + allDeployedReleaseNumbers.push(deployments[prop].version); + } + const deployedReleases = [...new Set(allDeployedReleaseNumbers.map((v) => v).sort((n1, n2) => n2 - n1))]; const prepareUndeployOrUndeploy = React.useCallback(() => { - switch (application.undeploySummary) { + switch (props.appDetails.application?.undeploySummary) { case UndeploySummary.UNDEPLOY: addAction({ action: { @@ -136,10 +180,17 @@ export const ServiceLane: React.FC<{ application: Application; hideMinors: boole showSnackbarError('Internal Error: Cannot prepare to undeploy or actual undeploy in unknown state.'); break; } - }, [application.name, application.undeploySummary]); - const minorReleases = useMinorsForApp(application.name); - const releases = getReleasesToDisplay(deployedReleases, allReleases, minorReleases, hideMinors); - + }, [application.name, props.appDetails.application?.undeploySummary]); + let minorReleases = useMinorsForApp(application.name); + if (!minorReleases) { + minorReleases = []; + } + const prepareUndeployOrUndeployText = deriveUndeployMessage(props.appDetails.application?.undeploySummary); + const releases = [ + ...new Set( + getReleasesToDisplay(deployedReleases, allReleases, minorReleases, hideMinors).sort((n1, n2) => n2 - n1) + ), + ]; const releases_lane = !!releases && releases.map((rel, index) => { @@ -216,8 +267,8 @@ export const ServiceLane: React.FC<{ application: Application; hideMinors: boole } const dotsMenu = ; - const appLocks = useFilteredApplicationLocks(application.name); - const teamLocks = useTeamLocksFilterByTeam(application.team); + const appLocks = Object.values(useAppDetailsForApp(application.name).appLocks); + const teamLocks = Object.values(useAppDetailsForApp(application.name).teamLocks); const dialog = ( e.name)} @@ -246,7 +297,7 @@ export const ServiceLane: React.FC<{ application: Application; hideMinors: boole
{dotsMenu}
- +
{releases_lane}
diff --git a/services/frontend-service/src/ui/components/ServiceLane/Warnings.tsx b/services/frontend-service/src/ui/components/ServiceLane/Warnings.tsx index b2964a76f..b50b2f857 100644 --- a/services/frontend-service/src/ui/components/ServiceLane/Warnings.tsx +++ b/services/frontend-service/src/ui/components/ServiceLane/Warnings.tsx @@ -15,10 +15,16 @@ along with kuberpult. If not, see Copyright freiheit.com*/ import * as React from 'react'; import { Application, UnusualDeploymentOrder, UpstreamNotDeployed, Warning } from '../../../api/api'; +import { updateWarnings } from '../../utils/store'; -export const WarningBoxes: React.FC<{ application: Application }> = (props) => { +export const WarningBoxes: React.FC<{ application: Application | undefined }> = (props) => { const { application } = props; - + if (application === undefined) { + return
; + } + const details = updateWarnings.get(); + details[application.name] = application.warnings; + updateWarnings.set(details); return (
{application.warnings.map((warning: Warning, index: number) => ( diff --git a/services/frontend-service/src/ui/components/Spinner/Spinner.scss b/services/frontend-service/src/ui/components/Spinner/Spinner.scss index 19c652c43..40cd14197 100644 --- a/services/frontend-service/src/ui/components/Spinner/Spinner.scss +++ b/services/frontend-service/src/ui/components/Spinner/Spinner.scss @@ -36,3 +36,12 @@ Copyright freiheit.com*/ background-color: transparent; color: black; } + +.spinner-small { + .spinner-animation { + } + margin-left: 10px; + border-radius: 10px; + background-color: transparent; + color: black; +} diff --git a/services/frontend-service/src/ui/components/Spinner/Spinner.tsx b/services/frontend-service/src/ui/components/Spinner/Spinner.tsx index 1f6dc31c2..08126ff37 100644 --- a/services/frontend-service/src/ui/components/Spinner/Spinner.tsx +++ b/services/frontend-service/src/ui/components/Spinner/Spinner.tsx @@ -28,3 +28,17 @@ export const Spinner: React.FC<{ message: string }> = (props): JSX.Element => {
); }; + +export const SmallSpinner: React.FC<{ appName: string }> = (props): JSX.Element => ( +
+
+ +
+
+); diff --git a/services/frontend-service/src/ui/components/TopAppBar/TopAppBar.tsx b/services/frontend-service/src/ui/components/TopAppBar/TopAppBar.tsx index bad8f9a81..c99bd1ed9 100644 --- a/services/frontend-service/src/ui/components/TopAppBar/TopAppBar.tsx +++ b/services/frontend-service/src/ui/components/TopAppBar/TopAppBar.tsx @@ -20,7 +20,13 @@ import { SideBar } from '../SideBar/SideBar'; import { useSearchParams } from 'react-router-dom'; import { TeamsFilterDropdown, FiltersDropdown } from '../dropdown/dropdown'; import classNames from 'classnames'; -import { useAllWarnings, useKuberpultVersion, useShownWarnings } from '../../utils/store'; +import { + applicationsWithWarnings, + useAllWarnings, + useAllWarningsAllApps, + useApplicationsFilteredAndSorted, + useKuberpultVersion, +} from '../../utils/store'; import { Warning } from '../../../api/api'; import { hideMinors, @@ -38,7 +44,7 @@ export type TopAppBarProps = { export const TopAppBar: React.FC = (props) => { const [params, setParams] = useSearchParams(); - + useAllWarningsAllApps(); const appNameParam = params.get('application') || ''; const teamsParam = (params.get('teams') || '').split(',').filter((val) => val !== ''); @@ -51,8 +57,13 @@ export const TopAppBar: React.FC = (props) => { const loggedInUser = decodedToken?.email || 'Guest'; const hideWithoutWarningsValue = hideWithoutWarnings(params); + const allWarnings: Warning[] = useAllWarnings(); - const shownWarnings: Warning[] = useShownWarnings(teamsParam, appNameParam); + + const shownApps = useApplicationsFilteredAndSorted(teamsParam, true, appNameParam); + + const ShownAppsWithWarnings = applicationsWithWarnings(shownApps); + const hideMinorsValue = hideMinors(params); const onWarningsFilterClick = useCallback((): void => { @@ -70,7 +81,7 @@ export const TopAppBar: React.FC = (props) => { '' ) : (
- {shownWarnings.length} warnings shown ({allWarnings.length} total). + {ShownAppsWithWarnings.length} warnings shown ({allWarnings.length} total).
); diff --git a/services/frontend-service/src/ui/utils/store.test.tsx b/services/frontend-service/src/ui/utils/store.test.tsx index 336cc78eb..56cc86314 100644 --- a/services/frontend-service/src/ui/utils/store.test.tsx +++ b/services/frontend-service/src/ui/utils/store.test.tsx @@ -23,6 +23,7 @@ import { SnackbarStatus, UpdateAction, updateActions, + updateAppDetails, UpdateOverview, UpdateRolloutStatus, UpdateSnackbar, @@ -36,6 +37,7 @@ import { BatchAction, Environment, EnvironmentGroup, + GetAppDetailsResponse, GetOverviewResponse, LockBehavior, Priority, @@ -1086,61 +1088,20 @@ describe('Test Calculate Release Difference', () => { type TestDataStore = { name: string; inputOverview: GetOverviewResponse; + inputAppDetails: { [p: string]: GetAppDetailsResponse }; inputVersion: number; expectedDifference: number; }; - const appName = 'testApp'; + const appName = 'differentApp'; const envName = 'testEnv'; const testdata: TestDataStore[] = [ { - name: 'Simple diff calculation', + name: 'app does not exist in the app Details', + inputAppDetails: {}, inputOverview: { - applications: { - [appName]: { - name: appName, - releases: [ - { - version: 10, - sourceCommitId: '', - sourceAuthor: '', - sourceMessage: '', - undeployVersion: false, - prNumber: '', - displayVersion: '', - isMinor: false, - isPrepublish: false, - }, - { - version: 12, - sourceCommitId: '', - sourceAuthor: '', - sourceMessage: '', - undeployVersion: false, - prNumber: '', - displayVersion: '', - isMinor: false, - isPrepublish: false, - }, - { - version: 15, - sourceCommitId: '', - sourceAuthor: '', - sourceMessage: '', - undeployVersion: false, - prNumber: '', - displayVersion: '', - isMinor: false, - isPrepublish: false, - }, - ], - undeploySummary: UndeploySummary.NORMAL, - sourceRepoUrl: '', - team: '', - warnings: [], - }, - }, + applications: {}, environmentGroups: [ { environmentGroupName: 'test', @@ -1148,17 +1109,7 @@ describe('Test Calculate Release Difference', () => { { name: envName, locks: {}, - applications: { - [appName]: { - name: appName, - version: 10, - locks: {}, - queuedVersion: 0, - undeployVersion: false, - teamLocks: {}, - team: '', - }, - }, + applications: {}, distanceToUpstream: 0, priority: Priority.PROD, }, @@ -1170,16 +1121,31 @@ describe('Test Calculate Release Difference', () => { gitRevision: '', branch: '', manifestRepoUrl: '', + lightweightApps: [ + { + name: 'test', + team: 'test', + }, + { + name: 'example-app', + team: '', + }, + ], }, - inputVersion: 15, - expectedDifference: 2, + inputVersion: 10, + expectedDifference: 0, }, + { - name: 'negative diff', - inputOverview: { - applications: { - [appName]: { - name: appName, + name: 'environment does not exist in the envs', + inputAppDetails: { + 'example-app': { + application: { + name: 'example-app', + undeploySummary: UndeploySummary.NORMAL, + sourceRepoUrl: '', + team: '', + warnings: [], releases: [ { version: 10, @@ -1204,21 +1170,29 @@ describe('Test Calculate Release Difference', () => { isPrepublish: false, }, ], - undeploySummary: UndeploySummary.NORMAL, - sourceRepoUrl: '', - team: '', - warnings: [], }, + deployments: { + test: { + version: 12, + queuedVersion: 0, + undeployVersion: false, + }, + }, + appLocks: {}, + teamLocks: {}, }, + }, + inputOverview: { + applications: {}, environmentGroups: [ { environmentGroupName: 'test', environments: [ { - name: envName, + name: 'exampleEnv', locks: {}, applications: { - [appName]: { + exampleApp: { name: appName, version: 12, locks: {}, @@ -1238,17 +1212,27 @@ describe('Test Calculate Release Difference', () => { ], gitRevision: '', branch: '', + lightweightApps: [ + { + name: 'test', + team: 'test', + }, + ], manifestRepoUrl: '', }, inputVersion: 10, - expectedDifference: -1, + expectedDifference: 0, }, { - name: 'the input version does not exist', - inputOverview: { - applications: { - [appName]: { + name: 'Simple diff calculation', + inputAppDetails: { + [appName]: { + application: { name: appName, + undeploySummary: UndeploySummary.NORMAL, + sourceRepoUrl: '', + team: '', + warnings: [], releases: [ { version: 10, @@ -1272,66 +1256,8 @@ describe('Test Calculate Release Difference', () => { isMinor: false, isPrepublish: false, }, - ], - undeploySummary: UndeploySummary.NORMAL, - sourceRepoUrl: '', - team: '', - warnings: [], - }, - }, - environmentGroups: [ - { - environmentGroupName: 'test', - environments: [ { - name: envName, - locks: {}, - applications: { - [appName]: { - name: appName, - version: 12, - locks: {}, - queuedVersion: 0, - undeployVersion: false, - teamLocks: {}, - team: '', - }, - }, - distanceToUpstream: 0, - priority: Priority.PROD, - }, - ], - distanceToUpstream: 0, - priority: Priority.PROD, - }, - ], - gitRevision: '', - branch: '', - manifestRepoUrl: '', - }, - inputVersion: 11, - expectedDifference: 0, - }, - { - name: 'app does not exist in the applications', - inputOverview: { - applications: { - exampleApp: { - name: appName, - releases: [ - { - version: 10, - sourceCommitId: '', - sourceAuthor: '', - sourceMessage: '', - undeployVersion: false, - prNumber: '', - displayVersion: '', - isMinor: false, - isPrepublish: false, - }, - { - version: 12, + version: 15, sourceCommitId: '', sourceAuthor: '', sourceMessage: '', @@ -1342,12 +1268,20 @@ describe('Test Calculate Release Difference', () => { isPrepublish: false, }, ], - undeploySummary: UndeploySummary.NORMAL, - sourceRepoUrl: '', - team: '', - warnings: [], }, + deployments: { + [envName]: { + version: 10, + queuedVersion: 0, + undeployVersion: false, + }, + }, + appLocks: {}, + teamLocks: {}, }, + }, + inputOverview: { + applications: {}, environmentGroups: [ { environmentGroupName: 'test', @@ -1358,7 +1292,7 @@ describe('Test Calculate Release Difference', () => { applications: { [appName]: { name: appName, - version: 12, + version: 10, locks: {}, queuedVersion: 0, undeployVersion: false, @@ -1376,17 +1310,29 @@ describe('Test Calculate Release Difference', () => { ], gitRevision: '', branch: '', + + lightweightApps: [ + { + name: 'test', + team: 'test', + }, + ], manifestRepoUrl: '', }, - inputVersion: 10, - expectedDifference: 0, + + inputVersion: 15, + expectedDifference: 2, }, { - name: 'app does not exist in the environment applications', - inputOverview: { - applications: { - [appName]: { + name: 'negative diff', + inputAppDetails: { + [appName]: { + application: { name: appName, + undeploySummary: UndeploySummary.NORMAL, + sourceRepoUrl: '', + team: '', + warnings: [], releases: [ { version: 10, @@ -1411,12 +1357,20 @@ describe('Test Calculate Release Difference', () => { isPrepublish: false, }, ], - undeploySummary: UndeploySummary.NORMAL, - sourceRepoUrl: '', - team: '', - warnings: [], }, + deployments: { + [envName]: { + version: 12, + queuedVersion: 0, + undeployVersion: false, + }, + }, + appLocks: {}, + teamLocks: {}, }, + }, + inputOverview: { + applications: {}, environmentGroups: [ { environmentGroupName: 'test', @@ -1425,7 +1379,7 @@ describe('Test Calculate Release Difference', () => { name: envName, locks: {}, applications: { - exampleApp: { + [appName]: { name: appName, version: 12, locks: {}, @@ -1445,17 +1399,27 @@ describe('Test Calculate Release Difference', () => { ], gitRevision: '', branch: '', + lightweightApps: [ + { + name: 'test', + team: 'test', + }, + ], manifestRepoUrl: '', }, inputVersion: 10, - expectedDifference: 0, + expectedDifference: -1, }, { - name: 'environment does not exist in the envs', - inputOverview: { - applications: { - [appName]: { + name: 'the input version does not exist', + inputAppDetails: { + appName: { + application: { name: appName, + undeploySummary: UndeploySummary.NORMAL, + sourceRepoUrl: '', + team: '', + warnings: [], releases: [ { version: 10, @@ -1480,23 +1444,31 @@ describe('Test Calculate Release Difference', () => { isPrepublish: false, }, ], - undeploySummary: UndeploySummary.NORMAL, - sourceRepoUrl: '', - team: '', - warnings: [], }, + deployments: { + [envName]: { + version: 12, + queuedVersion: 0, + undeployVersion: false, + }, + }, + appLocks: {}, + teamLocks: {}, }, + }, + inputOverview: { + applications: {}, environmentGroups: [ { environmentGroupName: 'test', environments: [ { - name: 'exampleEnv', + name: envName, locks: {}, applications: { - exampleApp: { + [appName]: { name: appName, - version: 12, + version: 11, locks: {}, queuedVersion: 0, undeployVersion: false, @@ -1515,16 +1487,24 @@ describe('Test Calculate Release Difference', () => { gitRevision: '', branch: '', manifestRepoUrl: '', + lightweightApps: [ + { + name: 'test', + team: 'test', + }, + ], }, - inputVersion: 10, + inputVersion: 11, expectedDifference: 0, }, ]; describe.each(testdata)('with', (testcase) => { + updateAppDetails.set({}); it(testcase.name, () => { updateActions([]); + updateAppDetails.set({}); UpdateOverview.set(testcase.inputOverview); - + updateAppDetails.set(testcase.inputAppDetails); const calculatedDiff = renderHook(() => useReleaseDifference(testcase.inputVersion, appName, envName)) .result.current; expect(calculatedDiff).toStrictEqual(testcase.expectedDifference); diff --git a/services/frontend-service/src/ui/utils/store.tsx b/services/frontend-service/src/ui/utils/store.tsx index 73f9f7cc0..b84b02075 100644 --- a/services/frontend-service/src/ui/utils/store.tsx +++ b/services/frontend-service/src/ui/utils/store.tsx @@ -15,7 +15,6 @@ along with kuberpult. If not, see Copyright freiheit.com*/ import { createStore } from 'react-use-sub'; import { - Application, BatchAction, BatchRequest, Environment, @@ -33,6 +32,8 @@ import { GetReleaseTrainPrognosisResponse, GetFailedEslsResponse, Environment_Application, + GetAppDetailsResponse, + OverviewApplication, } from '../../api/api'; import * as React from 'react'; import { useCallback, useMemo } from 'react'; @@ -64,6 +65,7 @@ type EnhancedOverview = GetOverviewResponse & { [key: string]: unknown; loaded: const emptyOverview: EnhancedOverview = { applications: {}, + lightweightApps: [], environmentGroups: [], gitRevision: '', loaded: false, @@ -74,6 +76,7 @@ const [useOverview, UpdateOverview_] = createStore(emptyOverview); export const UpdateOverview = UpdateOverview_; // we do not want to export "useOverview". The store.tsx should act like a facade to the data. export const useOverviewLoaded = (): boolean => useOverview(({ loaded }) => loaded); + type TagsResponse = { response: GetGitTagsResponse; tagsReady: boolean; @@ -90,6 +93,17 @@ export type CommitInfoResponse = { commitInfoReady: CommitInfoState; }; +export type AppDetailsResponse = { + response: GetAppDetailsResponse | undefined; + appDetailState: AppDetailsState; +}; +export enum AppDetailsState { + LOADING, + READY, + ERROR, + NOTFOUND, +} + export enum FailedEslsState { LOADING, READY, @@ -112,7 +126,6 @@ export type ReleaseTrainPrognosisResponse = { response: GetReleaseTrainPrognosisResponse | undefined; releaseTrainPrognosisReady: ReleaseTrainPrognosisState; }; - const emptyBatch: BatchRequest & { [key: string]: unknown } = { actions: [] }; export const [useAction, UpdateAction] = createStore(emptyBatch); const tagsResponse: GetGitTagsResponse = { tagData: [] }; @@ -129,6 +142,29 @@ export const refreshTags = (): void => { }; export const [useTag, updateTag] = createStore({ response: tagsResponse, tagsReady: false }); +const emtpyDetails: { [key: string]: GetAppDetailsResponse } = {}; +export const [useAppDetails, updateAppDetails] = createStore<{ [key: string]: GetAppDetailsResponse }>(emtpyDetails); + +const emptyWarnings: { [key: string]: Warning[] } = {}; +export const [useWarnings, updateWarnings] = createStore<{ [key: string]: Warning[] }>(emptyWarnings); + +export const useAllWarningsAllApps = (): Warning => useWarnings((map) => map); + +export const getAppDetails = (appName: string, authHeader: AuthHeader): void => { + useApi + .overviewService() + .GetAppDetails({ appName: appName }, authHeader) + .then((result: GetAppDetailsResponse) => { + const details = updateAppDetails.get(); + details[appName] = result; + updateAppDetails.set(details); + }) + .catch((e) => { + PanicOverview.set(e); + showSnackbarError(e.message); + }); +}; + export const getCommitInfo = (commitHash: string, pageNumber: number, authHeader: AuthHeader): void => { useApi .gitService() @@ -401,6 +437,8 @@ export const useOpenReleaseDialog = (app: string, version: number): (() => void) }, [app, params, setParams, version]); }; +export const useAppDetailsForApp = (app: string): GetAppDetailsResponse => useAppDetails((map) => map[app]); + export const useCloseReleaseDialog = (): (() => void) => { const [params, setParams] = useSearchParams(); return useCallback(() => { @@ -414,9 +452,12 @@ export const useReleaseDialogParams = (): { app: string | null; version: number const [params] = useSearchParams(); const app = params.get('dialog-app') ?? ''; const version = +(params.get('dialog-version') ?? ''); - const valid = useOverview(({ applications }) => - applications[app] ? !!applications[app].releases.find((r) => r.version === version) : false - ); + + const appDetails = useAppDetailsForApp(app); + if (!appDetails) { + return { app: null, version: null }; + } + const valid = !!appDetails.application?.releases.find((r) => r.version === version); return valid ? { app, version } : { app: null, version: null }; }; @@ -434,26 +475,37 @@ export const deleteAction = (action: BatchAction): void => { // doesn't return empty team names (i.e.: '') // doesn't return repeated team names export const useTeamNames = (): string[] => - useOverview(({ applications }) => [ + useOverview(({ lightweightApps }) => [ ...new Set( - Object.values(applications) - .map((app: Application) => app.team.trim() || '') + Object.values(lightweightApps) + .map((app: OverviewApplication) => app.team.trim() || '') .sort((a, b) => a.localeCompare(b)) ), ]); -export const useApplications = (): { [p: string]: Application } => useOverview(({ applications }) => applications); +export const useApplications = (): OverviewApplication[] => useOverview(({ lightweightApps }) => lightweightApps); export const useTeamFromApplication = (app: string): string | undefined => - useOverview(({ applications }) => applications[app]?.team?.trim()); + useOverview(({ lightweightApps }) => lightweightApps.find((data) => data.name === app)?.name); // returns warnings from all apps -export const useAllWarnings = (): Warning[] => - useOverview(({ applications }) => Object.values(applications).flatMap((app) => app.warnings)); - -// return warnings from all apps matching the given filtering criteria -export const useShownWarnings = (teams: string[], nameIncludes: string): Warning[] => { - const shownApps = useApplicationsFilteredAndSorted(teams, true, nameIncludes); - return shownApps.flatMap((app) => app.warnings); +export const useAllWarnings = (): Warning[] => { + const names = useOverview(({ lightweightApps }) => lightweightApps).map((curr) => curr.name); + const allAppDetails = updateAppDetails.get(); + return names + .map((name) => { + const resp = allAppDetails[name]; + if (resp === undefined) { + return []; + } else { + const app = resp.application; + if (app === undefined) { + return []; + } else { + return app.warnings; + } + } + }) + .flatMap((curr) => curr); }; export const useEnvironmentGroups = (): EnvironmentGroup[] => useOverview(({ environmentGroups }) => environmentGroups); @@ -502,19 +554,33 @@ export const getPriorityClassName = (envOrGroup: Environment | EnvironmentGroup) 'environment-priority-' + String(Priority[envOrGroup?.priority ?? Priority.UNRECOGNIZED]).toLowerCase(); // filter for apps included in the selected teams -const applicationsMatchingTeam = (applications: Application[], teams: string[]): Application[] => +const applicationsMatchingTeam = (applications: OverviewApplication[], teams: string[]): OverviewApplication[] => applications.filter((app) => teams.length === 0 || teams.includes(app.team.trim() || '')); -// filter for all application names that have warnings -const applicationsWithWarnings = (applications: Application[]): Application[] => - applications.filter((app) => app.warnings.length > 0); +//filter for all application names that have warnings +export const applicationsWithWarnings = (applications: OverviewApplication[]): OverviewApplication[] => + applications + .map((app) => { + const d = updateAppDetails.get()[app.name]; + if (d === undefined) { + return []; + } else { + const currApp = d.application; + if (currApp === undefined) { + return []; + } else { + return currApp.warnings.length > 0 ? [app] : []; + } + } + }) + .flatMap((curr) => curr); // filters given apps with the search terms or all for the empty string -const applicationsMatchingName = (applications: Application[], appNameParam: string): Application[] => +const applicationsMatchingName = (applications: OverviewApplication[], appNameParam: string): OverviewApplication[] => applications.filter((app) => appNameParam === '' || app.name.includes(appNameParam)); // sorts given apps by team -const applicationsSortedByTeam = (applications: Application[]): Application[] => +const applicationsSortedByTeam = (applications: OverviewApplication[]): OverviewApplication[] => applications.sort((a, b) => (a.team === b.team ? a.name?.localeCompare(b.name) : a.team?.localeCompare(b.team))); // returns applications to show on the home page @@ -522,8 +588,8 @@ export const useApplicationsFilteredAndSorted = ( teams: string[], withWarningsOnly: boolean, nameIncludes: string -): Application[] => { - const all = useOverview(({ applications }) => Object.values(applications)); +): OverviewApplication[] => { + const all = useOverview(({ lightweightApps }) => Object.values(lightweightApps)); const allMatchingTeam = applicationsMatchingTeam(all, teams); const allMatchingTeamAndWarnings = withWarningsOnly ? applicationsWithWarnings(allMatchingTeam) : allMatchingTeam; const allMatchingTeamAndWarningsAndName = applicationsMatchingName(allMatchingTeamAndWarnings, nameIncludes); @@ -639,7 +705,7 @@ export const useLocksConflictingWithActions = (): AllLocks => { if (action.action?.$case === 'deploy') { const app = action.action.deploy.application; const env = action.action.deploy.environment; - const appTeam = appMap[app].team; + const appTeam = appMap.find((curr) => curr.name === app)?.team; if (teamLock.environment === env && teamLock.team === appTeam) { // found a team lock that matches return true; @@ -923,8 +989,12 @@ export const sortLocks = (displayLocks: DisplayLock[], sorting: 'oldestToNewest' }; // returns the release number {$version} of {$application} -export const useRelease = (application: string, version: number): Release | undefined => - useOverview(({ applications }) => applications[application]?.releases?.find((r) => r.version === version)); +export const useRelease = (application: string, version: number): Release | undefined => { + const appDetails = useAppDetailsForApp(application); + + if (!appDetails) return undefined; + return appDetails.application?.releases.find((r) => r.version === version); +}; export const useReleaseOrLog = (application: string, version: number): Release | undefined => { const release = useRelease(application, version); @@ -937,15 +1007,10 @@ export const useReleaseOrLog = (application: string, version: number): Release | }; export const useReleaseOptional = (application: string, env: Environment): Release | undefined => { - const x = env.applications[application]; - return useOverview(({ applications }) => { - const version = x ? x.version : 0; - const res = applications[application].releases.find((r) => r.version === version); - if (!x) { - return undefined; - } - return res; - }); + const appDetails = useAppDetailsForApp(application); + const deployment = appDetails.deployments[env.name]; + if (!deployment) return undefined; + return appDetails.application?.releases.find((r) => r.version === deployment.version); }; // returns the release versions that are currently deployed to at least one environment @@ -1017,19 +1082,29 @@ export const useCurrentlyExistsAtGroup = (application: string): EnvironmentGroup }; // Get all releases for an app -export const useReleasesForApp = (app: string): Release[] => - useOverview(({ applications }) => applications[app]?.releases?.sort((a, b) => b.version - a.version)); - -// Get all release versions for an app -export const useVersionsForApp = (app: string): number[] => useReleasesForApp(app).map((rel) => rel.version); +export const useReleasesForApp = (app: string): Release[] => { + const appDetails = useAppDetailsForApp(app); + if (!appDetails?.application?.releases) { + return []; + } else { + return appDetails.application?.releases; + } +}; // Calculated release difference between a specific release and currently deployed release on a specific environment export const useReleaseDifference = (toDeployVersion: number, application: string, environment: string): number => { - const envApplications = useEnvironments().find((env) => env.name === environment)?.applications; - const currentDeployedIndex = useReleasesForApp(application)?.findIndex( - (rel) => rel.version === envApplications?.[application]?.version + const appDetails = useAppDetailsForApp(application); + if (!appDetails) { + return 0; + } + const deployment = appDetails.deployments[environment]; + if (!deployment) { + return 0; + } + const currentDeployedIndex = appDetails.application?.releases.findIndex( + (rel) => rel.version === deployment.version ); - const newVersionIndex = useReleasesForApp(application)?.findIndex((rel) => rel.version === toDeployVersion); + const newVersionIndex = appDetails.application?.releases?.findIndex((rel) => rel.version === toDeployVersion); if ( currentDeployedIndex === undefined || newVersionIndex === undefined || @@ -1038,12 +1113,13 @@ export const useReleaseDifference = (toDeployVersion: number, application: strin ) { return 0; } - return currentDeployedIndex - newVersionIndex; + + return newVersionIndex - currentDeployedIndex; }; // Get all minor releases for an app -export const useMinorsForApp = (app: string): number[] => - useReleasesForApp(app) - .filter((rel) => rel.isMinor) +export const useMinorsForApp = (app: string): number[] | undefined => + useAppDetailsForApp(app) + .application?.releases.filter((rel) => rel.isMinor) .map((rel) => rel.version); // Navigate while keeping search params, returns new navigation url, and a callback function to navigate