diff --git a/engine/api/api_routes.go b/engine/api/api_routes.go index bca4723dcc..2e5a52b76f 100644 --- a/engine/api/api_routes.go +++ b/engine/api/api_routes.go @@ -189,6 +189,7 @@ func (api *API) InitRouter() { // Application r.Handle("/project/{permProjectKey}/application/{applicationName}", Scope(sdk.AuthConsumerScopeProject), r.GET(api.getApplicationHandler), r.PUT(api.updateApplicationHandler), r.DELETE(api.deleteApplicationHandler)) + r.Handle("/project/{permProjectKey}/application/{applicationName}/ascode", Scope(sdk.AuthConsumerScopeProject), r.PUT(api.updateAsCodeApplicationHandler)) r.Handle("/project/{permProjectKey}/application/{applicationName}/metrics/{metricName}", Scope(sdk.AuthConsumerScopeProject), r.GET(api.getApplicationMetricHandler)) r.Handle("/project/{permProjectKey}/application/{applicationName}/keys", Scope(sdk.AuthConsumerScopeProject), r.GET(api.getKeysInApplicationHandler), r.POST(api.addKeyInApplicationHandler)) r.Handle("/project/{permProjectKey}/application/{applicationName}/keys/{name}", Scope(sdk.AuthConsumerScopeProject), r.DELETE(api.deleteKeyInApplicationHandler)) diff --git a/engine/api/application.go b/engine/api/application.go index fd84669dac..ae811e49c4 100644 --- a/engine/api/application.go +++ b/engine/api/application.go @@ -8,22 +8,25 @@ import ( "net/http" "strings" - "github.com/ovh/cds/engine/api/permission" - "github.com/ovh/cds/engine/api/user" - "github.com/go-gorp/gorp" "github.com/gorilla/mux" "github.com/ovh/cds/engine/api/application" + "github.com/ovh/cds/engine/api/ascode" "github.com/ovh/cds/engine/api/cache" "github.com/ovh/cds/engine/api/environment" "github.com/ovh/cds/engine/api/event" "github.com/ovh/cds/engine/api/group" + "github.com/ovh/cds/engine/api/keys" + "github.com/ovh/cds/engine/api/operation" + "github.com/ovh/cds/engine/api/permission" "github.com/ovh/cds/engine/api/project" "github.com/ovh/cds/engine/api/repositoriesmanager" + "github.com/ovh/cds/engine/api/user" "github.com/ovh/cds/engine/api/workflow" "github.com/ovh/cds/engine/service" "github.com/ovh/cds/sdk" + "github.com/ovh/cds/sdk/exportentities" ) func (api *API) getApplicationsHandler() service.Handler { @@ -147,6 +150,18 @@ func (api *API) getApplicationHandler() service.Handler { app.Usage = &usage } + proj, err := project.Load(api.mustDB(), projectKey) + if err != nil { + return err + } + wkAscodeHolder, err := workflow.LoadByRepo(ctx, api.mustDB(), *proj, app.FromRepository, workflow.LoadOptions{ + WithTemplate: true, + }) + if err != nil && !sdk.ErrorIs(err, sdk.ErrNotFound) { + return sdk.NewErrorFrom(err, "cannot found workflow holder of the pipeline") + } + app.WorkflowAscodeHolder = wkAscodeHolder + return service.WriteJSON(w, app, http.StatusOK) } } @@ -390,6 +405,110 @@ func cloneApplication(ctx context.Context, db gorp.SqlExecutor, store cache.Stor return nil } +func (api *API) updateAsCodeApplicationHandler() service.Handler { + return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { + vars := mux.Vars(r) + key := vars[permProjectKey] + name := vars["applicationName"] + + branch := FormString(r, "branch") + message := FormString(r, "message") + + if branch == "" || message == "" { + return sdk.NewErrorFrom(sdk.ErrWrongRequest, "missing branch or message data") + } + + var a sdk.Application + if err := service.UnmarshalBody(r, &a); err != nil { + return err + } + + // check application name pattern + regexp := sdk.NamePatternRegex + if !regexp.MatchString(a.Name) { + return sdk.WrapError(sdk.ErrInvalidApplicationPattern, "Application name %s do not respect pattern", a.Name) + } + + proj, err := project.Load(api.mustDB(), key, project.LoadOptions.WithClearKeys) + if err != nil { + return err + } + + appDB, err := application.LoadByName(api.mustDB(), key, name) + if err != nil { + return sdk.WrapError(err, "cannot load application %s", name) + } + + if appDB.FromRepository == "" { + return sdk.NewErrorFrom(sdk.ErrForbidden, "current application is not ascode") + } + + wkHolder, err := workflow.LoadByRepo(ctx, api.mustDB(), *proj, appDB.FromRepository, workflow.LoadOptions{ + WithTemplate: true, + }) + if err != nil { + return err + } + if wkHolder.TemplateInstance != nil { + return sdk.NewErrorFrom(sdk.ErrForbidden, "cannot edit an application that was generated by a template") + } + + var rootApp *sdk.Application + if wkHolder.WorkflowData.Node.Context != nil && wkHolder.WorkflowData.Node.Context.ApplicationID != 0 { + rootApp, err = application.LoadByIDWithClearVCSStrategyPassword(api.mustDB(), wkHolder.WorkflowData.Node.Context.ApplicationID) + if err != nil { + return err + } + } + if rootApp == nil { + return sdk.NewErrorFrom(sdk.ErrWrongRequest, "cannot find the root application of the workflow %s that hold the pipeline", wkHolder.Name) + } + + // create keys + for i := range a.Keys { + k := &a.Keys[i] + newKey, err := keys.GenerateKey(k.Name, k.Type) + if err != nil { + return err + } + k.Public = newKey.Public + k.Private = newKey.Private + k.KeyID = newKey.KeyID + } + + u := getAPIConsumer(ctx) + a.ProjectID = proj.ID + app, err := application.ExportApplication(api.mustDB(), a, project.EncryptWithBuiltinKey, fmt.Sprintf("app:%d:%s", appDB.ID, branch)) + if err != nil { + return sdk.WrapError(err, "unable to export app %s", a.Name) + } + wp := exportentities.WorkflowComponents{ + Applications: []exportentities.Application{app}, + } + + ope, err := operation.PushOperationUpdate(ctx, api.mustDB(), api.Cache, *proj, wp, rootApp.VCSServer, rootApp.RepositoryFullname, branch, message, rootApp.RepositoryStrategy, u) + if err != nil { + return err + } + + sdk.GoRoutine(context.Background(), fmt.Sprintf("UpdateAsCodeApplicationHandler-%s", ope.UUID), func(ctx context.Context) { + ed := ascode.EntityData{ + FromRepo: appDB.FromRepository, + Type: ascode.ApplicationEvent, + ID: appDB.ID, + Name: appDB.Name, + OperationUUID: ope.UUID, + } + asCodeEvent := ascode.UpdateAsCodeResult(ctx, api.mustDB(), api.Cache, *proj, wkHolder.ID, *rootApp, ed, u) + if asCodeEvent != nil { + event.PublishAsCodeEvent(ctx, proj.Key, *asCodeEvent, u) + } + }, api.PanicDump()) + + return service.WriteJSON(w, ope, http.StatusOK) + } +} + func (api *API) updateApplicationHandler() service.Handler { return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { vars := mux.Vars(r) diff --git a/engine/api/application/application_exporter.go b/engine/api/application/application_exporter.go index 03377cbfde..9812350d70 100644 --- a/engine/api/application/application_exporter.go +++ b/engine/api/application/application_exporter.go @@ -5,13 +5,12 @@ import ( "github.com/go-gorp/gorp" - "github.com/ovh/cds/engine/api/cache" "github.com/ovh/cds/sdk" "github.com/ovh/cds/sdk/exportentities" ) // Export an application -func Export(db gorp.SqlExecutor, cache cache.Store, key string, appName string, encryptFunc sdk.EncryptFunc) (exportentities.Application, error) { +func Export(db gorp.SqlExecutor, key string, appName string, encryptFunc sdk.EncryptFunc) (exportentities.Application, error) { app, err := LoadByNameWithClearVCSStrategyPassword(db, key, appName, LoadOptions.WithVariablesWithClearPassword, LoadOptions.WithClearKeys, @@ -21,11 +20,11 @@ func Export(db gorp.SqlExecutor, cache cache.Store, key string, appName string, return exportentities.Application{}, sdk.WrapError(err, "cannot load application %s", appName) } - return ExportApplication(db, *app, encryptFunc) + return ExportApplication(db, *app, encryptFunc, fmt.Sprintf("appID:%d", app.ID)) } // ExportApplication encrypt and export -func ExportApplication(db gorp.SqlExecutor, app sdk.Application, encryptFunc sdk.EncryptFunc) (exportentities.Application, error) { +func ExportApplication(db gorp.SqlExecutor, app sdk.Application, encryptFunc sdk.EncryptFunc, encryptPrefix string) (exportentities.Application, error) { var appvars []sdk.ApplicationVariable for _, v := range app.Variables { switch v.Type { @@ -33,7 +32,7 @@ func ExportApplication(db gorp.SqlExecutor, app sdk.Application, encryptFunc sdk return exportentities.Application{}, sdk.NewErrorFrom(sdk.ErrUnknownError, "variable %s: variable of type key are deprecated. Please use the standard keys from your project or your application", v.Name) case sdk.SecretVariable: - content, err := encryptFunc(db, app.ProjectID, fmt.Sprintf("appID:%d:%s", app.ID, v.Name), v.Value) + content, err := encryptFunc(db, app.ProjectID, fmt.Sprintf("%s:%s", encryptPrefix, v.Name), v.Value) if err != nil { return exportentities.Application{}, sdk.WrapError(err, "unknown key type") } @@ -49,7 +48,7 @@ func ExportApplication(db gorp.SqlExecutor, app sdk.Application, encryptFunc sdk var keys []exportentities.EncryptedKey // Parse keys for _, k := range app.Keys { - content, err := encryptFunc(db, app.ProjectID, fmt.Sprintf("appID:%d:%s", app.ID, k.Name), k.Private) + content, err := encryptFunc(db, app.ProjectID, fmt.Sprintf("%s:%s", encryptPrefix, k.Name), k.Private) if err != nil { return exportentities.Application{}, sdk.WrapError(err, "unable to encrypt key") } @@ -62,7 +61,7 @@ func ExportApplication(db gorp.SqlExecutor, app sdk.Application, encryptFunc sdk } if app.RepositoryStrategy.Password != "" { - content, err := encryptFunc(db, app.ProjectID, fmt.Sprintf("appID:%d:%s", app.ID, "vcs:password"), app.RepositoryStrategy.Password) + content, err := encryptFunc(db, app.ProjectID, fmt.Sprintf("%s:%s", encryptPrefix, "vcs:password"), app.RepositoryStrategy.Password) if err != nil { return exportentities.Application{}, sdk.WrapError(err, "unable to encrypt password") } @@ -72,7 +71,7 @@ func ExportApplication(db gorp.SqlExecutor, app sdk.Application, encryptFunc sdk for pfName, pfConfig := range app.DeploymentStrategies { for k, v := range pfConfig { if v.Type == sdk.SecretVariable { - content, err := encryptFunc(db, app.ProjectID, fmt.Sprintf("appID:%d:%s:%s:%s", app.ID, pfName, k, "deployment:password"), v.Value) + content, err := encryptFunc(db, app.ProjectID, fmt.Sprintf("%s:%s:%s:%s", encryptPrefix, pfName, k, "deployment:password"), v.Value) if err != nil { return exportentities.Application{}, sdk.WrapError(err, "Unable to encrypt password") } diff --git a/engine/api/application_export.go b/engine/api/application_export.go index 6ba2533f90..c733c89b7e 100644 --- a/engine/api/application_export.go +++ b/engine/api/application_export.go @@ -27,7 +27,7 @@ func (api *API) getApplicationExportHandler() service.Handler { return err } - app, err := application.Export(api.mustDB(), api.Cache, key, appName, project.EncryptWithBuiltinKey) + app, err := application.Export(api.mustDB(), key, appName, project.EncryptWithBuiltinKey) if err != nil { return sdk.WithStack(err) } diff --git a/engine/api/application_key.go b/engine/api/application_key.go index cc9a612be1..1ad0ac6537 100644 --- a/engine/api/application_key.go +++ b/engine/api/application_key.go @@ -104,25 +104,13 @@ func (api *API) addKeyInApplicationHandler() service.Handler { newKey.Name = "app-" + newKey.Name } - switch newKey.Type { - case sdk.KeyTypeSSH: - k, errK := keys.GenerateSSHKey(newKey.Name) - if errK != nil { - return sdk.WrapError(errK, "addKeyInApplicationHandler> Cannot generate ssh key") - } - newKey.Public = k.Public - newKey.Private = k.Private - case sdk.KeyTypePGP: - k, errGenerate := keys.GeneratePGPKeyPair(newKey.Name) - if errGenerate != nil { - return sdk.WrapError(errGenerate, "addKeyInApplicationHandler> Cannot generate pgpKey") - } - newKey.Public = k.Public - newKey.Private = k.Private - newKey.KeyID = k.KeyID - default: - return sdk.WrapError(sdk.ErrUnknownKeyType, "addKeyInApplicationHandler> unknown key of type: %s", newKey.Type) + k, err := keys.GenerateKey(newKey.Name, newKey.Type) + if err != nil { + return err } + newKey.Public = k.Public + newKey.Private = k.Private + newKey.KeyID = k.KeyID tx, errT := api.mustDB().Begin() if errT != nil { diff --git a/engine/api/application_test.go b/engine/api/application_test.go index b49b8373cf..2d5a74350f 100644 --- a/engine/api/application_test.go +++ b/engine/api/application_test.go @@ -2,7 +2,18 @@ package api import ( "context" + "encoding/json" + "github.com/go-gorp/gorp" + "github.com/golang/mock/gomock" + "github.com/ovh/cds/engine/api/pipeline" + "github.com/ovh/cds/engine/api/repositoriesmanager" + "github.com/ovh/cds/engine/api/services" + "github.com/ovh/cds/engine/api/services/mock_services" + "github.com/ovh/cds/engine/api/workflow" + "net/http" + "net/http/httptest" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -60,3 +71,225 @@ func Test_postApplicationMetadataHandler_AsProvider(t *testing.T) { test.NoError(t, err) assert.Equal(t, 1, len(apps)) } + +func TestUpdateAsCodeApplicationHandler(t *testing.T) { + api, db, _ := newTestAPI(t) + + u, pass := assets.InsertAdminUser(t, db) + + UUID := sdk.UUID() + + svcs, errS := services.LoadAll(context.TODO(), db) + assert.NoError(t, errS) + for _, s := range svcs { + _ = services.Delete(db, &s) // nolint + } + + _, _ = assets.InsertService(t, db, t.Name()+"_HOOKS", services.TypeHooks) + _, _ = assets.InsertService(t, db, t.Name()+"_VCS", services.TypeVCS) + _, _ = assets.InsertService(t, db, t.Name()+"_REPO", services.TypeRepositories) + + // Setup a mock for all services called by the API + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + servicesClients := mock_services.NewMockClient(ctrl) + services.NewClient = func(_ gorp.SqlExecutor, _ []sdk.Service) services.Client { + return servicesClients + } + defer func() { + services.NewClient = services.NewDefaultClient + }() + + servicesClients.EXPECT(). + DoJSONRequest(gomock.Any(), "POST", "/task/bulk", gomock.Any(), gomock.Any()). + Return(nil, 201, nil) + + servicesClients.EXPECT(). + DoJSONRequest(gomock.Any(), "GET", "/vcs/github/webhooks", gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn( + func(ctx context.Context, method, path string, in interface{}, out interface{}, _ interface{}) (http.Header, int, error) { + *(out.(*repositoriesmanager.WebhooksInfos)) = repositoriesmanager.WebhooksInfos{ + WebhooksSupported: true, + WebhooksDisabled: false, + Icon: sdk.GitHubIcon, + Events: []string{ + "push", + }, + } + + return nil, 200, nil + }, + ) + servicesClients.EXPECT().DoJSONRequest(gomock.Any(), "GET", "/vcs/github/repos/foo/myrepo/branches", gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, method, path string, in interface{}, out interface{}, _ interface{}) (http.Header, int, error) { + bs := []sdk.VCSBranch{} + b := sdk.VCSBranch{ + DisplayID: "master", + LatestCommit: "aaaaaaa", + } + bs = append(bs, b) + out = bs + return nil, 200, nil + }).Times(1) + + servicesClients.EXPECT(). + DoJSONRequest(gomock.Any(), "POST", "/vcs/github/repos/foo/myrepo/hooks", gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn( + func(ctx context.Context, method, path string, in interface{}, out interface{}, _ interface{}) (http.Header, int, error) { + vcsHooks, _ := in.(*sdk.VCSHook) + vcsHooks.Events = []string{"push"} + vcsHooks.ID = sdk.UUID() + *(out.(*sdk.VCSHook)) = *vcsHooks + return nil, 200, nil + }, + ).Times(1) + + servicesClients.EXPECT(). + DoJSONRequest(gomock.Any(), "GET", "/vcs/github/repos/foo/myrepo", gomock.Any(), gomock.Any(), gomock.Any()).MinTimes(0) + + servicesClients.EXPECT(). + DoMultiPartRequest(gomock.Any(), "POST", "/operations", gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, method, path string, _ interface{}, in interface{}, out interface{}) (int, error) { + ope := new(sdk.Operation) + ope.UUID = UUID + *(out.(*sdk.Operation)) = *ope + return 200, nil + }).Times(1) + + servicesClients.EXPECT(). + DoJSONRequest(gomock.Any(), "GET", "/vcs/github/repos/foo/myrepo/pullrequests?state=open", gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, method, path string, in interface{}, out interface{}, _ interface{}) (http.Header, int, error) { + vcsPRs := []sdk.VCSPullRequest{} + *(out.(*[]sdk.VCSPullRequest)) = vcsPRs + return nil, 200, nil + }).Times(1) + + servicesClients.EXPECT(). + DoJSONRequest(gomock.Any(), "POST", "/vcs/github/repos/foo/myrepo/pullrequests", gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, method, path string, in interface{}, out interface{}, _ interface{}) (http.Header, int, error) { + vcsPR := sdk.VCSPullRequest{} + vcsPR.URL = "myURL" + *(out.(*sdk.VCSPullRequest)) = vcsPR + return nil, 200, nil + }).Times(1) + + servicesClients.EXPECT(). + DoJSONRequest(gomock.Any(), "GET", "/operations/"+UUID, gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn( + func(ctx context.Context, method, path string, in interface{}, out interface{}) (http.Header, int, error) { + ope := new(sdk.Operation) + ope.URL = "https://github.com/fsamin/go-repo.git" + ope.UUID = UUID + ope.Status = sdk.OperationStatusDone + ope.VCSServer = "github" + ope.RepoFullName = "fsamin/go-repo" + ope.RepositoryStrategy.Branch = "master" + ope.Setup.Checkout.Branch = "master" + ope.RepositoryInfo = new(sdk.OperationRepositoryInfo) + ope.RepositoryInfo.Name = "fsamin/go-repo" + ope.RepositoryInfo.DefaultBranch = "master" + ope.RepositoryInfo.FetchURL = "https://github.com/fsamin/go-repo.git" + ope.LoadFiles.Pattern = workflow.WorkflowAsCodePattern + + *(out.(*sdk.Operation)) = *ope + return nil, 200, nil + }, + ).Times(1) + + assert.NoError(t, workflow.CreateBuiltinWorkflowHookModels(db)) + + // Create Project + pkey := sdk.RandomString(10) + proj := assets.InsertTestProject(t, db, api.Cache, pkey, pkey) + vcsServer := sdk.ProjectVCSServerLink{ + ProjectID: proj.ID, + Name: "github", + } + vcsServer.Set("token", "foo") + vcsServer.Set("secret", "bar") + assert.NoError(t, repositoriesmanager.InsertProjectVCSServerLink(context.TODO(), db, &vcsServer)) + + pip := sdk.Pipeline{ + Name: sdk.RandomString(10), + ProjectID: proj.ID, + FromRepository: "myrepofrom", + } + assert.NoError(t, pipeline.InsertPipeline(db, &pip)) + + pip.Stages = []sdk.Stage{ + { + Name: "mystage", + BuildOrder: 1, + Enabled: true, + }, + } + + app := sdk.Application{ + Name: sdk.RandomString(10), + ProjectID: proj.ID, + RepositoryFullname: "foo/myrepo", + VCSServer: "github", + FromRepository: "myrepofrom", + } + assert.NoError(t, application.Insert(db, *proj, &app)) + assert.NoError(t, repositoriesmanager.InsertForApplication(db, &app)) + + repoModel, err := workflow.LoadHookModelByName(db, sdk.RepositoryWebHookModelName) + assert.NoError(t, err) + + wk := initWorkflow(t, db, proj, &app, &pip, repoModel) + wk.FromRepository = "myrepofrom" + require.NoError(t, workflow.Insert(context.Background(), db, api.Cache, *proj, wk)) + + uri := api.Router.GetRoute("PUT", api.updateAsCodeApplicationHandler, map[string]string{ + "permProjectKey": proj.Key, + "applicationName": app.Name, + }) + req := assets.NewJWTAuthentifiedRequest(t, pass, "PUT", uri, app) + q := req.URL.Query() + q.Set("branch", "master") + q.Set("message", "my message") + req.URL.RawQuery = q.Encode() + + // Do the request + wr := httptest.NewRecorder() + api.Router.Mux.ServeHTTP(wr, req) + assert.Equal(t, 200, wr.Code) + myOpe := new(sdk.Operation) + test.NoError(t, json.Unmarshal(wr.Body.Bytes(), myOpe)) + assert.NotEmpty(t, myOpe.UUID) + + cpt := 0 + for { + if cpt >= 10 { + t.Fail() + return + } + + // Get operation + uriGET := api.Router.GetRoute("GET", api.getWorkflowAsCodeHandler, map[string]string{ + "key": proj.Key, + "permWorkflowName": wk.Name, + "uuid": myOpe.UUID, + }) + reqGET, err := http.NewRequest("GET", uriGET, nil) + test.NoError(t, err) + assets.AuthentifyRequest(t, reqGET, u, pass) + wrGet := httptest.NewRecorder() + api.Router.Mux.ServeHTTP(wrGet, reqGET) + assert.Equal(t, 200, wrGet.Code) + myOpeGet := new(sdk.Operation) + err = json.Unmarshal(wrGet.Body.Bytes(), myOpeGet) + assert.NoError(t, err) + + if myOpeGet.Status < sdk.OperationStatusDone { + cpt++ + time.Sleep(1 * time.Second) + continue + } + test.NoError(t, json.Unmarshal(wrGet.Body.Bytes(), myOpeGet)) + assert.Equal(t, "myURL", myOpeGet.Setup.Push.PRLink) + break + } +} diff --git a/engine/api/ascode/pull_request.go b/engine/api/ascode/pull_request.go index 4ac49a8785..86770ba940 100644 --- a/engine/api/ascode/pull_request.go +++ b/engine/api/ascode/pull_request.go @@ -20,8 +20,9 @@ type EventType string // AsCodeEventType values. const ( - PipelineEvent EventType = "pipeline" - WorkflowEvent EventType = "workflow" + PipelineEvent EventType = "pipeline" + WorkflowEvent EventType = "workflow" + ApplicationEvent EventType = "application" ) type EntityData struct { @@ -184,6 +185,20 @@ func createPullRequest(ctx context.Context, db *gorp.DbMap, store cache.Store, p if !found { asCodeEvent.Data.Pipelines[ed.ID] = ed.Name } + case ApplicationEvent: + if asCodeEvent.Data.Applications == nil { + asCodeEvent.Data.Applications = make(map[int64]string) + } + found := false + for k := range asCodeEvent.Data.Applications { + if k == ed.ID { + found = true + break + } + } + if !found { + asCodeEvent.Data.Applications[ed.ID] = ed.Name + } } if err := UpsertEvent(db, asCodeEvent); err != nil { diff --git a/engine/api/environment_key.go b/engine/api/environment_key.go index c81fa4c4f7..c66794180f 100644 --- a/engine/api/environment_key.go +++ b/engine/api/environment_key.go @@ -102,28 +102,13 @@ func (api *API) addKeyInEnvironmentHandler() service.Handler { newKey.Name = "env-" + newKey.Name } - switch newKey.Type { - case sdk.KeyTypeSSH: - k, err := keys.GenerateSSHKey(newKey.Name) - if err != nil { - return sdk.WrapError(err, "addKeyInEnvironmentHandler> Cannot generate ssh key") - } - newKey.KeyID = k.KeyID - newKey.Public = k.Public - newKey.Private = k.Private - newKey.Type = k.Type - case sdk.KeyTypePGP: - k, err := keys.GeneratePGPKeyPair(newKey.Name) - if err != nil { - return sdk.WrapError(err, "addKeyInEnvironmentHandler> Cannot generate pgpKey") - } - newKey.KeyID = k.KeyID - newKey.Public = k.Public - newKey.Private = k.Private - newKey.Type = k.Type - default: - return sdk.WrapError(sdk.ErrUnknownKeyType, "addKeyInEnvironmentHandler> unknown key of type: %s", newKey.Type) + k, err := keys.GenerateKey(newKey.Name, newKey.Type) + if err != nil { + return err } + newKey.Public = k.Public + newKey.Private = k.Private + newKey.ID = k.ID tx, errT := api.mustDB().Begin() if errT != nil { diff --git a/engine/api/keys/key.go b/engine/api/keys/key.go new file mode 100644 index 0000000000..9de2488150 --- /dev/null +++ b/engine/api/keys/key.go @@ -0,0 +1,14 @@ +package keys + +import "github.com/ovh/cds/sdk" + +func GenerateKey(name string, t sdk.KeyType) (sdk.Key, error) { + switch t { + case sdk.KeyTypeSSH: + return GenerateSSHKey(name) + case sdk.KeyTypePGP: + return GeneratePGPKeyPair(name) + default: + return sdk.Key{}, sdk.WrapError(sdk.ErrUnknownKeyType, "unknown key of type: %s", t) + } +} diff --git a/engine/api/keys/parse.go b/engine/api/keys/parse.go index 1274a9d113..1cb275cc5b 100644 --- a/engine/api/keys/parse.go +++ b/engine/api/keys/parse.go @@ -60,24 +60,11 @@ func Parse(db gorp.SqlExecutor, projID int64, kname string, kval exportentities. return nil, sdk.ErrUnknownKeyType } } else if kval.Regen == nil || *kval.Regen == true { - switch k.Type { - //Compute PGP Keys - case sdk.KeyTypePGP: - ktemp, err := GeneratePGPKeyPair(kname) - if err != nil { - return nil, sdk.WrapError(err, "Unable to generate PGP key pair") - } - k = &ktemp - //Compute SSH Keys - case sdk.KeyTypeSSH: - ktemp, err := GenerateSSHKey(kname) - if err != nil { - return nil, sdk.WrapError(err, "Unable to generate SSH key pair") - } - k = &ktemp - default: - return nil, sdk.ErrUnknownKeyType + ktemp, err := GenerateKey(kname, k.Type) + if err != nil { + return nil, err } + k = &ktemp } else { log.Debug("keys.Parse> Skip key regeneration") } diff --git a/engine/api/pipeline.go b/engine/api/pipeline.go index 3ae3d45385..315fc4b507 100644 --- a/engine/api/pipeline.go +++ b/engine/api/pipeline.go @@ -65,7 +65,7 @@ func (api *API) updateAsCodePipelineHandler() service.Handler { return sdk.NewErrorFrom(sdk.ErrForbidden, "current pipeline is not ascode") } - wkHolder, err := workflow.LoadByRepo(ctx, api.Cache, api.mustDB(), *proj, pipelineDB.FromRepository, workflow.LoadOptions{ + wkHolder, err := workflow.LoadByRepo(ctx, api.mustDB(), *proj, pipelineDB.FromRepository, workflow.LoadOptions{ WithTemplate: true, }) if err != nil { @@ -327,7 +327,7 @@ func (api *API) getPipelineHandler() service.Handler { } if p.FromRepository != "" { - wkAscodeHolder, err := workflow.LoadByRepo(ctx, api.Cache, api.mustDB(), *proj, p.FromRepository, workflow.LoadOptions{ + wkAscodeHolder, err := workflow.LoadByRepo(ctx, api.mustDB(), *proj, p.FromRepository, workflow.LoadOptions{ WithTemplate: true, }) if err != nil && !sdk.ErrorIs(err, sdk.ErrNotFound) { diff --git a/engine/api/project.go b/engine/api/project.go index 199f80f5ca..23648d33ef 100644 --- a/engine/api/project.go +++ b/engine/api/project.go @@ -565,25 +565,15 @@ func (api *API) postProjectHandler() service.Handler { for i := range p.Keys { k := &p.Keys[i] k.ProjectID = p.ID - switch k.Type { - case sdk.KeyTypeSSH: - keyTemp, err := keys.GenerateSSHKey(k.Name) - if err != nil { - return sdk.WrapError(err, "cannot generate ssh key for project %s", p.Name) - } - k.Private = keyTemp.Private - k.Public = keyTemp.Public - k.Type = keyTemp.Type - case sdk.KeyTypePGP: - keyTemp, err := keys.GeneratePGPKeyPair(k.Name) - if err != nil { - return sdk.WrapError(err, "cannot generate pgp key for project %s", p.Name) - } - k.Private = keyTemp.Private - k.Public = keyTemp.Public - k.Type = keyTemp.Type - k.KeyID = keyTemp.KeyID + + newKey, err := keys.GenerateKey(k.Name, k.Type) + if err != nil { + return err } + k.Private = newKey.Private + k.Public = newKey.Public + k.KeyID = newKey.KeyID + if err := project.InsertKey(tx, k); err != nil { return sdk.WrapError(err, "cannot add key %s in project %s", k.Name, p.Name) } diff --git a/engine/api/project_key.go b/engine/api/project_key.go index c291106cea..fcfe4c9173 100644 --- a/engine/api/project_key.go +++ b/engine/api/project_key.go @@ -96,27 +96,13 @@ func (api *API) addKeyInProjectHandler() service.Handler { newKey.Name = "proj-" + newKey.Name } - switch newKey.Type { - case sdk.KeyTypeSSH: - k, errK := keys.GenerateSSHKey(newKey.Name) - if errK != nil { - return sdk.WrapError(errK, "addKeyInProjectHandler> Cannot generate ssh key") - } - newKey.Private = k.Private - newKey.Public = k.Public - newKey.Type = k.Type - case sdk.KeyTypePGP: - k, errGenerate := keys.GeneratePGPKeyPair(newKey.Name) - if errGenerate != nil { - return sdk.WrapError(errGenerate, "addKeyInProjectHandler> Cannot generate pgpKey") - } - newKey.Private = k.Private - newKey.Public = k.Public - newKey.Type = k.Type - newKey.KeyID = k.KeyID - default: - return sdk.WrapError(sdk.ErrUnknownKeyType, "addKeyInProjectHandler> unknown key of type: %s", newKey.Type) + k, err := keys.GenerateKey(newKey.Name, newKey.Type) + if err != nil { + return err } + newKey.Private = k.Private + newKey.Public = k.Public + newKey.KeyID = k.KeyID tx, errT := api.mustDB().Begin() if errT != nil { diff --git a/engine/api/workflow/dao.go b/engine/api/workflow/dao.go index 799a2f29b1..cfe6a19c00 100644 --- a/engine/api/workflow/dao.go +++ b/engine/api/workflow/dao.go @@ -69,13 +69,13 @@ func Exists(db gorp.SqlExecutor, key string, name string) (bool, error) { return count > 0, nil } -func LoadByRepo(ctx context.Context, store cache.Store, db gorp.SqlExecutor, proj sdk.Project, repo string, opts LoadOptions) (*sdk.Workflow, error) { +func LoadByRepo(ctx context.Context, db gorp.SqlExecutor, proj sdk.Project, repo string, opts LoadOptions) (*sdk.Workflow, error) { ctx, end := observability.Span(ctx, "workflow.Load") defer end() dao := opts.GetWorkflowDAO() dao.Filters.FromRepository = repo - dao.Limit = 1 + dao.Filters.ProjectKey = proj.Key ws, err := dao.Load(ctx, db) if err != nil { diff --git a/engine/api/workflow/workflow_exporter.go b/engine/api/workflow/workflow_exporter.go index d3e14e2a08..032fe92bf6 100644 --- a/engine/api/workflow/workflow_exporter.go +++ b/engine/api/workflow/workflow_exporter.go @@ -113,7 +113,7 @@ func Pull(ctx context.Context, db gorp.SqlExecutor, cache cache.Store, proj sdk. if a.FromRepository != wf.FromRepository { // don't export if coming from an other repository continue } - app, err := application.ExportApplication(db, a, encryptFunc) + app, err := application.ExportApplication(db, a, encryptFunc, fmt.Sprintf("appID:%d", a.ID)) if err != nil { return wp, sdk.WrapError(err, "unable to export app %s", a.Name) } diff --git a/sdk/application.go b/sdk/application.go index 182e969a44..153b0da194 100644 --- a/sdk/application.go +++ b/sdk/application.go @@ -32,6 +32,8 @@ type Application struct { DeploymentStrategies map[string]IntegrationConfig `json:"deployment_strategies,omitempty" db:"-" cli:"-"` Vulnerabilities []Vulnerability `json:"vulnerabilities,omitempty" db:"-" cli:"-"` FromRepository string `json:"from_repository,omitempty" db:"from_repository" cli:"-"` + // aggregate + WorkflowAscodeHolder *Workflow `json:"workflow_ascode_holder,omitempty" cli:"-" db:"-"` } // IsValid returns error if the application is not valid. diff --git a/sdk/exportentities/application.go b/sdk/exportentities/application.go index 220bb6888b..18e0430f87 100644 --- a/sdk/exportentities/application.go +++ b/sdk/exportentities/application.go @@ -49,7 +49,7 @@ func NewApplication(app sdk.Application, keys []EncryptedKey) (a Application, er a.Variables = make(map[string]VariableValue, len(app.Variables)) for _, v := range app.Variables { - at := string(v.Type) + at := v.Type if at == "string" { at = "" } diff --git a/ui/e2e/pages/workflow.ts b/ui/e2e/pages/workflow.ts index be31da586b..7727757ea8 100644 --- a/ui/e2e/pages/workflow.ts +++ b/ui/e2e/pages/workflow.ts @@ -1,5 +1,5 @@ import config from '../config'; -import { Selector, ClientFunction, t } from "testcafe"; +import { Selector, t } from "testcafe"; export default class WorkflowPage { diff --git a/ui/src/app/app.component.spec.ts b/ui/src/app/app.component.spec.ts index c0eb0b9092..faf6eba7bb 100644 --- a/ui/src/app/app.component.spec.ts +++ b/ui/src/app/app.component.spec.ts @@ -1,7 +1,6 @@ import { HttpClient, HttpRequest } from '@angular/common/http'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { Injector } from '@angular/core'; -import { getTestBed, TestBed } from '@angular/core/testing'; +import { TestBed } from '@angular/core/testing'; import { ActivatedRoute } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; @@ -41,8 +40,6 @@ import { NavbarModule } from './views/navbar/navbar.module'; describe('App: CDS', () => { - let injector: Injector; - beforeEach(() => { TestBed.configureTestingModule({ declarations: [ @@ -88,15 +85,8 @@ describe('App: CDS', () => { }), ] }); - - injector = getTestBed(); }); - afterEach(() => { - injector = undefined; - }); - - it('should create the app', () => { let fixture = TestBed.createComponent(AppComponent); let app = fixture.debugElement.componentInstance; diff --git a/ui/src/app/app.component.ts b/ui/src/app/app.component.ts index f6aa9c5f8a..68960c2df2 100644 --- a/ui/src/app/app.component.ts +++ b/ui/src/app/app.component.ts @@ -281,7 +281,7 @@ export class AppComponent implements OnInit { this.startSSE(); } } else { - this._appService.manageEvent(e); + this._appService.manageEvent(e); } } }); diff --git a/ui/src/app/model/application.model.ts b/ui/src/app/model/application.model.ts index f0d74af6b5..579cc4c145 100644 --- a/ui/src/app/model/application.model.ts +++ b/ui/src/app/model/application.model.ts @@ -3,7 +3,7 @@ import { Metric } from './metric.model'; import { Usage } from './usage.model'; import { Variable } from './variable.model'; import { VCSStrategy } from './vcs.model'; -import { Notification } from './workflow.model'; +import { Notification, Workflow } from './workflow.model'; import { WorkflowRun } from './workflow.run.model'; export const applicationNamePattern: RegExp = new RegExp('^[a-zA-Z0-9._-]+$'); @@ -21,7 +21,7 @@ export class Application { usage: Usage; keys: Array; vcs_strategy: VCSStrategy; - deployment_strategies: Map; + deployment_strategies: {}; vulnerabilities: Array; project_key: string; // project unique key from_repository: string; @@ -29,6 +29,8 @@ export class Application { // true if someone has updated the application ( used for warnings ) externalChange: boolean; + editModeChanged: boolean; + workflow_ascode_holder: Workflow; // Return true if pattern is good public static checkName(name: string): boolean { diff --git a/ui/src/app/model/authentication.model.ts b/ui/src/app/model/authentication.model.ts index 160d959faa..480ced40b0 100644 --- a/ui/src/app/model/authentication.model.ts +++ b/ui/src/app/model/authentication.model.ts @@ -22,7 +22,7 @@ export class AuthScope implements WithKey { key(): string { return this.value; } -}; +} export class AuthDriverManifests { is_first_connection: boolean; diff --git a/ui/src/app/service/application/application.service.ts b/ui/src/app/service/application/application.service.ts index 13f11ac3f2..6af028c1b6 100644 --- a/ui/src/app/service/application/application.service.ts +++ b/ui/src/app/service/application/application.service.ts @@ -3,6 +3,8 @@ import { HttpClient, HttpParams } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Application, Vulnerability } from 'app/model/application.model'; import { Key } from 'app/model/keys.model'; +import { Operation } from 'app/model/operation.model'; +import { Pipeline } from 'app/model/pipeline.model'; import { Observable } from 'rxjs'; @Injectable() @@ -55,4 +57,18 @@ export class ApplicationService { let url = '/project/' + key + '/application/' + appName + '/vulnerability/' + v.id; return this._http.post(url, v); } + + /** + * Update application as code + * @param key Project key + * @param application Application to update + * @param branch Branch name to create the PR + * @param message Message of the commit + */ + updateAsCode(key: string, oldAppName, application: Application, branch, message: string): Observable { + let params = new HttpParams(); + params = params.append('branch', branch); + params = params.append('message', message) + return this._http.put(`/project/${key}/application/${oldAppName}/ascode`, application, { params }); + } } diff --git a/ui/src/app/service/authentication/logout.interceptor.ts b/ui/src/app/service/authentication/logout.interceptor.ts index 45bb9733f5..099b2bfb46 100644 --- a/ui/src/app/service/authentication/logout.interceptor.ts +++ b/ui/src/app/service/authentication/logout.interceptor.ts @@ -1,8 +1,6 @@ import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { NavigationExtras, Router } from '@angular/router'; -import { TranslateService } from '@ngx-translate/core'; -import { ToastService } from 'app/shared/toast/ToastService'; import { Observable, throwError as observableThrowError } from 'rxjs'; import { catchError } from 'rxjs/operators'; @@ -10,9 +8,7 @@ import { catchError } from 'rxjs/operators'; export class LogoutInterceptor implements HttpInterceptor { constructor( - private _toast: ToastService, - private _router: Router, - private _translate: TranslateService) { + private _router: Router) { } intercept(req: HttpRequest, next: HttpHandler): Observable> { diff --git a/ui/src/app/shared/action/step/form/step.form.component.spec.ts b/ui/src/app/shared/action/step/form/step.form.component.spec.ts index 56bcd59859..5ecb613155 100644 --- a/ui/src/app/shared/action/step/form/step.form.component.spec.ts +++ b/ui/src/app/shared/action/step/form/step.form.component.spec.ts @@ -1,8 +1,7 @@ /* tslint:disable:no-unused-variable */ import { APP_BASE_HREF } from '@angular/common'; -import { Injector } from '@angular/core'; -import { fakeAsync, getTestBed, TestBed, tick } from '@angular/core/testing'; +import { fakeAsync, TestBed, tick } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { TranslateLoader, TranslateModule, TranslateParser, TranslateService } from '@ngx-translate/core'; import { Action } from '../../../../model/action.model'; @@ -13,7 +12,6 @@ import { StepEvent } from '../step.event'; import { ActionStepFormComponent } from './step.form.component'; describe('CDS: Step Form Component', () => { - let injector: Injector; beforeEach(() => { TestBed.configureTestingModule({ @@ -33,15 +31,8 @@ describe('CDS: Step Form Component', () => { SharedModule ] }); - - injector = getTestBed(); - }); - - afterEach(() => { - injector = undefined; }); - it('should send add step event', fakeAsync(() => { // Create component let fixture = TestBed.createComponent(ActionStepFormComponent); diff --git a/ui/src/app/shared/action/step/form/step.form.component.ts b/ui/src/app/shared/action/step/form/step.form.component.ts index dd2f166370..366cba10ea 100644 --- a/ui/src/app/shared/action/step/form/step.form.component.ts +++ b/ui/src/app/shared/action/step/form/step.form.component.ts @@ -26,7 +26,7 @@ export class ActionStepFormComponent implements OnInit { selectAction(id: number): void { this.selected = this.actions.find(a => a.id === Number(id)); - }; + } clickAddStep(): void { this.expended = false; diff --git a/ui/src/app/shared/action/step/step.component.spec.ts b/ui/src/app/shared/action/step/step.component.spec.ts index 1e4169d6d1..c9bc7a625d 100644 --- a/ui/src/app/shared/action/step/step.component.spec.ts +++ b/ui/src/app/shared/action/step/step.component.spec.ts @@ -1,9 +1,8 @@ /* tslint:disable:no-unused-variable */ -import {TestBed, fakeAsync, tick, getTestBed} from '@angular/core/testing'; +import {TestBed, fakeAsync, tick} from '@angular/core/testing'; import {TranslateService, TranslateParser, TranslateLoader, TranslateModule} from '@ngx-translate/core'; import {RouterTestingModule} from '@angular/router/testing'; -import {Injector} from '@angular/core'; import {SharedService} from '../../shared.service'; import {ParameterService} from '../../../service/parameter/parameter.service'; import {SharedModule} from '../../shared.module'; @@ -15,8 +14,6 @@ import {APP_BASE_HREF} from '@angular/common'; describe('CDS: Step Component', () => { - let injector: Injector; - beforeEach(() => { TestBed.configureTestingModule({ declarations: [ @@ -35,16 +32,8 @@ describe('CDS: Step Component', () => { SharedModule ] }); - - injector = getTestBed(); - }); - - afterEach(() => { - injector = undefined; }); - - it('should send remove step event', fakeAsync( () => { // Create component let fixture = TestBed.createComponent(ActionStepComponent); diff --git a/ui/src/app/shared/ascode/save-modal/ascode.save-modal.component.ts b/ui/src/app/shared/ascode/save-modal/ascode.save-modal.component.ts index 2990ef73f4..6cb69d4b94 100644 --- a/ui/src/app/shared/ascode/save-modal/ascode.save-modal.component.ts +++ b/ui/src/app/shared/ascode/save-modal/ascode.save-modal.component.ts @@ -1,10 +1,12 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, ViewChild } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { ModalTemplate, SuiActiveModal, SuiModalService, TemplateModalConfig } from '@richardlt/ng2-semantic-ui'; +import { Application } from 'app/model/application.model'; import { Operation } from 'app/model/operation.model'; import { Pipeline } from 'app/model/pipeline.model'; import { Project } from 'app/model/project.model'; import { Workflow } from 'app/model/workflow.model'; +import { ApplicationService } from 'app/service/application/application.service'; import { PipelineService } from 'app/service/pipeline/pipeline.service'; import { WorkflowService } from 'app/service/workflow/workflow.service'; import { AutoUnsubscribe } from 'app/shared/decorator/autoUnsubscribe'; @@ -32,7 +34,6 @@ export class AsCodeSaveModalComponent { dataToSave: any; dataType: string; loading: boolean; - webworkerSub: Subscription; asCodeOperation: Operation; pollingOperationSub: Subscription; parameters: ParamData; @@ -44,7 +45,8 @@ export class AsCodeSaveModalComponent { private _toast: ToastService, private _translate: TranslateService, private _workflowService: WorkflowService, - private _pipService: PipelineService + private _pipService: PipelineService, + private _appService: ApplicationService ) { } show(data: any, type: string) { @@ -90,6 +92,15 @@ export class AsCodeSaveModalComponent { this.startPollingOperation((this.dataToSave).workflow_ascode_holder.name); }); break; + case 'application': + this.loading = true; + this._cd.markForCheck(); + this._appService.updateAsCode(this.project.key, this.name, this.dataToSave, + this.parameters.branch_name, this.parameters.commit_message).subscribe(o => { + this.asCodeOperation = o; + this.startPollingOperation((this.dataToSave).workflow_ascode_holder.name); + }); + break; default: this._toast.error('', this._translate.instant('ascode_error_unknown_type')) } diff --git a/ui/src/app/shared/commit/commit.list.spec.ts b/ui/src/app/shared/commit/commit.list.spec.ts index f550816118..a82b50154c 100644 --- a/ui/src/app/shared/commit/commit.list.spec.ts +++ b/ui/src/app/shared/commit/commit.list.spec.ts @@ -1,7 +1,6 @@ /* tslint:disable:no-unused-variable */ -import {TestBed, fakeAsync, getTestBed} from '@angular/core/testing'; +import {TestBed, fakeAsync} from '@angular/core/testing'; import {RouterTestingModule} from '@angular/router/testing'; -import {Injector} from '@angular/core'; import {TranslateService, TranslateLoader, TranslateParser, TranslateModule} from '@ngx-translate/core'; import {CommitListComponent} from './commit.list.component'; import {SharedModule} from '../shared.module'; @@ -9,8 +8,6 @@ import {APP_BASE_HREF} from '@angular/common'; describe('CDS: Commit List', () => { - let injector: Injector; - beforeEach(() => { TestBed.configureTestingModule({ declarations: [], @@ -26,12 +23,6 @@ describe('CDS: Commit List', () => { SharedModule ] }); - - injector = getTestBed(); - }); - - afterEach(() => { - injector = undefined; }); it('should load component', fakeAsync(() => { diff --git a/ui/src/app/shared/parameter/form/parameter.form.spec.ts b/ui/src/app/shared/parameter/form/parameter.form.spec.ts index 08ab86b581..a49bd77fd4 100644 --- a/ui/src/app/shared/parameter/form/parameter.form.spec.ts +++ b/ui/src/app/shared/parameter/form/parameter.form.spec.ts @@ -1,6 +1,5 @@ import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { Injector } from '@angular/core'; -import { fakeAsync, getTestBed, TestBed, tick } from '@angular/core/testing'; +import { fakeAsync, TestBed, tick } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { TranslateLoader, TranslateModule, TranslateParser, TranslateService } from '@ngx-translate/core'; import { Parameter } from 'app/model/parameter.model'; @@ -13,7 +12,6 @@ import { ParameterFormComponent } from './parameter.form'; import { Observable, of } from 'rxjs'; describe('CDS: parameter From Component', () => { - let injector: Injector; beforeEach(() => { TestBed.configureTestingModule({ @@ -35,11 +33,6 @@ describe('CDS: parameter From Component', () => { HttpClientTestingModule ] }); - injector = getTestBed(); - }); - - afterEach(() => { - injector = undefined; }); @@ -92,4 +85,4 @@ class MockParameterService { getTypesFromAPI(): Observable { return of(["string", "password"]) } -} \ No newline at end of file +} diff --git a/ui/src/app/shared/parameter/value/parameter.value.component.ts b/ui/src/app/shared/parameter/value/parameter.value.component.ts index d587eb8ead..5041734b83 100644 --- a/ui/src/app/shared/parameter/value/parameter.value.component.ts +++ b/ui/src/app/shared/parameter/value/parameter.value.component.ts @@ -37,7 +37,7 @@ export class ParameterValueComponent implements OnInit, AfterViewChecked { @Input('value') set value(data: string | number | boolean) { this.castValue(data); - }; + } @Input() editList = true; @Input() edit = true; @@ -47,7 +47,7 @@ export class ParameterValueComponent implements OnInit, AfterViewChecked { @Input('ref') set ref(data: Parameter | Array) { if (data && (data).type === 'list') { - this.refValue = ((data).value).split(';'); + this.refValue = (data).value.split(';'); } else if (data && Array.isArray(data)) { this.list = data; } diff --git a/ui/src/app/shared/permission/form/permission.form.component.spec.ts b/ui/src/app/shared/permission/form/permission.form.component.spec.ts index 5e3f749067..0e42f843ed 100644 --- a/ui/src/app/shared/permission/form/permission.form.component.spec.ts +++ b/ui/src/app/shared/permission/form/permission.form.component.spec.ts @@ -1,6 +1,6 @@ import { HttpRequest } from '@angular/common/http'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { fakeAsync, inject, TestBed, tick } from '@angular/core/testing'; +import { fakeAsync, TestBed, tick } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { TranslateLoader, TranslateModule, TranslateParser, TranslateService } from '@ngx-translate/core'; import { Group, GroupPermission } from '../../../model/group.model'; diff --git a/ui/src/app/shared/table/data-table.component.ts b/ui/src/app/shared/table/data-table.component.ts index da76015276..9af687d9b3 100644 --- a/ui/src/app/shared/table/data-table.component.ts +++ b/ui/src/app/shared/table/data-table.component.ts @@ -59,7 +59,7 @@ export class SelectorPipe implements PipeTransform { let type: ColumnType; switch (typeof c.type) { case 'function': - type = (>c.type)(data); + type = c.type(data); break; default: type = c.type; @@ -138,7 +138,7 @@ export class DataTableComponent extends Table implements O if (this.withSelect) { this.allData.forEach(d => this.selected[d.key()] = false); if (typeof this.withSelect === 'function') { - this.allData.filter(>this.withSelect).forEach(d => this.selected[d.key()] = true); + this.allData.filter(this.withSelect).forEach(d => this.selected[d.key()] = true); this.emitSelectChange(); } } diff --git a/ui/src/app/shared/table/table.spec.ts b/ui/src/app/shared/table/table.spec.ts index 3a5ab85674..4d17e13a4e 100644 --- a/ui/src/app/shared/table/table.spec.ts +++ b/ui/src/app/shared/table/table.spec.ts @@ -1,13 +1,9 @@ /* tslint:disable:no-unused-variable */ - -import { Injector } from '@angular/core'; import { fakeAsync, TestBed } from '@angular/core/testing'; import { Table } from './table'; describe('CDS: Table component', () => { - let injector: Injector; - beforeEach(() => { TestBed.configureTestingModule({ declarations: [ @@ -20,11 +16,6 @@ describe('CDS: Table component', () => { }); - afterEach(() => { - injector = undefined; - }); - - it('Table method', fakeAsync(() => { // Create loginComponent let myTable = new MyTable(); diff --git a/ui/src/app/shared/variable/audit/variable.audit.spec.ts b/ui/src/app/shared/variable/audit/variable.audit.spec.ts index c87fc2fc72..9fd8f0ced4 100644 --- a/ui/src/app/shared/variable/audit/variable.audit.spec.ts +++ b/ui/src/app/shared/variable/audit/variable.audit.spec.ts @@ -1,18 +1,15 @@ /* tslint:disable:no-unused-variable */ -import {TestBed, getTestBed, fakeAsync, tick} from '@angular/core/testing'; +import {TestBed, fakeAsync, tick} from '@angular/core/testing'; import {TranslateService, TranslateLoader, TranslateParser, TranslateModule} from '@ngx-translate/core'; import {SharedService} from '../../shared.service'; import {RouterTestingModule} from '@angular/router/testing'; import {VariableAudit} from '../../../model/variable.model'; -import {Injector} from '@angular/core'; import {SharedModule} from '../../shared.module'; import {VariableAuditComponent} from './audit.component'; import {APP_BASE_HREF} from '@angular/common'; describe('CDS: Variable Audit Component', () => { - let injector: Injector; - beforeEach(() => { TestBed.configureTestingModule({ declarations: [ @@ -31,15 +28,8 @@ describe('CDS: Variable Audit Component', () => { ] }); - injector = getTestBed(); - }); - afterEach(() => { - injector = undefined; - }); - - it('Load Component', fakeAsync( () => { // Create component let fixture = TestBed.createComponent(VariableAuditComponent); diff --git a/ui/src/app/shared/variable/form/variable.form.spec.ts b/ui/src/app/shared/variable/form/variable.form.spec.ts index fe84ef3e1c..64104787e1 100644 --- a/ui/src/app/shared/variable/form/variable.form.spec.ts +++ b/ui/src/app/shared/variable/form/variable.form.spec.ts @@ -1,24 +1,20 @@ /* tslint:disable:no-unused-variable */ -import {TestBed, getTestBed, tick, fakeAsync} from '@angular/core/testing'; +import {TestBed, tick, fakeAsync} from '@angular/core/testing'; import {VariableService} from '../../../service/variable/variable.service'; import {TranslateService, TranslateLoader, TranslateParser, TranslateModule} from '@ngx-translate/core'; import {SharedService} from '../../shared.service'; import {RouterTestingModule} from '@angular/router/testing'; -import {Injector} from '@angular/core'; import {VariableFormComponent} from './variable.form'; import {GroupService} from '../../../service/group/group.service'; import {Variable} from '../../../model/variable.model'; import {VariableEvent} from '../variable.event.model'; import {SharedModule} from '../../shared.module'; import {HttpClientTestingModule} from '@angular/common/http/testing'; -import { ParameterService } from 'app/service/parameter/parameter.service'; import { Observable, of } from 'rxjs'; describe('CDS: Variable From Component', () => { - let injector: Injector; - beforeEach(() => { TestBed.configureTestingModule({ declarations: [ @@ -38,11 +34,6 @@ describe('CDS: Variable From Component', () => { HttpClientTestingModule ] }); - injector = getTestBed(); - }); - - afterEach(() => { - injector = undefined; }); @@ -100,4 +91,4 @@ class MockApplicationService { getTypesFromAPI(): Observable { return of(["string", "password"]) } -} \ No newline at end of file +} diff --git a/ui/src/app/shared/variable/list/variable.component.ts b/ui/src/app/shared/variable/list/variable.component.ts index dc2836c24c..f230f4c215 100644 --- a/ui/src/app/shared/variable/list/variable.component.ts +++ b/ui/src/app/shared/variable/list/variable.component.ts @@ -39,15 +39,15 @@ export class VariableComponent extends Table { @Input('maxPerPage') set maxPerPage(data: number) { this.nbElementsByPage = data; - }; + } // display mode: edit (edit all field) / launcher (only type value) /ro (display field, no edit) @Input() mode = 'edit'; // project/application/environment @Input() auditContext: string; @Input() project: Project; - @Input() environment: Environment; - @Input() application: Application; + @Input() environmentName: string; + @Input() applicationName: string; @Output() event = new EventEmitter(); @@ -110,7 +110,7 @@ export class VariableComponent extends Table { }); break; case 'environment': - this._envAudit.getVariableAudit(this.project.key, this.environment.name, v.name).subscribe(audits => { + this._envAudit.getVariableAudit(this.project.key, this.environmentName, v.name).subscribe(audits => { this.currentVariableAudits = audits; setTimeout(() => { this.auditModal.show({ observeChanges: true }); @@ -118,7 +118,7 @@ export class VariableComponent extends Table { }); break; case 'application': - this._appAudit.getVariableAudit(this.project.key, this.application.name, v.name).subscribe(audits => { + this._appAudit.getVariableAudit(this.project.key, this.applicationName, v.name).subscribe(audits => { this.currentVariableAudits = audits; setTimeout(() => { this.auditModal.show({ observeChanges: true }); diff --git a/ui/src/app/shared/vcs/vcs.strategy.component.ts b/ui/src/app/shared/vcs/vcs.strategy.component.ts index 3c31ee47d9..8440bec595 100644 --- a/ui/src/app/shared/vcs/vcs.strategy.component.ts +++ b/ui/src/app/shared/vcs/vcs.strategy.component.ts @@ -53,7 +53,6 @@ export class VCSStrategyComponent implements OnInit { @Output() strategyChange = new EventEmitter(); keys: AllKeys; connectionType = VCSConnections; - displayVCSStrategy = false; defaultKeyType = 'ssh'; @ViewChild('createKey') diff --git a/ui/src/app/shared/vulnerability/list/vulnerability.list.component.ts b/ui/src/app/shared/vulnerability/list/vulnerability.list.component.ts index b77ace7573..6101384e16 100644 --- a/ui/src/app/shared/vulnerability/list/vulnerability.list.component.ts +++ b/ui/src/app/shared/vulnerability/list/vulnerability.list.component.ts @@ -33,7 +33,7 @@ export class VulnerabilitiesListComponent { this.allVulnerabilities = data; this.updateVulns(); } - }; + } @Input() edit = false; application: Application; diff --git a/ui/src/app/shared/worker/shared.worker.ts b/ui/src/app/shared/worker/shared.worker.ts index 1a6514887b..a38d6386be 100644 --- a/ui/src/app/shared/worker/shared.worker.ts +++ b/ui/src/app/shared/worker/shared.worker.ts @@ -3,7 +3,7 @@ import { BehaviorSubject, Observable } from 'rxjs'; import { CDSWorker } from './worker'; -declare module SharedWorker { +declare module SharedWorkerModule { interface AbstractWorker extends EventTarget { onerror: (ev: ErrorEvent) => any; } @@ -13,14 +13,14 @@ declare module SharedWorker { } } declare var SharedWorker: { - prototype: SharedWorker.SharedWorker; - new(stringUrl: string, name?: string): SharedWorker.SharedWorker; + prototype: SharedWorkerModule.SharedWorker; + new(stringUrl: string, name?: string): SharedWorkerModule.SharedWorker; }; export class CDSSharedWorker implements CDSWorker { // Webworker - sharedWorker: SharedWorker.SharedWorker; + sharedWorker: SharedWorkerModule.SharedWorker; sharedWorkerScript: string; diff --git a/ui/src/app/shared/workflow/node/run/node.run.param.component.ts b/ui/src/app/shared/workflow/node/run/node.run.param.component.ts index 5bdc0042fa..f98cf9ccb2 100644 --- a/ui/src/app/shared/workflow/node/run/node.run.param.component.ts +++ b/ui/src/app/shared/workflow/node/run/node.run.param.component.ts @@ -169,9 +169,7 @@ export class WorkflowNodeRunParamComponent implements AfterViewInit { }); } else { let isPipelineRoot = false; - if (!this.currentWorkflowRun) { - isPipelineRoot = true; - } else if (this.currentWorkflowRun && this.currentWorkflowRun.workflow.workflow_data.node.id === this.nodeToRun.id) { + if (!this.currentWorkflowRun || this.currentWorkflowRun.workflow.workflow_data.node.id === this.nodeToRun.id) { isPipelineRoot = true; } // run a workflow or a child pipeline, first run diff --git a/ui/src/app/store/applications.action.ts b/ui/src/app/store/applications.action.ts index 9b89a0071e..fcfd0fd080 100644 --- a/ui/src/app/store/applications.action.ts +++ b/ui/src/app/store/applications.action.ts @@ -113,4 +113,9 @@ export class ClearCacheApplication { constructor() { } } +export class CancelApplicationEdition { + static readonly type = '[Application] Cancel application edition'; + constructor() { } +} + diff --git a/ui/src/app/store/applications.state.ts b/ui/src/app/store/applications.state.ts index 866c06993d..799340cc27 100644 --- a/ui/src/app/store/applications.state.ts +++ b/ui/src/app/store/applications.state.ts @@ -14,17 +14,21 @@ import * as ActionProject from './project.action'; export class ApplicationStateModel { public application: Application; + public editApplication: Application; public overview: Overview; public currentProjectKey: string; public loading: boolean; + public editMode: boolean; } export function getInitialApplicationsState(): ApplicationStateModel { return { application: null, + editApplication: null, overview: null, currentProjectKey: null, loading: true, + editMode: false }; } @@ -89,10 +93,17 @@ export class ApplicationsState { @Action(ActionApplication.LoadApplication) load(ctx: StateContext, action: ActionApplication.LoadApplication) { const state = ctx.getState(); + + let editMode = false; + if (action.payload.from_repository) { + editMode = true; + } ctx.setState({ ...state, currentProjectKey: action.payload.project_key, application: action.payload, + editApplication: cloneDeep(action.payload), + editMode: editMode, loading: false, }); } @@ -109,6 +120,22 @@ export class ApplicationsState { @Action(ActionApplication.UpdateApplication) update(ctx: StateContext, action: ActionApplication.UpdateApplication) { + const stateEditMode = ctx.getState(); + if (stateEditMode.editMode) { + let appToUpdate = cloneDeep(stateEditMode.editApplication); + appToUpdate.name = action.payload.changes.name; + appToUpdate.description = action.payload.changes.description; + appToUpdate.deployment_strategies = action.payload.changes.deployment_strategies; + appToUpdate.vcs_strategy = action.payload.changes.vcs_strategy; + appToUpdate.editModeChanged = true; + return ctx.setState({ + ...stateEditMode, + editApplication: appToUpdate, + }); + return; + } + + return this._http.put( `/project/${action.payload.projectKey}/application/${action.payload.applicationName}`, action.payload.changes @@ -171,6 +198,24 @@ export class ApplicationsState { @Action(ActionApplication.AddApplicationVariable) addVariable(ctx: StateContext, action: ActionApplication.AddApplicationVariable) { let variable = action.payload.variable; + + const stateEditMode = ctx.getState(); + if (stateEditMode.editMode) { + let appToupdate = cloneDeep(stateEditMode.editApplication); + if (!appToupdate.variables) { + appToupdate.variables = new Array(); + } + action.payload.variable.hasChanged = false; + action.payload.variable.updating = false; + appToupdate.variables.push(cloneDeep(action.payload.variable)); + appToupdate.editModeChanged = true; + return ctx.setState({ + ...stateEditMode, + editApplication: appToupdate, + }); + } + + let url = '/project/' + action.payload.projectKey + '/application/' + action.payload.applicationName + '/variable/' + variable.name; return this._http.post(url, variable) .pipe(tap((v) => { @@ -190,6 +235,23 @@ export class ApplicationsState { @Action(ActionApplication.UpdateApplicationVariable) updateVariable(ctx: StateContext, action: ActionApplication.UpdateApplicationVariable) { let variable = action.payload.variable; + + const stateEditMode = ctx.getState(); + if (stateEditMode.editMode) { + let appToupdate = cloneDeep(stateEditMode.editApplication); + let varIndex = appToupdate.variables.findIndex(app => action.payload.variableName === app.name); + action.payload.variable.hasChanged = false; + action.payload.variable.updating = false; + appToupdate.variables[varIndex] = action.payload.variable; + appToupdate.editModeChanged = true; + ctx.setState({ + ...stateEditMode, + editApplication: appToupdate, + }); + return + } + + let url = '/project/' + action.payload.projectKey + '/application/' + action.payload.applicationName + '/variable/' + action.payload.variableName; @@ -214,6 +276,20 @@ export class ApplicationsState { @Action(ActionApplication.DeleteApplicationVariable) deleteVariable(ctx: StateContext, action: ActionApplication.DeleteApplicationVariable) { let variable = action.payload.variable; + + const stateEditMode = ctx.getState(); + if (stateEditMode.editMode) { + let appToupdate = cloneDeep(stateEditMode.editApplication); + action.payload.variable.hasChanged = false; + action.payload.variable.updating = false; + appToupdate.variables = appToupdate.variables.filter(app => app.name !== action.payload.variable.name); + appToupdate.editModeChanged = true; + ctx.setState({ + ...stateEditMode, + editApplication: appToupdate, + }); + return + } let url = `/project/${action.payload.projectKey}/application/${action.payload.applicationName}/variable/${variable.name}`; return this._http.delete(url) .pipe(tap(() => { @@ -232,6 +308,22 @@ export class ApplicationsState { @Action(ActionApplication.AddApplicationKey) addKey(ctx: StateContext, action: ActionApplication.AddApplicationKey) { let key = action.payload.key; + + const stateEditMode = ctx.getState(); + if (stateEditMode.editMode) { + let appToupdate = cloneDeep(stateEditMode.editApplication); + if (!appToupdate.keys) { + appToupdate.keys = new Array(); + } + appToupdate.keys.push(action.payload.key); + appToupdate.editModeChanged = true; + ctx.setState({ + ...stateEditMode, + editApplication: appToupdate, + }); + return + } + let url = '/project/' + action.payload.projectKey + '/application/' + action.payload.applicationName + '/keys'; return this._http.post(url, key) .pipe(tap((newKey) => { @@ -249,6 +341,19 @@ export class ApplicationsState { @Action(ActionApplication.DeleteApplicationKey) deleteKey(ctx: StateContext, action: ActionApplication.DeleteApplicationKey) { let key = action.payload.key; + + const stateEditMode = ctx.getState(); + if (stateEditMode.editMode) { + let appToupdate = cloneDeep(stateEditMode.editApplication); + appToupdate.keys = appToupdate.keys.filter(k => action.payload.key.name !== k.name); + appToupdate.editModeChanged = true; + ctx.setState({ + ...stateEditMode, + editApplication: appToupdate, + }); + return + } + let url = `/project/${action.payload.projectKey}/application/${action.payload.applicationName}/keys/${key.name}`; return this._http.delete(url) .pipe(tap(() => { @@ -267,6 +372,23 @@ export class ApplicationsState { @Action(ActionApplication.AddApplicationDeployment) addDeployment(ctx: StateContext, action: ActionApplication.AddApplicationDeployment) { let integration = action.payload.integration; + + const stateEditMode = ctx.getState(); + if (stateEditMode.editMode) { + let appToupdate = cloneDeep(stateEditMode.editApplication); + if (!appToupdate.deployment_strategies) { + appToupdate.deployment_strategies = {}; + } + console.log(integration); + appToupdate.deployment_strategies[integration.name] = cloneDeep(integration.model.deployment_default_config); + appToupdate.editModeChanged = true; + ctx.setState({ + ...stateEditMode, + editApplication: appToupdate, + }); + return + } + let url = '/project/' + action.payload.projectKey + '/application/' + action.payload.applicationName + '/deployment/config/' + integration.name; return this._http.post(url, integration.model.deployment_default_config) @@ -290,6 +412,18 @@ export class ApplicationsState { integration.model = new IntegrationModel(); integration.model.deployment_default_config = action.payload.config; + const stateEditMode = ctx.getState(); + if (stateEditMode.editMode) { + let appToupdate = cloneDeep(stateEditMode.editApplication); + appToupdate.deployment_strategies[integration.name] = integration.model.deployment_default_config; + appToupdate.editModeChanged = true; + ctx.setState({ + ...stateEditMode, + editApplication: appToupdate, + }); + return + } + return ctx.dispatch(new ActionApplication.AddApplicationDeployment({ projectKey: action.payload.projectKey, applicationName: action.payload.applicationName, @@ -299,6 +433,19 @@ export class ApplicationsState { @Action(ActionApplication.DeleteApplicationDeployment) deleteDeployment(ctx: StateContext, action: ActionApplication.DeleteApplicationDeployment) { + + const stateEditMode = ctx.getState(); + if (stateEditMode.editMode) { + let appToupdate = cloneDeep(stateEditMode.editApplication); + delete appToupdate.deployment_strategies[action.payload.integrationName]; + appToupdate.editModeChanged = true; + ctx.setState({ + ...stateEditMode, + editApplication: appToupdate, + }); + return + } + let url = '/project/' + action.payload.projectKey + '/application/' + action.payload.applicationName + '/deployment/config/' + action.payload.integrationName; return this._http.delete(url) @@ -393,4 +540,19 @@ export class ApplicationsState { clearCache(ctx: StateContext, _: ActionApplication.ClearCacheApplication) { ctx.setState(getInitialApplicationsState()); } + + @Action(ActionApplication.CancelApplicationEdition) + cancelApplicationEdition(ctx: StateContext, _: ActionApplication.CancelApplicationEdition) { + const state = ctx.getState(); + let editMode = state.editMode; + if (state.application.from_repository) { + editMode = true; + } + let editApplication = cloneDeep(state.application); + ctx.setState({ + ...state, + editApplication: editApplication, + editMode: editMode, + }); + } } diff --git a/ui/src/app/store/pipelines.state.spec.ts b/ui/src/app/store/pipelines.state.spec.ts index 1ee18f26ad..9087b934ec 100644 --- a/ui/src/app/store/pipelines.state.spec.ts +++ b/ui/src/app/store/pipelines.state.spec.ts @@ -23,7 +23,6 @@ import { PipelineService } from 'app/service/pipeline/pipeline.service'; import { EnvironmentService } from 'app/service/environment/environment.service'; import { ApplicationService } from 'app/service/application/application.service'; import { RouterService } from 'app/service/router/router.service'; -import { Router } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; describe('Pipelines', () => { diff --git a/ui/src/app/store/pipelines.state.ts b/ui/src/app/store/pipelines.state.ts index dc485e4130..ae97d2fb7c 100644 --- a/ui/src/app/store/pipelines.state.ts +++ b/ui/src/app/store/pipelines.state.ts @@ -154,6 +154,21 @@ export class PipelinesState { @Action(actionPipeline.AddPipelineParameter) addParameter(ctx: StateContext, action: actionPipeline.AddPipelineParameter) { let parameter = action.payload.parameter; + const stateEditMode = ctx.getState(); + if (stateEditMode.editMode) { + let pipToUpdate = cloneDeep(stateEditMode.editPipeline); + if (!pipToUpdate.parameters) { + pipToUpdate.parameters = new Array(); + } + pipToUpdate.parameters.push(action.payload.parameter); + pipToUpdate.editModeChanged = true; + ctx.setState({ + ...stateEditMode, + editPipeline: pipToUpdate, + }); + return; + } + let url = '/project/' + action.payload.projectKey + '/pipeline/' + action.payload.pipelineName + '/parameter/' + parameter.name; return this._http.post(url, parameter) .pipe(tap((param) => { @@ -173,6 +188,22 @@ export class PipelinesState { @Action(actionPipeline.UpdatePipelineParameter) updateParameter(ctx: StateContext, action: actionPipeline.UpdatePipelineParameter) { let parameter = action.payload.parameter; + + const stateEditMode = ctx.getState(); + if (stateEditMode.editMode) { + let pipToUpdate = cloneDeep(stateEditMode.editPipeline); + let indexParam = pipToUpdate.parameters.findIndex(p => p.name === action.payload.parameterName); + action.payload.parameter.hasChanged = false; + action.payload.parameter.updating = false; + pipToUpdate.parameters[indexParam] = action.payload.parameter + pipToUpdate.editModeChanged = true; + ctx.setState({ + ...stateEditMode, + editPipeline: pipToUpdate, + }); + return; + } + let url = '/project/' + action.payload.projectKey + '/pipeline/' + action.payload.pipelineName + '/parameter/' + action.payload.parameterName; @@ -197,6 +228,20 @@ export class PipelinesState { @Action(actionPipeline.DeletePipelineParameter) deleteParameter(ctx: StateContext, action: actionPipeline.DeletePipelineParameter) { let parameter = action.payload.parameter; + + const stateEditMode = ctx.getState(); + if (stateEditMode.editMode) { + let pipToUpdate = cloneDeep(stateEditMode.editPipeline); + pipToUpdate.parameters = pipToUpdate.parameters.filter(p => p.name !== action.payload.parameter.name); + pipToUpdate.editModeChanged = true; + ctx.setState({ + ...stateEditMode, + editPipeline: pipToUpdate, + }); + return; + } + + let url = `/project/${action.payload.projectKey}/pipeline/${action.payload.pipelineName}/parameter/${parameter.name}`; return this._http.delete(url) .pipe(tap(() => { diff --git a/ui/src/app/store/project.state.spec.ts b/ui/src/app/store/project.state.spec.ts index 322dc62ad2..52617354ff 100644 --- a/ui/src/app/store/project.state.spec.ts +++ b/ui/src/app/store/project.state.spec.ts @@ -26,7 +26,6 @@ import { PipelineService } from 'app/service/pipeline/pipeline.service'; import { EnvironmentService } from 'app/service/environment/environment.service'; import { ApplicationService } from 'app/service/application/application.service'; import { RouterService } from 'app/service/router/router.service'; -import { Router } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; describe('Project', () => { diff --git a/ui/src/app/store/workflow.state.spec.ts b/ui/src/app/store/workflow.state.spec.ts index 4762c04bd0..489e6e0c27 100644 --- a/ui/src/app/store/workflow.state.spec.ts +++ b/ui/src/app/store/workflow.state.spec.ts @@ -10,7 +10,7 @@ import { ProjectService } from 'app/service/project/project.service'; import { ProjectStore } from 'app/service/project/project.store'; import { WorkflowRunService } from 'app/service/workflow/run/workflow.run.service'; import { WorkflowService } from 'app/service/workflow/workflow.service'; -import { ApplicationsState, ApplicationStateModel } from './applications.state'; +import { ApplicationsState } from './applications.state'; import { PipelinesState } from './pipelines.state'; import { AddProject } from './project.action'; import { ProjectState, ProjectStateModel } from './project.state'; @@ -21,10 +21,6 @@ import { EnvironmentService } from 'app/service/environment/environment.service' import { ApplicationService } from 'app/service/application/application.service'; import { RouterService } from 'app/service/router/router.service'; import { RouterTestingModule } from '@angular/router/testing'; -import { Router } from '@angular/router'; -import { Application } from 'app/model/application.model'; -import { Usage } from 'app/model/usage.model'; -import { of } from 'rxjs'; describe('Workflows', () => { let store: Store; diff --git a/ui/src/app/views/admin/service/show/service.show.component.ts b/ui/src/app/views/admin/service/show/service.show.component.ts index 0be5ce9e33..dcb95b5881 100644 --- a/ui/src/app/views/admin/service/show/service.show.component.ts +++ b/ui/src/app/views/admin/service/show/service.show.component.ts @@ -67,9 +67,7 @@ export class ServiceShowComponent implements OnInit { this.service.version = element.value; } if (srv.status !== 'AL') { - if (element.status === 'AL') { - srv.status = element.status; - } else if (element.status === 'WARN') { + if (element.status === 'AL' || element.status === 'WARN') { srv.status = element.status; } } diff --git a/ui/src/app/views/application/show/admin/application.admin.component.spec.ts b/ui/src/app/views/application/show/admin/application.admin.component.spec.ts index f5cd72afe7..1fcadb5c98 100644 --- a/ui/src/app/views/application/show/admin/application.admin.component.spec.ts +++ b/ui/src/app/views/application/show/admin/application.admin.component.spec.ts @@ -43,7 +43,6 @@ class DummyComponent { describe('CDS: Application Admin Component', () => { let injector: Injector; - let appStore: ApplicationStore; let router: Router; beforeEach(() => { @@ -91,13 +90,11 @@ describe('CDS: Application Admin Component', () => { injector = getTestBed(); - appStore = injector.get(ApplicationStore); router = injector.get(Router); }); afterEach(() => { injector = undefined; - appStore = undefined; router = undefined; }); diff --git a/ui/src/app/views/application/show/admin/application.admin.component.ts b/ui/src/app/views/application/show/admin/application.admin.component.ts index 927feb5e22..890ab532a6 100644 --- a/ui/src/app/views/application/show/admin/application.admin.component.ts +++ b/ui/src/app/views/application/show/admin/application.admin.component.ts @@ -2,15 +2,15 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit, V import { Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; import { Store } from '@ngxs/store'; +import { Application } from 'app/model/application.model'; +import { Project } from 'app/model/project.model'; +import { AuthentifiedUser } from 'app/model/user.model'; +import { WarningModalComponent } from 'app/shared/modal/warning/warning.component'; +import { ToastService } from 'app/shared/toast/ToastService'; import { DeleteApplication, UpdateApplication } from 'app/store/applications.action'; import { AuthenticationState } from 'app/store/authentication.state'; import cloneDeep from 'lodash-es/cloneDeep'; import { finalize } from 'rxjs/operators'; -import { Application } from '../../../../model/application.model'; -import { Project } from '../../../../model/project.model'; -import { AuthentifiedUser } from '../../../../model/user.model'; -import { WarningModalComponent } from '../../../../shared/modal/warning/warning.component'; -import { ToastService } from '../../../../shared/toast/ToastService'; @Component({ selector: 'app-application-admin', @@ -21,6 +21,7 @@ import { ToastService } from '../../../../shared/toast/ToastService'; export class ApplicationAdminComponent implements OnInit { @Input() application: Application; @Input() project: Project; + @Input() editMode: boolean; @ViewChild('updateWarning') private updateWarningModal: WarningModalComponent; @@ -64,10 +65,15 @@ export class ApplicationAdminComponent implements OnInit { this._cd.markForCheck(); })) .subscribe(() => { - this._toast.success('', this._translate.instant('application_update_ok')); - if (nameUpdated) { - this._router.navigate(['/project', this.project.key, 'application', this.newName]); + if (this.editMode) { + this._toast.info('', this._translate.instant('application_ascode_updated')); + } else { + this._toast.success('', this._translate.instant('application_update_ok')); + if (nameUpdated) { + this._router.navigate(['/project', this.project.key, 'application', this.newName]); + } } + }); } } diff --git a/ui/src/app/views/application/show/admin/application.admin.html b/ui/src/app/views/application/show/admin/application.admin.html index 44a5838853..bef6c0b402 100644 --- a/ui/src/app/views/application/show/admin/application.admin.html +++ b/ui/src/app/views/application/show/admin/application.admin.html @@ -13,7 +13,7 @@
-
@@ -31,7 +31,7 @@
-
@@ -60,13 +60,13 @@ - + - + diff --git a/ui/src/app/views/application/show/admin/deployment/application.deployment.component.ts b/ui/src/app/views/application/show/admin/deployment/application.deployment.component.ts index 2c7fa4e3c4..92c9906f8c 100644 --- a/ui/src/app/views/application/show/admin/deployment/application.deployment.component.ts +++ b/ui/src/app/views/application/show/admin/deployment/application.deployment.component.ts @@ -6,6 +6,7 @@ import { DeleteApplicationDeployment, UpdateApplicationDeployment } from 'app/store/applications.action'; +import { cloneDeep } from 'lodash-es'; import { finalize } from 'rxjs/operators'; import { Application } from '../../../../../model/application.model'; import { ProjectIntegration } from '../../../../../model/integration.model'; @@ -20,28 +21,28 @@ import { ToastService } from '../../../../../shared/toast/ToastService'; changeDetection: ChangeDetectionStrategy.OnPush }) export class ApplicationDeploymentComponent { + + _project: Project; @Input('project') set project(project: Project) { this._project = project; if (project.integrations) { - this.filteredIntegrations = project.integrations.filter(p => p.model.deployment); + this.filteredIntegrations = cloneDeep(project.integrations.filter(p => p.model.deployment)); } } get project(): Project { return this._project; } - - filteredIntegrations: Array; - selectedIntegration: ProjectIntegration; - - public loadingBtn = false; - - _project: Project; + @Input() application: Application; + @Input() editMode: boolean; @ViewChild('removeWarning') removeWarningModal: WarningModalComponent; @ViewChild('linkWarning') linkWarningModal: WarningModalComponent; - @Input() application: Application; + filteredIntegrations: Array; + selectedIntegration: ProjectIntegration; + + loadingBtn = false; constructor( private _toast: ToastService, @@ -69,7 +70,14 @@ export class ApplicationDeploymentComponent { this.loadingBtn = false; this._cd.markForCheck(); })) - .subscribe(() => this._toast.success('', this._translate.instant('application_integration_deleted'))); + .subscribe(() => { + if (this.editMode) { + this._toast.info('', this._translate.instant('application_ascode_updated')); + } else { + this._toast.success('', this._translate.instant('application_integration_deleted')); + } + + }); } updateIntegration(pfName: string) { @@ -83,7 +91,14 @@ export class ApplicationDeploymentComponent { this.loadingBtn = false; this._cd.markForCheck(); })) - .subscribe(() => this._toast.success('', this._translate.instant('application_integration_updated'))); + .subscribe(() => { + if (this.editMode) { + this._toast.info('', this._translate.instant('application_ascode_updated')); + } else { + this._toast.success('', this._translate.instant('application_integration_updated')); + } + + }); } addIntegration() { @@ -99,7 +114,11 @@ export class ApplicationDeploymentComponent { })) .subscribe(() => { this.selectedIntegration = null; - this._toast.success('', this._translate.instant('application_integration_added')); + if (this.editMode) { + this._toast.info('', this._translate.instant('application_ascode_updated')); + } else { + this._toast.success('', this._translate.instant('application_integration_added')); + } }); } } diff --git a/ui/src/app/views/application/show/admin/deployment/application.deployment.html b/ui/src/app/views/application/show/admin/deployment/application.deployment.html index 62bedaafbe..8e3ef8141b 100644 --- a/ui/src/app/views/application/show/admin/deployment/application.deployment.html +++ b/ui/src/app/views/application/show/admin/deployment/application.deployment.html @@ -35,7 +35,7 @@
+ [disabled]="!selectedIntegration || loadingBtn">{{ 'btn_add' | translate }}
@@ -81,11 +81,11 @@
-
- +
diff --git a/ui/src/app/views/application/show/admin/repository/application.repo.component.ts b/ui/src/app/views/application/show/admin/repository/application.repo.component.ts index 4a1f7ed4c5..fe96faeeba 100644 --- a/ui/src/app/views/application/show/admin/repository/application.repo.component.ts +++ b/ui/src/app/views/application/show/admin/repository/application.repo.component.ts @@ -24,6 +24,7 @@ export class ApplicationRepositoryComponent implements OnInit { @Input() project: Project; @Input() application: Application; + @Input() editMode: boolean; selectedRepoManager: string; selectedRepo: string; @@ -132,6 +133,13 @@ export class ApplicationRepositoryComponent implements OnInit { this.loadingBtn = false; this._cd.markForCheck(); })) - .subscribe(() => this._toast.success('', this._translate.instant('application_update_ok'))); + .subscribe(() => { + if (this.editMode) { + this._toast.info('', this._translate.instant('application_ascode_updated')); + } else { + this._toast.success('', this._translate.instant('application_update_ok')); + } + + }); } } diff --git a/ui/src/app/views/application/show/admin/repository/application.repo.html b/ui/src/app/views/application/show/admin/repository/application.repo.html index d5c83fa6c6..e0221920d2 100644 --- a/ui/src/app/views/application/show/admin/repository/application.repo.html +++ b/ui/src/app/views/application/show/admin/repository/application.repo.html @@ -1,6 +1,6 @@
-
+
@@ -66,7 +66,7 @@

