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 734723c04..752505632 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 " + @@ -5634,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/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/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 c308054ab..0651a83fb 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 } @@ -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 16b87918d..305b19c3e 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" @@ -1734,6 +1735,12 @@ func TestCreateEnvironmentUpdatesOverview(t *testing.T) { }, }, }, + LightweightApps: []*api.OverviewApplication{ + { + Name: "app", + Team: "", + }, + }, EnvironmentGroups: []*api.EnvironmentGroup{ &api.EnvironmentGroup{ EnvironmentGroupName: "development", @@ -2264,7 +2271,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 +3656,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/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 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) { 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)