{{ 'application_repo_strat_title' | translate }}

- +
diff --git a/ui/src/app/views/application/show/application.component.spec.ts b/ui/src/app/views/application/show/application.component.spec.ts index e268b74046..c47493a27b 100644 --- a/ui/src/app/views/application/show/application.component.spec.ts +++ b/ui/src/app/views/application/show/application.component.spec.ts @@ -6,37 +6,39 @@ import { ActivatedRoute, ActivatedRouteSnapshot, Router } from '@angular/router' import { RouterTestingModule } from '@angular/router/testing'; import { TranslateLoader, TranslateModule, TranslateParser, TranslateService } from '@ngx-translate/core'; import { Store } from '@ngxs/store'; +import { Permission } from 'app/model/permission.model'; import { Variable } from 'app/model/variable.model'; import { AuthenticationService } from 'app/service/authentication/authentication.service'; import { UserService } from 'app/service/user/user.service'; import { VariableEvent } from 'app/shared/variable/variable.event.model'; import { AddApplicationVariable, DeleteApplicationVariable, UpdateApplicationVariable } from 'app/store/applications.action'; import { ApplicationStateModel } from 'app/store/applications.state'; +import { ProjectStateModel } from 'app/store/project.state'; import { NgxsStoreModule } from 'app/store/store.module'; import { of } from 'rxjs'; import 'rxjs/add/observable/of'; -import { Application } from '../../../model/application.model'; -import { Project } from '../../../model/project.model'; -import { Usage } from '../../../model/usage.model'; -import { ApplicationService } from '../../../service/application/application.service'; -import { ApplicationStore } from '../../../service/application/application.store'; -import { ApplicationWorkflowService } from '../../../service/application/application.workflow.service'; -import { EnvironmentService } from '../../../service/environment/environment.service'; -import { NavbarService } from '../../../service/navbar/navbar.service'; -import { PipelineService } from '../../../service/pipeline/pipeline.service'; -import { ProjectService } from '../../../service/project/project.service'; -import { ProjectStore } from '../../../service/project/project.store'; +import { Application } from 'app/model/application.model'; +import { Project } from 'app/model/project.model'; +import { Usage } from 'app/model/usage.model'; +import { ApplicationService } from 'app/service/application/application.service'; +import { ApplicationStore } from 'app/service/application/application.store'; +import { ApplicationWorkflowService } from 'app/service/application/application.workflow.service'; +import { EnvironmentService } from 'app/service/environment/environment.service'; +import { NavbarService } from 'app/service/navbar/navbar.service'; +import { PipelineService } from 'app/service/pipeline/pipeline.service'; +import { ProjectService } from 'app/service/project/project.service'; +import { ProjectStore } from 'app/service/project/project.store'; import { MonitoringService, RouterService, ServicesModule, WorkflowRunService, WorkflowStore -} from '../../../service/services.module'; -import { VariableService } from '../../../service/variable/variable.service'; -import { WorkflowService } from '../../../service/workflow/workflow.service'; -import { SharedModule } from '../../../shared/shared.module'; -import { ToastService } from '../../../shared/toast/ToastService'; +} from 'app/service/services.module'; +import { VariableService } from 'app/service/variable/variable.service'; +import { WorkflowService } from 'app/service/workflow/workflow.service'; +import { SharedModule } from 'app/shared/shared.module'; +import { ToastService } from 'app/shared/toast/ToastService'; import { ApplicationModule } from '../application.module'; import { ApplicationShowComponent } from './application.component'; @@ -46,7 +48,6 @@ describe('CDS: Application', () => { let appStore: ApplicationStore; let store: Store; let router: Router; - let prjStore: ProjectStore; beforeEach(() => { TestBed.configureTestingModule({ @@ -92,7 +93,6 @@ describe('CDS: Application', () => { appStore = injector.get(ApplicationStore); store = injector.get(Store); router = injector.get(Router); - prjStore = injector.get(ProjectStore); }); afterEach(() => { @@ -100,19 +100,29 @@ describe('CDS: Application', () => { appStore = undefined; store = undefined; router = undefined; - prjStore = undefined; }); it('Load component + load application', fakeAsync(() => { spyOn(appStore, 'updateRecentApplication'); + let callOrder = 0; spyOn(store, 'select').and.callFake(() => { + if (callOrder === 0) { + callOrder++; + let state = new ProjectStateModel(); + let p = new Project(); + p.permissions = new Permission(); + p.permissions.writable = true; + state.project = p; + return of(state); + } let state = new ApplicationStateModel(); let app: Application = new Application(); app.name = 'app1'; app.usage = new Usage(); state.application = app; + state.editMode = false; return of(state); }); @@ -150,7 +160,17 @@ describe('CDS: Application', () => { it('should run add variable', fakeAsync(() => { let call = 0; + let callOrder = 0; spyOn(store, 'select').and.callFake(() => { + if (callOrder === 0) { + callOrder++; + let state = new ProjectStateModel(); + let p = new Project(); + p.permissions = new Permission(); + p.permissions.writable = true; + state.project = p; + return of(state); + } let state = new ApplicationStateModel(); let app: Application = new Application(); app.name = 'app1'; @@ -185,7 +205,17 @@ describe('CDS: Application', () => { })); it('should run update variable', fakeAsync(() => { + let callOrder = 0; spyOn(store, 'select').and.callFake(() => { + if (callOrder === 0) { + callOrder++; + let state = new ProjectStateModel(); + let p = new Project(); + p.permissions = new Permission(); + p.permissions.writable = true; + state.project = p; + return of(state); + } let state = new ApplicationStateModel(); let app: Application = new Application(); app.name = 'app1'; @@ -221,7 +251,17 @@ describe('CDS: Application', () => { })); it('should run remove variable', fakeAsync(() => { + let callOrder = 0; spyOn(store, 'select').and.callFake(() => { + if (callOrder === 0) { + callOrder++; + let state = new ProjectStateModel(); + let p = new Project(); + p.permissions = new Permission(); + p.permissions.writable = true; + state.project = p; + return of(state); + } let state = new ApplicationStateModel(); let app: Application = new Application(); app.name = 'app1'; diff --git a/ui/src/app/views/application/show/application.component.ts b/ui/src/app/views/application/show/application.component.ts index 158eccf411..5e3e39bf8a 100644 --- a/ui/src/app/views/application/show/application.component.ts +++ b/ui/src/app/views/application/show/application.component.ts @@ -9,13 +9,14 @@ import { Project } from 'app/model/project.model'; import { AuthentifiedUser } from 'app/model/user.model'; import { Workflow } from 'app/model/workflow.model'; import { ApplicationStore } from 'app/service/application/application.store'; +import { AsCodeSaveModalComponent } from 'app/shared/ascode/save-modal/ascode.save-modal.component'; import { AutoUnsubscribe } from 'app/shared/decorator/autoUnsubscribe'; import { WarningModalComponent } from 'app/shared/modal/warning/warning.component'; import { ToastService } from 'app/shared/toast/ToastService'; import { VariableEvent } from 'app/shared/variable/variable.event.model'; import { CDSWebWorker } from 'app/shared/worker/web.worker'; import * as applicationsActions from 'app/store/applications.action'; -import { ClearCacheApplication } from 'app/store/applications.action'; +import { CancelApplicationEdition, ClearCacheApplication } from 'app/store/applications.action'; import { ApplicationsState, ApplicationStateModel } from 'app/store/applications.state'; import { AuthenticationState } from 'app/store/authentication.state'; import { ProjectState, ProjectStateModel } from 'app/store/project.state'; @@ -40,13 +41,14 @@ export class ApplicationShowComponent implements OnInit, OnDestroy { // Project & Application data urlAppName: string; project: Project; + readOnlyApplication: Application; application: Application; + editMode: boolean; + readOnly: boolean; // Subscription - applicationSubscription: Subscription; projectSubscription: Subscription; _routeParamsSub: Subscription; - _routeDataSub: Subscription; _queryParamsSub: Subscription; worker: CDSWebWorker; @@ -55,6 +57,8 @@ export class ApplicationShowComponent implements OnInit, OnDestroy { @ViewChild('varWarning') private varWarningModal: WarningModalComponent; + @ViewChild('updateEditMode') + asCodeSaveModal: AsCodeSaveModalComponent; // queryparam for breadcrum workflowName: string; @@ -110,7 +114,17 @@ export class ApplicationShowComponent implements OnInit, OnDestroy { .pipe(filter((s: ApplicationStateModel) => s.application != null && s.application.name === this.urlAppName)) .subscribe((s: ApplicationStateModel) => { this.readyApp = true; - this.application = cloneDeep(s.application); + this.readOnly = (s.application.workflow_ascode_holder && !!s.application.workflow_ascode_holder.from_template) || + !this.project.permissions.writable; + this.editMode = s.editMode; + this.readOnlyApplication = s.application; + if (this.editMode) { + this.application = cloneDeep(s.editApplication); + } else { + this.application = cloneDeep(s.application); + } + + if (this.application.usage) { this.workflows = this.application.usage.workflows || []; this.environments = this.application.usage.environments || []; @@ -160,7 +174,14 @@ export class ApplicationShowComponent implements OnInit, OnDestroy { event.variable.updating = false; this.varFormLoading = false; this._cd.markForCheck(); - })).subscribe(() => this._toast.success('', this._translate.instant('variable_added'))); + })).subscribe(() => { + if (this.editMode) { + this._toast.info('', this._translate.instant('application_ascode_updated')); + } else { + this._toast.success('', this._translate.instant('variable_added')); + } + + }); break; case 'update': this._store.dispatch(new applicationsActions.UpdateApplicationVariable({ @@ -172,7 +193,13 @@ export class ApplicationShowComponent implements OnInit, OnDestroy { event.variable.updating = false; this._cd.markForCheck(); })) - .subscribe(() => this._toast.success('', this._translate.instant('variable_updated'))); + .subscribe(() => { + if (this.editMode) { + this._toast.info('', this._translate.instant('application_ascode_updated')); + } else { + this._toast.success('', this._translate.instant('variable_updated')); + } + }); break; case 'delete': this._store.dispatch(new applicationsActions.DeleteApplicationVariable({ @@ -180,12 +207,31 @@ export class ApplicationShowComponent implements OnInit, OnDestroy { applicationName: this.application.name, variable: event.variable })).pipe(finalize(() => this._cd.markForCheck())) - .subscribe(() => this._toast.success('', this._translate.instant('variable_deleted'))); + .subscribe(() => { + if (this.editMode) { + this._toast.info('', this._translate.instant('application_ascode_updated')); + } else { + this._toast.success('', this._translate.instant('variable_deleted')); + } + }); break; } } } + cancelApplication(): void { + if (this.editMode) { + this._store.dispatch(new CancelApplicationEdition()); + } + } + + saveEditMode(): void { + if (this.editMode && this.application.from_repository && this.asCodeSaveModal) { + // show modal to save as code + this.asCodeSaveModal.show(this.application, 'application'); + } + } + ngOnDestroy(): void { this._store.dispatch(new ClearCacheApplication()); } diff --git a/ui/src/app/views/application/show/application.html b/ui/src/app/views/application/show/application.html index 4c35b838bd..43e441f090 100644 --- a/ui/src/app/views/application/show/application.html +++ b/ui/src/app/views/application/show/application.html @@ -1,7 +1,7 @@
-
{{ 'common_advanced' | translate }} +
+ + +
+
+ +
@@ -68,16 +76,16 @@
+ *ngIf="!readOnly">

{{ 'application_variable_form_title' | translate}}

{{ 'application_variable_list_title' | translate}}

+ [applicationName]="readOnlyApplication.name"> @@ -98,11 +106,11 @@

{{ 'application_variable_list_title' | translate}}

- +
- +
@@ -114,3 +122,8 @@

{{ 'application_variable_list_title' | translate}}

Loading application...
+ + + + diff --git a/ui/src/app/views/application/show/application.scss b/ui/src/app/views/application/show/application.scss index e341a7256c..f33cdef758 100644 --- a/ui/src/app/views/application/show/application.scss +++ b/ui/src/app/views/application/show/application.scss @@ -13,3 +13,11 @@ height: 100%; } +sm-menu { + .ui.buttons { + position: absolute; + right: 15px; + margin-top: 2px; + } +} + diff --git a/ui/src/app/views/application/show/keys/application.keys.component.ts b/ui/src/app/views/application/show/keys/application.keys.component.ts index 81e2a8811b..4c8ca4e696 100644 --- a/ui/src/app/views/application/show/keys/application.keys.component.ts +++ b/ui/src/app/views/application/show/keys/application.keys.component.ts @@ -18,6 +18,8 @@ export class ApplicationKeysComponent { @Input() project: Project; @Input() application: Application; + @Input() editMode: boolean; + @Input() readOnly: boolean; loading = false; @@ -42,7 +44,13 @@ export class ApplicationKeysComponent { this.loading = false; this._cd.markForCheck(); })) - .subscribe(() => this._toast.success('', this._translate.instant('keys_added'))); + .subscribe(() => { + if (this.editMode) { + this._toast.info('', this._translate.instant('application_ascode_updated')); + } else { + this._toast.success('', this._translate.instant('keys_added')); + } + }); break; case 'delete': this.loading = true; @@ -54,7 +62,13 @@ export class ApplicationKeysComponent { this.loading = false; this._cd.markForCheck(); })) - .subscribe(() => this._toast.success('', this._translate.instant('keys_removed'))); + .subscribe(() => { + if (this.editMode) { + this._toast.info('', this._translate.instant('application_ascode_updated')); + } else { + this._toast.success('', this._translate.instant('keys_removed')); + } + }); } } } diff --git a/ui/src/app/views/application/show/keys/application.keys.html b/ui/src/app/views/application/show/keys/application.keys.html index e4cc44470a..c3013b9a9d 100644 --- a/ui/src/app/views/application/show/keys/application.keys.html +++ b/ui/src/app/views/application/show/keys/application.keys.html @@ -7,10 +7,10 @@

{{ 'keys_list_title' | translate}}

+ [edit]="!readOnly" (deleteEvent)="manageKeyEvent($event)"> - +

{{ 'keys_add_title' | translate }}

diff --git a/ui/src/app/views/environment/show/environment.show.html b/ui/src/app/views/environment/show/environment.show.html index 0166798e27..dff495e76f 100644 --- a/ui/src/app/views/environment/show/environment.show.html +++ b/ui/src/app/views/environment/show/environment.show.html @@ -55,7 +55,7 @@

{{ 'environment_variable_list_title' | translate}}

+ [environment]="environment.name">
diff --git a/ui/src/app/views/home/heatmap/home.heatmap.component.ts b/ui/src/app/views/home/heatmap/home.heatmap.component.ts index fec2dc2c18..c996556f59 100644 --- a/ui/src/app/views/home/heatmap/home.heatmap.component.ts +++ b/ui/src/app/views/home/heatmap/home.heatmap.component.ts @@ -19,7 +19,6 @@ import { ToolbarComponent } from './toolbar/toolbar.component'; }) @AutoUnsubscribe() export class HomeHeatmapComponent implements AfterViewInit { -; loading = true; events: Array; diff --git a/ui/src/app/views/navbar/navbar.component.ts b/ui/src/app/views/navbar/navbar.component.ts index ae01f9aa4c..de35377649 100644 --- a/ui/src/app/views/navbar/navbar.component.ts +++ b/ui/src/app/views/navbar/navbar.component.ts @@ -361,35 +361,6 @@ export class NavbarComponent implements OnInit { }); } - navigateToResult(result: NavbarSearchItem) { - if (!result) { - return; - } - switch (result.type) { - case 'workflow': - this.navigateToWorkflow(result.projectKey, result.value.split('/', 2)[1]); - break; - case 'application': - this.navigateToApplication(result.projectKey, result.value.split('/', 2)[1]); - break; - default: - this.navigateToProject(result.projectKey); - } - } - - searchItem(list: Array, query: string): boolean | Array { - let queryLowerCase = query.toLowerCase(); - let found: Array = []; - for (let elt of list) { - if (query === elt.projectKey) { - found.push(elt); - } else if (elt.title && elt.title.toLowerCase().indexOf(queryLowerCase) !== -1) { - found.push(elt); - } - } - return found; - } - /** * Navigate to the selected project. * @param key Project unique key get by the event diff --git a/ui/src/app/views/pipeline/add/pipeline.add.component.spec.ts b/ui/src/app/views/pipeline/add/pipeline.add.component.spec.ts index 5f096320e0..b10ed4fc4b 100644 --- a/ui/src/app/views/pipeline/add/pipeline.add.component.spec.ts +++ b/ui/src/app/views/pipeline/add/pipeline.add.component.spec.ts @@ -34,7 +34,6 @@ describe('CDS: Pipeline Add Component', () => { let injector: Injector; let store: Store; - let router: Router; beforeEach(() => { TestBed.configureTestingModule({ @@ -74,13 +73,11 @@ describe('CDS: Pipeline Add Component', () => { injector = getTestBed(); store = injector.get(Store); - router = injector.get(Router); }); afterEach(() => { injector = undefined; store = undefined; - router = undefined; }); it('should create an empty pipeline', fakeAsync(() => { diff --git a/ui/src/app/views/pipeline/show/pipeline.show.component.ts b/ui/src/app/views/pipeline/show/pipeline.show.component.ts index e3ffcf4df3..42ccc62640 100644 --- a/ui/src/app/views/pipeline/show/pipeline.show.component.ts +++ b/ui/src/app/views/pipeline/show/pipeline.show.component.ts @@ -228,7 +228,13 @@ export class PipelineShowComponent implements OnInit { })).pipe(finalize(() => { this.paramFormLoading = false; this._cd.markForCheck(); - })).subscribe(() => this._toast.success('', this._translate.instant('parameter_added'))); + })).subscribe(() => { + if (this.editMode) { + this._toast.info('', this._translate.instant('pipeline_ascode_updated')); + } else { + this._toast.success('', this._translate.instant('parameter_added')) + } + }); break; case 'update': this._store.dispatch(new UpdatePipelineParameter({ @@ -236,14 +242,26 @@ export class PipelineShowComponent implements OnInit { pipelineName: this.pipeline.name, parameterName: event.parameter.previousName || event.parameter.name, parameter: event.parameter - })).subscribe(() => this._toast.success('', this._translate.instant('parameter_updated'))); + })).subscribe(() => { + if (this.editMode) { + this._toast.info('', this._translate.instant('pipeline_ascode_updated')); + } else { + this._toast.success('', this._translate.instant('parameter_updated')); + } + }); break; case 'delete': this._store.dispatch(new DeletePipelineParameter({ projectKey: this.project.key, pipelineName: this.pipeline.name, parameter: event.parameter - })).subscribe(() => this._toast.success('', this._translate.instant('parameter_deleted'))); + })).subscribe(() => { + if (this.editMode) { + this._toast.info('', this._translate.instant('pipeline_ascode_updated')); + } else { + this._toast.success('', this._translate.instant('parameter_deleted')) + } + }); break; } } diff --git a/ui/src/app/views/project/show/admin/project.admin.component.ts b/ui/src/app/views/project/show/admin/project.admin.component.ts index 867bd70153..a24dc77351 100644 --- a/ui/src/app/views/project/show/admin/project.admin.component.ts +++ b/ui/src/app/views/project/show/admin/project.admin.component.ts @@ -32,7 +32,7 @@ export class ProjectAdminComponent implements OnInit { private _router: Router, private _store: Store, private _cd: ChangeDetectorRef - ) { }; + ) { } ngOnInit(): void { if (!this.project.permissions.writable) { @@ -53,7 +53,7 @@ export class ProjectAdminComponent implements OnInit { })) .subscribe(() => this._toast.success('', this._translate.instant('project_update_msg_ok'))); } - }; + } deleteProject(): void { this.loading = true; diff --git a/ui/src/app/views/project/show/admin/repomanager/list/project.repomanager.list.spec.ts b/ui/src/app/views/project/show/admin/repomanager/list/project.repomanager.list.spec.ts index 1c0173d7a8..2e818094c6 100644 --- a/ui/src/app/views/project/show/admin/repomanager/list/project.repomanager.list.spec.ts +++ b/ui/src/app/views/project/show/admin/repomanager/list/project.repomanager.list.spec.ts @@ -32,7 +32,6 @@ import { ProjectRepoManagerComponent } from './project.repomanager.list.componen describe('CDS: Project RepoManager List Component', () => { let injector: Injector; - let projectStore: ProjectStore; beforeEach(() => { TestBed.configureTestingModule({ @@ -72,13 +71,11 @@ describe('CDS: Project RepoManager List Component', () => { ] }); injector = getTestBed(); - projectStore = injector.get(ProjectStore); }); afterEach(() => { injector = undefined; - projectStore = undefined; }); diff --git a/ui/src/app/views/workflow/run/node/artifact/artifact.list.spec.ts b/ui/src/app/views/workflow/run/node/artifact/artifact.list.spec.ts index 0c165d9236..fa5ceca4a5 100644 --- a/ui/src/app/views/workflow/run/node/artifact/artifact.list.spec.ts +++ b/ui/src/app/views/workflow/run/node/artifact/artifact.list.spec.ts @@ -1,7 +1,6 @@ /* tslint:disable:no-unused-variable */ -import {TestBed, fakeAsync, getTestBed} from '@angular/core/testing'; +import {TestBed, fakeAsync} from '@angular/core/testing'; import {RouterTestingModule} from '@angular/router/testing'; -import {Injector} from '@angular/core'; import {TranslateService, TranslateLoader, TranslateParser, TranslateModule} from '@ngx-translate/core'; import {SharedModule} from '../../../../../shared/shared.module'; import {WorkflowRunArtifactListComponent} from './artifact.list.component'; @@ -9,8 +8,6 @@ import {WorkflowModule} from '../../../workflow.module'; describe('CDS: Artifact List', () => { - let injector: Injector; - beforeEach(() => { TestBed.configureTestingModule({ declarations: [], @@ -26,12 +23,6 @@ describe('CDS: Artifact List', () => { SharedModule ] }); - - injector = getTestBed(); - }); - - afterEach(() => { - injector = undefined; }); it('should load component', fakeAsync(() => { diff --git a/ui/src/app/views/workflow/run/node/pipeline/spawninfo/spawninfo.component.ts b/ui/src/app/views/workflow/run/node/pipeline/spawninfo/spawninfo.component.ts index bcef584ad4..00bc2aefba 100644 --- a/ui/src/app/views/workflow/run/node/pipeline/spawninfo/spawninfo.component.ts +++ b/ui/src/app/views/workflow/run/node/pipeline/spawninfo/spawninfo.component.ts @@ -44,7 +44,7 @@ export class WorkflowRunJobSpawnInfoComponent implements OnDestroy, OnInit { currentJobID: number; jobStatus: string; - spawnInfos: String; + spawnInfos: string; variables: Array; @Output() displayServicesLogsChange = new EventEmitter(); diff --git a/ui/src/app/views/workflow/run/node/test/tests.component.spec.ts b/ui/src/app/views/workflow/run/node/test/tests.component.spec.ts index cc459235b2..d55fc6b701 100644 --- a/ui/src/app/views/workflow/run/node/test/tests.component.spec.ts +++ b/ui/src/app/views/workflow/run/node/test/tests.component.spec.ts @@ -1,7 +1,6 @@ /* tslint:disable:no-unused-variable */ -import {TestBed, fakeAsync, getTestBed} from '@angular/core/testing'; +import {TestBed, fakeAsync} from '@angular/core/testing'; import {RouterTestingModule} from '@angular/router/testing'; -import {Injector} from '@angular/core'; import {TranslateService, TranslateLoader, TranslateParser, TranslateModule} from '@ngx-translate/core'; import {WorkflowRunTestsResultComponent} from './tests.component'; import {WorkflowModule} from '../../../workflow.module'; @@ -9,8 +8,6 @@ import {SharedModule} from '../../../../../shared/shared.module'; describe('CDS: Test Report component', () => { - let injector: Injector; - beforeEach(() => { TestBed.configureTestingModule({ declarations: [], @@ -26,12 +23,6 @@ describe('CDS: Test Report component', () => { SharedModule ] }); - - injector = getTestBed(); - }); - - afterEach(() => { - injector = undefined; }); it('should load component', fakeAsync(() => { diff --git a/ui/src/app/views/workflow/show/admin/workflow.admin.component.ts b/ui/src/app/views/workflow/show/admin/workflow.admin.component.ts index 96f729dfaa..39b678c2d7 100644 --- a/ui/src/app/views/workflow/show/admin/workflow.admin.component.ts +++ b/ui/src/app/views/workflow/show/admin/workflow.admin.component.ts @@ -43,8 +43,8 @@ export class WorkflowAdminComponent implements OnInit, OnDestroy { this.purgeTag = this._workflow.purge_tags[0]; } } - }; - get workflow() { return this._workflow }; + } + get workflow() { return this._workflow } @Input() editMode: boolean; diff --git a/ui/src/app/views/workflow/show/notification/form/workflow.notification.form.component.ts b/ui/src/app/views/workflow/show/notification/form/workflow.notification.form.component.ts index a294e28bf3..6e4ec563d5 100644 --- a/ui/src/app/views/workflow/show/notification/form/workflow.notification.form.component.ts +++ b/ui/src/app/views/workflow/show/notification/form/workflow.notification.form.component.ts @@ -89,7 +89,7 @@ export class WorkflowNotificationFormComponent implements OnInit { initNotif(): void { if (this.nodes && this.notification && !this.notification.id) { - (this.notification).source_node_ref = this.nodes.map(n => { + this.notification.source_node_ref = this.nodes.map(n => { return n.name; }); } diff --git a/ui/src/app/views/workflow/workflow.component.ts b/ui/src/app/views/workflow/workflow.component.ts index f4e71bb266..83db4159b5 100644 --- a/ui/src/app/views/workflow/workflow.component.ts +++ b/ui/src/app/views/workflow/workflow.component.ts @@ -2,14 +2,12 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit, - QueryList, ViewChild, - ViewChildren } from '@angular/core'; import { ActivatedRoute, NavigationStart, Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; import { Select, Store } from '@ngxs/store'; -import { SuiPopup, SuiPopupController, SuiPopupTemplateController } from '@richardlt/ng2-semantic-ui'; +import { SuiPopup } from '@richardlt/ng2-semantic-ui'; import { Project } from 'app/model/project.model'; import { Workflow } from 'app/model/workflow.model'; import { WorkflowCoreService } from 'app/service/workflow/workflow.core.service'; diff --git a/ui/src/assets/i18n/en.json b/ui/src/assets/i18n/en.json index 1268667737..0a26c8b0b8 100644 --- a/ui/src/assets/i18n/en.json +++ b/ui/src/assets/i18n/en.json @@ -55,6 +55,7 @@ "action_btn_init_from_job": "Init a new action from job", "action_help_add_from": "Current action was initialized with data from \"{{jobName}}\" job of \"{{pipelineName}}\" pipeline in \"{{projectKey}}\" project. The data will not be stored until you save the action.", "admin_queue_title": "Current CDS jobs queue", + "application_ascode_updated": "Draft updated", "application_home_history": "History", "application_home_repo": "GIT repository", "application_home_no_repo": "Link a repository", @@ -132,6 +133,7 @@ "btn_run_workflow": "Run workflow", "btn_validate": "Validate", "btn_save": "Save", + "btn_save_application": "Save application", "btn_save_pipeline": "Save pipeline", "btn_save_workflow": "Save workflow", "btn_next": "Next", diff --git a/ui/src/assets/i18n/fr.json b/ui/src/assets/i18n/fr.json index 5795e8991c..c2903bafa2 100644 --- a/ui/src/assets/i18n/fr.json +++ b/ui/src/assets/i18n/fr.json @@ -55,6 +55,7 @@ "action_usage_warning_action": "L'action parente ne sera pas en mesure d'exécuter cette action car elles ne font pas parties du même groupe.", "action_usage_warning_pipeline": "Ce pipeline ne sera pas en mesure d'exécuter cette action car la permission pour le groupe n'existe pas.", "admin_queue_title": "File d'attente", + "application_ascode_updated": "Brouillon mis à jour", "application_create": "Créer une application", "application_created": "Application créée", "application_delete_description": "Une fois l'application supprimée, il n'y a pas de retour possible.", @@ -153,6 +154,7 @@ "btn_run_workflow": "Lancer le workflow", "btn_run": "Lancer le pipeline", "btn_save": "Sauvegarder", + "btn_save_application": "Sauvegarder l'application", "btn_save_pipeline": "Sauvegarder le pipeline", "btn_save_workflow": "Sauvegarder le workflow", "btn_synchronize": "Synchronizer",