diff --git a/docs/static/images/tutorials/npm-audit-parser/click_edit.png b/docs/static/images/tutorials/npm-audit-parser/click_edit.png index f88238d0df..e69de29bb2 100644 Binary files a/docs/static/images/tutorials/npm-audit-parser/click_edit.png and b/docs/static/images/tutorials/npm-audit-parser/click_edit.png differ diff --git a/engine/api/event/publish_workflow_template.go b/engine/api/event/publish_workflow_template.go index 9004fda9a5..abedea9d90 100644 --- a/engine/api/event/publish_workflow_template.go +++ b/engine/api/event/publish_workflow_template.go @@ -30,10 +30,11 @@ func PublishWorkflowTemplateAdd(wt sdk.WorkflowTemplate, u *sdk.User) { } // PublishWorkflowTemplateUpdate publishes an event for the update of the given workflow template. -func PublishWorkflowTemplateUpdate(new, old sdk.WorkflowTemplate, u *sdk.User) { +func PublishWorkflowTemplateUpdate(old, new sdk.WorkflowTemplate, changeMessage string, u *sdk.User) { publishWorkflowTemplateEvent(sdk.EventWorkflowTemplateUpdate{ - NewWorkflowTemplate: new, OldWorkflowTemplate: old, + NewWorkflowTemplate: new, + ChangeMessage: changeMessage, }, u) } @@ -43,9 +44,9 @@ func PublishWorkflowTemplateInstanceAdd(wti sdk.WorkflowTemplateInstance, u *sdk } // PublishWorkflowTemplateInstanceUpdate publishes an event for the update of the given workflow template instance. -func PublishWorkflowTemplateInstanceUpdate(new, old sdk.WorkflowTemplateInstance, u *sdk.User) { +func PublishWorkflowTemplateInstanceUpdate(old, new sdk.WorkflowTemplateInstance, u *sdk.User) { publishWorkflowTemplateEvent(sdk.EventWorkflowTemplateInstanceUpdate{ - NewWorkflowTemplateInstance: new, OldWorkflowTemplateInstance: old, + NewWorkflowTemplateInstance: new, }, u) } diff --git a/engine/api/templates.go b/engine/api/templates.go index 8e5dd9c6ac..89d8ec4edb 100644 --- a/engine/api/templates.go +++ b/engine/api/templates.go @@ -8,6 +8,7 @@ import ( "io" "io/ioutil" "net/http" + "strconv" "github.com/gorilla/mux" yaml "gopkg.in/yaml.v2" @@ -235,7 +236,7 @@ func (api *API) putTemplateHandler() service.Handler { return err } - event.PublishWorkflowTemplateUpdate(*old, new, u) + event.PublishWorkflowTemplateUpdate(*old, new, data.ChangeMessage, u) if err := group.AggregateOnWorkflowTemplate(api.mustDB(), &new); err != nil { return err @@ -564,8 +565,17 @@ func (api *API) getTemplateAuditsHandler() service.Handler { } t := getWorkflowTemplate(ctx) - as, err := workflowtemplate.GetAuditsByTemplateIDsAndEventTypes(api.mustDB(), - []int64{t.ID}, []string{"WorkflowTemplateAdd", "WorkflowTemplateUpdate"}) + since := r.FormValue("sinceVersion") + var version int64 + if since != "" { + version, err = strconv.ParseInt(since, 10, 64) + if err != nil || version < 0 { + return sdk.NewError(sdk.ErrWrongRequest, err) + } + } + + as, err := workflowtemplate.GetAuditsByTemplateIDsAndEventTypesAndVersionGTE(api.mustDB(), + []int64{t.ID}, []string{"WorkflowTemplateAdd", "WorkflowTemplateUpdate"}, version) if err != nil { return err } diff --git a/engine/api/workflowtemplate/aggregate.go b/engine/api/workflowtemplate/aggregate.go index aac18fc0cc..b9e23058bd 100644 --- a/engine/api/workflowtemplate/aggregate.go +++ b/engine/api/workflowtemplate/aggregate.go @@ -8,7 +8,8 @@ import ( // AggregateAuditsOnWorkflowTemplate set audits for each workflow template. func AggregateAuditsOnWorkflowTemplate(db gorp.SqlExecutor, wts ...*sdk.WorkflowTemplate) error { - as, err := GetAuditsByTemplateIDsAndEventTypes(db, sdk.WorkflowTemplatesToIDs(wts), []string{"WorkflowTemplateAdd", "WorkflowTemplateUpdate"}) + as, err := GetAuditsByTemplateIDsAndEventTypesAndVersionGTE(db, sdk.WorkflowTemplatesToIDs(wts), + []string{"WorkflowTemplateAdd", "WorkflowTemplateUpdate"}, 0) if err != nil { return err } @@ -21,11 +22,11 @@ func AggregateAuditsOnWorkflowTemplate(db gorp.SqlExecutor, wts ...*sdk.Workflow m[a.WorkflowTemplateID] = append(m[a.WorkflowTemplateID], a) } - // assume that audits are sorted by creation date with GetAudits + // assume that audits are sorted by creation date desc by GetAudits for _, wt := range wts { if as, ok := m[wt.ID]; ok { - wt.FirstAudit = &as[0] - wt.LastAudit = &as[len(as)-1] + wt.FirstAudit = &as[len(as)-1] + wt.LastAudit = &as[0] } } diff --git a/engine/api/workflowtemplate/aggregate_test.go b/engine/api/workflowtemplate/aggregate_test.go index bc7323320a..bb776a6faf 100644 --- a/engine/api/workflowtemplate/aggregate_test.go +++ b/engine/api/workflowtemplate/aggregate_test.go @@ -16,14 +16,8 @@ func TestAggregateAuditsOnWorkflowTemplate(t *testing.T) { awts := i.(*[]sdk.AuditWorkflowTemplate) *awts = append(*awts, sdk.AuditWorkflowTemplate{ AuditCommon: sdk.AuditCommon{ - ID: 1, - EventType: "EventWorkflowTemplateAdd", - }, - WorkflowTemplateID: 1, - }, sdk.AuditWorkflowTemplate{ - AuditCommon: sdk.AuditCommon{ - ID: 2, - EventType: "EventWorkflowTemplateAdd", + ID: 4, + EventType: "EventWorkflowTemplateUpdate", }, WorkflowTemplateID: 2, }, sdk.AuditWorkflowTemplate{ @@ -34,10 +28,16 @@ func TestAggregateAuditsOnWorkflowTemplate(t *testing.T) { WorkflowTemplateID: 1, }, sdk.AuditWorkflowTemplate{ AuditCommon: sdk.AuditCommon{ - ID: 4, - EventType: "EventWorkflowTemplateUpdate", + ID: 2, + EventType: "EventWorkflowTemplateAdd", }, WorkflowTemplateID: 2, + }, sdk.AuditWorkflowTemplate{ + AuditCommon: sdk.AuditCommon{ + ID: 1, + EventType: "EventWorkflowTemplateAdd", + }, + WorkflowTemplateID: 1, }) } diff --git a/engine/api/workflowtemplate/audit.go b/engine/api/workflowtemplate/audit.go index 1804c976e5..849389d5dc 100644 --- a/engine/api/workflowtemplate/audit.go +++ b/engine/api/workflowtemplate/audit.go @@ -83,12 +83,12 @@ func (a updateWorkflowTemplateAudit) Compute(db gorp.SqlExecutor, e sdk.Event) e return sdk.WrapError(err, "Unable to decode payload") } - before, err := json.Marshal(wtEvent.NewWorkflowTemplate) + before, err := json.Marshal(wtEvent.OldWorkflowTemplate) if err != nil { return sdk.WrapError(err, "Unable to marshal workflow template") } - after, err := json.Marshal(wtEvent.OldWorkflowTemplate) + after, err := json.Marshal(wtEvent.NewWorkflowTemplate) if err != nil { return sdk.WrapError(err, "Unable to marshal workflow template") } @@ -103,6 +103,7 @@ func (a updateWorkflowTemplateAudit) Compute(db gorp.SqlExecutor, e sdk.Event) e DataType: "json", }, WorkflowTemplateID: wtEvent.NewWorkflowTemplate.ID, + ChangeMessage: wtEvent.ChangeMessage, }) } @@ -139,12 +140,12 @@ func (a updateWorkflowTemplateInstanceAudit) Compute(db gorp.SqlExecutor, e sdk. return sdk.WrapError(err, "Unable to decode payload") } - before, err := json.Marshal(wtEvent.NewWorkflowTemplateInstance) + before, err := json.Marshal(wtEvent.OldWorkflowTemplateInstance) if err != nil { return sdk.WrapError(err, "Unable to marshal workflow template instance") } - after, err := json.Marshal(wtEvent.OldWorkflowTemplateInstance) + after, err := json.Marshal(wtEvent.NewWorkflowTemplateInstance) if err != nil { return sdk.WrapError(err, "Unable to marshal workflow template instance") } diff --git a/engine/api/workflowtemplate/dao.go b/engine/api/workflowtemplate/dao.go index b025dd9945..847c9affcf 100644 --- a/engine/api/workflowtemplate/dao.go +++ b/engine/api/workflowtemplate/dao.go @@ -117,16 +117,17 @@ func InsertAudit(db gorp.SqlExecutor, awt *sdk.AuditWorkflowTemplate) error { return sdk.WrapError(gorpmapping.Insert(db, awt), "Unable to insert audit for workflow template %d", awt.WorkflowTemplateID) } -// GetAuditsByTemplateIDsAndEventTypes returns all workflow template audits by template ids and event types. -func GetAuditsByTemplateIDsAndEventTypes(db gorp.SqlExecutor, templateIDs []int64, eventTypes []string) ([]sdk.AuditWorkflowTemplate, error) { +// GetAuditsByTemplateIDsAndEventTypesAndVersionGTE returns all workflow template audits by template ids, event types and version greater or equal. +func GetAuditsByTemplateIDsAndEventTypesAndVersionGTE(db gorp.SqlExecutor, templateIDs []int64, eventTypes []string, version int64) ([]sdk.AuditWorkflowTemplate, error) { awts := []sdk.AuditWorkflowTemplate{} if _, err := db.Select(&awts, `SELECT * FROM workflow_template_audit WHERE workflow_template_id = ANY(string_to_array($1, ',')::int[]) AND event_type = ANY(string_to_array($2, ',')::text[]) - ORDER BY created ASC`, - gorpmapping.IDsToQueryString(templateIDs), strings.Join(eventTypes, ","), + AND (data_after->>'version')::int >= $3 + ORDER BY created DESC`, + gorpmapping.IDsToQueryString(templateIDs), strings.Join(eventTypes, ","), version, ); err != nil { return nil, sdk.WrapError(err, "Cannot get workflow template audits") } diff --git a/engine/api/workflowtemplate/import.go b/engine/api/workflowtemplate/import.go index 2b0997bb02..4dc6f2a723 100644 --- a/engine/api/workflowtemplate/import.go +++ b/engine/api/workflowtemplate/import.go @@ -112,7 +112,7 @@ func Push(db gorp.SqlExecutor, u *sdk.User, tr *tar.Reader) ([]sdk.Message, *sdk return nil, nil, err } - event.PublishWorkflowTemplateUpdate(*old, new, u) + event.PublishWorkflowTemplateUpdate(*old, new, "", u) return []sdk.Message{sdk.NewMessage(sdk.MsgWorkflowTemplateImportedUpdated, grp.Name, new.Slug)}, &new, nil } diff --git a/engine/sql/145_template_audit_json.sql b/engine/sql/145_template_audit_json.sql new file mode 100644 index 0000000000..985cc4a035 --- /dev/null +++ b/engine/sql/145_template_audit_json.sql @@ -0,0 +1,11 @@ +-- +migrate Up + +ALTER TABLE workflow_template_audit ALTER COLUMN data_before TYPE JSONB USING data_after::JSONB; +ALTER TABLE workflow_template_audit ALTER COLUMN data_after TYPE JSONB USING data_after::JSONB; +ALTER TABLE workflow_template_audit ADD COLUMN change_message TEXT NOT NULL DEFAULT ''; + +-- +migrate Down + +ALTER TABLE workflow_template_audit ALTER COLUMN data_before TYPE TEXT USING data_after::TEXT; +ALTER TABLE workflow_template_audit ALTER COLUMN data_after TYPE TEXT USING data_after::TEXT; +ALTER TABLE workflow_template_audit DROP COLUMN change_message; diff --git a/sdk/audit.go b/sdk/audit.go index 1dfbd5dfc1..e1ab464f7a 100644 --- a/sdk/audit.go +++ b/sdk/audit.go @@ -39,7 +39,8 @@ type Audit interface { // AuditWorkflowTemplate represents an audit data on a workflow template. type AuditWorkflowTemplate struct { AuditCommon - WorkflowTemplateID int64 `json:"workflow_template_id" db:"workflow_template_id"` + WorkflowTemplateID int64 `json:"workflow_template_id" db:"workflow_template_id"` + ChangeMessage string `json:"change_message,omitempty" db:"change_message"` } // AuditWorkflowTemplateInstance represents an audit data on a workflow template instance. diff --git a/sdk/event_workflow_template.go b/sdk/event_workflow_template.go index 4483dfde33..c0d30ef9b2 100644 --- a/sdk/event_workflow_template.go +++ b/sdk/event_workflow_template.go @@ -7,8 +7,9 @@ type EventWorkflowTemplateAdd struct { // EventWorkflowTemplateUpdate represents the event when updating a workflow template. type EventWorkflowTemplateUpdate struct { - NewWorkflowTemplate WorkflowTemplate `json:"new_workflow_template"` OldWorkflowTemplate WorkflowTemplate `json:"old_workflow_template"` + NewWorkflowTemplate WorkflowTemplate `json:"new_workflow_template"` + ChangeMessage string `json:"change_message"` } // EventWorkflowTemplateInstanceAdd represents the event when adding a workflow template instance. @@ -18,6 +19,6 @@ type EventWorkflowTemplateInstanceAdd struct { // EventWorkflowTemplateInstanceUpdate represents the event when updating a workflow template instance. type EventWorkflowTemplateInstanceUpdate struct { - NewWorkflowTemplateInstance WorkflowTemplateInstance `json:"new_workflow_template_instance"` OldWorkflowTemplateInstance WorkflowTemplateInstance `json:"old_workflow_template_instance"` + NewWorkflowTemplateInstance WorkflowTemplateInstance `json:"new_workflow_template_instance"` } diff --git a/sdk/workflow_template.go b/sdk/workflow_template.go index 2b3699c24a..3527476af6 100644 --- a/sdk/workflow_template.go +++ b/sdk/workflow_template.go @@ -53,10 +53,11 @@ type WorkflowTemplate struct { Environments EnvironmentTemplates `json:"environments" db:"environments"` Version int64 `json:"version" db:"version"` // aggregates - Group *Group `json:"group,omitempty" db:"-"` - FirstAudit *AuditWorkflowTemplate `json:"first_audit,omitempty" db:"-"` - LastAudit *AuditWorkflowTemplate `json:"last_audit,omitempty" db:"-"` - Editable bool `json:"editable,omitempty" db:"-"` + Group *Group `json:"group,omitempty" db:"-"` + FirstAudit *AuditWorkflowTemplate `json:"first_audit,omitempty" db:"-"` + LastAudit *AuditWorkflowTemplate `json:"last_audit,omitempty" db:"-"` + Editable bool `json:"editable,omitempty" db:"-"` + ChangeMessage string `json:"change_message,omitempty" db:"-"` } // IsValid returns workflow template validity. diff --git a/ui/src/app/model/audit.model.ts b/ui/src/app/model/audit.model.ts index ee89602610..00a5dd7c47 100644 --- a/ui/src/app/model/audit.model.ts +++ b/ui/src/app/model/audit.model.ts @@ -15,4 +15,5 @@ export class AuditWorkflow extends Audit { export class AuditWorkflowTemplate extends Audit { workflow_template_id: string; + change_message: string; } diff --git a/ui/src/app/model/workflow-template.model.ts b/ui/src/app/model/workflow-template.model.ts index 5e124a0dd4..13c25d75c7 100644 --- a/ui/src/app/model/workflow-template.model.ts +++ b/ui/src/app/model/workflow-template.model.ts @@ -17,6 +17,7 @@ export class WorkflowTemplate { first_audit: AuditWorkflowTemplate; last_audit: AuditWorkflowTemplate; editable: boolean; + change_message: string; } export class WorkflowTemplateParameter { diff --git a/ui/src/app/service/workflow-template/workflow-template.service.ts b/ui/src/app/service/workflow-template/workflow-template.service.ts index fc6b1fcbfb..47e4422cee 100644 --- a/ui/src/app/service/workflow-template/workflow-template.service.ts +++ b/ui/src/app/service/workflow-template/workflow-template.service.ts @@ -50,7 +50,8 @@ export class WorkflowTemplateService { return this._http.get('/project/' + projectKey + '/workflow/' + workflowName + '/templateInstance'); } - getAudits(groupName: string, templateSlug: string): Observable> { - return this._http.get>('/template/' + groupName + '/' + templateSlug + '/audit'); + getAudits(groupName: string, templateSlug: string, version?: number): Observable> { + let url = '/template/' + groupName + '/' + templateSlug + '/audit' + (version ? '?sinceVersion=' + version : ''); + return this._http.get>(url); } } diff --git a/ui/src/app/shared/audit/list/audit.list.html b/ui/src/app/shared/audit/list/audit.list.html index 74096244af..2bb14b87f8 100644 --- a/ui/src/app/shared/audit/list/audit.list.html +++ b/ui/src/app/shared/audit/list/audit.list.html @@ -23,7 +23,7 @@

{{ 'audit_title' | translate }}

- + diff --git a/ui/src/app/shared/button/confirm/confirm.button.html b/ui/src/app/shared/button/confirm/confirm.button.html index 9696e6bb92..44883fb8d0 100644 --- a/ui/src/app/shared/button/confirm/confirm.button.html +++ b/ui/src/app/shared/button/confirm/confirm.button.html @@ -1,9 +1,10 @@ -
-
+ \ No newline at end of file diff --git a/ui/src/app/shared/button/confirm/confirm.button.scss b/ui/src/app/shared/button/confirm/confirm.button.scss index e69de29bb2..e017ac18f0 100644 --- a/ui/src/app/shared/button/confirm/confirm.button.scss +++ b/ui/src/app/shared/button/confirm/confirm.button.scss @@ -0,0 +1 @@ +@import '../../../../common'; diff --git a/ui/src/app/shared/button/confirm/confirm.button.ts b/ui/src/app/shared/button/confirm/confirm.button.ts index 42686d4bb8..f815b3e940 100644 --- a/ui/src/app/shared/button/confirm/confirm.button.ts +++ b/ui/src/app/shared/button/confirm/confirm.button.ts @@ -6,21 +6,20 @@ import {Component, EventEmitter, Input, Output} from '@angular/core'; styleUrls: ['./confirm.button.scss'] }) export class ConfirmButtonComponent { - @Input() loading = false; @Input() icon = ''; @Input() disabled = false; @Input() color = 'primary'; @Input() class: string; + @Output() event = new EventEmitter(); @Input() title: string; - @Output() event = new EventEmitter(); showConfirmation = false; constructor() {} confirmEvent() { - this.event.emit(true); + this.event.emit(); this.reset(); } diff --git a/ui/src/app/shared/diff/diff.ts b/ui/src/app/shared/diff/diff.ts new file mode 100644 index 0000000000..257d961321 --- /dev/null +++ b/ui/src/app/shared/diff/diff.ts @@ -0,0 +1,79 @@ +import { WorkflowTemplate } from '../../model/workflow-template.model'; +import { Item } from './list/diff.list.component'; + +export function calculateWorkflowTemplateDiff(before: WorkflowTemplate, after: WorkflowTemplate): Array { + let beforeTemplate: any; + if (before) { + beforeTemplate = { + name: before.name, + slug: before.slug, + group_id: before.group_id, + description: before.description, + parameters: before.parameters + } + } + + let afterTemplate: any; + if (after) { + afterTemplate = { + name: after.name, + slug: after.slug, + group_id: after.group_id, + description: after.description, + parameters: after.parameters + } + } + + let diffItems = [ + { + translate: 'workflow_template', + before: beforeTemplate ? JSON.stringify(beforeTemplate) : null, + after: afterTemplate ? JSON.stringify(afterTemplate) : null, + type: 'application/json' + }, + { + translate: 'workflow_template_diff_workflow', + before: before ? atob(before.value) : null, + after: after ? atob(after.value) : null, + type: 'text/x-yaml' + } + ]; + + let pipelinesLength = Math.max(before ? before.pipelines.length : 0, after ? after.pipelines.length : 0); + for (let i = 0; i < pipelinesLength; i++) { + diffItems.push( + { + translate: 'workflow_template_diff_pipeline', + translateData: { number: pipelinesLength > 1 ? i : '' }, + before: before && before.pipelines[i] ? atob(before.pipelines[i].value) : null, + after: after && after.pipelines[i] ? atob(after.pipelines[i].value) : null, + type: 'text/x-yaml' + }) + } + + let applicationsLength = Math.max(before ? before.applications.length : 0, after ? after.applications.length : 0); + for (let i = 0; i < applicationsLength; i++) { + diffItems.push( + { + translate: 'workflow_template_diff_application', + translateData: { number: applicationsLength > 1 ? i : '' }, + before: before && before.applications[i] ? atob(before.applications[i].value) : null, + after: after && after.applications[i] ? atob(after.applications[i].value) : null, + type: 'text/x-yaml' + }) + } + + let environmentsLength = Math.max(before ? before.environments.length : 0, after ? after.environments.length : 0); + for (let i = 0; i < environmentsLength; i++) { + diffItems.push( + { + translate: 'workflow_template_diff_environment', + translateData: { number: environmentsLength > 1 ? i : '' }, + before: before && before.environments[i] ? atob(before.environments[i].value) : null, + after: after && after.environments[i] ? atob(after.environments[i].value) : null, + type: 'text/x-yaml' + }) + } + + return diffItems; +} diff --git a/ui/src/app/shared/diff/item/diff.item.component.ts b/ui/src/app/shared/diff/item/diff.item.component.ts index 6d91095042..44de736d22 100644 --- a/ui/src/app/shared/diff/item/diff.item.component.ts +++ b/ui/src/app/shared/diff/item/diff.item.component.ts @@ -68,7 +68,7 @@ export class DiffItemComponent implements OnChanges { } ngOnChanges() { - if (this.original && this.updated) { + if (this.original || this.updated) { this.refresh(); } this.codeMirrorConfig.mode = this.type; @@ -80,7 +80,10 @@ export class DiffItemComponent implements OnChanges { original = ''; } - let updated = this.updated; + let updated = this.updated || ''; + if (updated === 'null') { + updated = ''; + } if (this.type === 'application/json') { try { original = JSON.stringify(JSON.parse(original), null, 2); diff --git a/ui/src/app/shared/diff/list/diff.list.component.ts b/ui/src/app/shared/diff/list/diff.list.component.ts index e6531aea8a..0107733b9d 100644 --- a/ui/src/app/shared/diff/list/diff.list.component.ts +++ b/ui/src/app/shared/diff/list/diff.list.component.ts @@ -3,6 +3,8 @@ import { Mode } from '../item/diff.item.component'; export class Item { name: string; + translate: string; + translateData: any; before: string; after: string; type: string; diff --git a/ui/src/app/shared/diff/list/diff.list.html b/ui/src/app/shared/diff/list/diff.list.html index 26e0a45269..30ff631839 100644 --- a/ui/src/app/shared/diff/list/diff.list.html +++ b/ui/src/app/shared/diff/list/diff.list.html @@ -6,7 +6,8 @@

- {{i.name}} + {{i.name}} + {{i.translate | translate: i.translateData}}

diff --git a/ui/src/app/shared/shared.module.ts b/ui/src/app/shared/shared.module.ts index 8f07273b55..3fad0ed41f 100644 --- a/ui/src/app/shared/shared.module.ts +++ b/ui/src/app/shared/shared.module.ts @@ -60,7 +60,7 @@ import { RequirementsListComponent } from './requirements/list/requirements.list import { ScrollviewComponent } from './scrollview/scrollview.component'; import { SharedService } from './shared.service'; import { StatusIconComponent } from './status/status.component'; -import { DataTableComponent } from './table/data-table.component'; +import { DataTableComponent, SelectorPipe } from './table/data-table.component'; import { TabsComponent } from './tabs/tabs.component'; import { ToastService } from './toast/ToastService'; import { TokenListComponent } from './token/list/token.list.component'; @@ -208,6 +208,7 @@ import { ZoneComponent } from './zone/zone.component'; DiffListComponent, VCSStrategyComponent, FavoriteCardsComponent, + SelectorPipe, DataTableComponent, WorkflowTemplateApplyFormComponent, WorkflowTemplateModalComponent, @@ -313,6 +314,7 @@ import { ZoneComponent } from './zone/zone.component'; DiffListComponent, VCSStrategyComponent, FavoriteCardsComponent, + SelectorPipe, DataTableComponent, WorkflowTemplateApplyFormComponent, WorkflowTemplateModalComponent, diff --git a/ui/src/app/shared/table/data-table.component.ts b/ui/src/app/shared/table/data-table.component.ts index 91f002c678..056dc1022e 100644 --- a/ui/src/app/shared/table/data-table.component.ts +++ b/ui/src/app/shared/table/data-table.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { Component, EventEmitter, Input, OnChanges, Output, Pipe, PipeTransform, } from '@angular/core'; import { Table } from './table'; type direction = string; @@ -11,7 +11,8 @@ export enum ColumnType { LINK = 'link', ROUTER_LINK = 'router-link', MARKDOWN = 'markdown', - DATE = 'date' + DATE = 'date', + CONFIRM_BUTTON = 'confirm-button' } export type Selector = (d: any) => any; @@ -20,17 +21,30 @@ export type Filter = (f: string) => (d: any) => any; export class Column { type: ColumnType; name: string; + class: string; selector: Selector; sortable: boolean; sortKey: string; } +@Pipe({ name: 'selector' }) +export class SelectorPipe implements PipeTransform { + transform(columns: Array, data: any): Array { + return columns.map(c => { + return { + ...c, + selector: c.selector(data) + }; + }); + } +} + @Component({ selector: 'app-data-table', templateUrl: './data-table.html', styleUrls: ['./data-table.scss'] }) -export class DataTableComponent extends Table { +export class DataTableComponent extends Table implements OnChanges { @Input() columns: Array; @Output() sortChange = new EventEmitter(); @Output() dataChange = new EventEmitter(); @@ -38,23 +52,9 @@ export class DataTableComponent extends Table { @Input() withLineClick: boolean; @Output() clickLine = new EventEmitter(); - @Input() set data(d: any) { - this.allData = d; - this.getDataForCurrentPage(); - } - get data() { return this.allData; } - - @Input() set withPagination(n: number) { - this.nbElementsByPage = n; - this.getDataForCurrentPage(); - } - get withPagination() { return this.nbElementsByPage; } - - @Input() set withFilter(f: Filter) { - this.filterFunc = f; - this.getDataForCurrentPage(); - } - get withFilter() { return this.filterFunc; } + @Input() data: any; + @Input() withPagination: number; + @Input() withFilter: Filter; sortedColumn: Column; sortedColumnDirection: direction; @@ -66,6 +66,13 @@ export class DataTableComponent extends Table { filteredData: any; indexSelected: number; + ngOnChanges() { + this.allData = this.data; + this.nbElementsByPage = this.withPagination; + this.filterFunc = this.withFilter; + this.getDataForCurrentPage(); + } + columnClick(event: Event, c: Column) { if (!c.sortable) { return; diff --git a/ui/src/app/shared/table/data-table.html b/ui/src/app/shared/table/data-table.html index 78d299d347..e1afbd926d 100644 --- a/ui/src/app/shared/table/data-table.html +++ b/ui/src/app/shared/table/data-table.html @@ -7,10 +7,10 @@
- +
- - @@ -35,27 +35,21 @@ - - - - - - - diff --git a/ui/src/app/shared/table/data-table.scss b/ui/src/app/shared/table/data-table.scss index 299f3e415c..cce1a6fee1 100644 --- a/ui/src/app/shared/table/data-table.scss +++ b/ui/src/app/shared/table/data-table.scss @@ -1,17 +1,21 @@ @import '../../../common'; - th.sortable:after { font-family: Icons; content: '\f0dc'; } + th.ascending:after { font-family: Icons; content: '\f0de'; } + th.descending:after { font-family: Icons; content: '\f0dd'; } -td.center { - text-align: center; + +.table { + tbody tr td { + overflow: hidden; + } } diff --git a/ui/src/app/shared/workflow-template/modal/workflow-template.modal.component.ts b/ui/src/app/shared/workflow-template/modal/workflow-template.modal.component.ts index d6e4838c5a..8d95d36d1f 100644 --- a/ui/src/app/shared/workflow-template/modal/workflow-template.modal.component.ts +++ b/ui/src/app/shared/workflow-template/modal/workflow-template.modal.component.ts @@ -1,10 +1,13 @@ import { Component, Input, ViewChild } from '@angular/core'; import { ModalTemplate, TemplateModalConfig } from 'ng2-semantic-ui'; import { ActiveModal, SuiModalService } from 'ng2-semantic-ui/dist'; +import { forkJoin } from 'rxjs'; import { Project } from '../../../model/project.model'; import { WorkflowTemplate, WorkflowTemplateInstance } from '../../../model/workflow-template.model'; import { Workflow } from '../../../model/workflow.model'; import { WorkflowTemplateService } from '../../../service/services.module'; +import { calculateWorkflowTemplateDiff } from '../../../shared/diff/diff'; +import { Item } from '../../../shared/diff/list/diff.list.component'; @Component({ selector: 'app-workflow-template-modal', @@ -12,44 +15,58 @@ import { WorkflowTemplateService } from '../../../service/services.module'; styleUrls: ['./workflow-template.modal.scss'] }) export class WorkflowTemplateModalComponent { - @ViewChild('workflowTemplateModal') - workflowTemplateModal: ModalTemplate; - modal: ActiveModal; - + @ViewChild('workflowTemplateModal') workflowTemplateModal: ModalTemplate; @Input() project: Project; @Input() workflow: Workflow; - + modal: ActiveModal; workflowTemplate: WorkflowTemplate; workflowTemplateInstance: WorkflowTemplateInstance; + diffVisible: boolean; + diffItems: Array; + workflowTemplateAuditMessages: Array; - constructor(private _modalService: SuiModalService, private _templateService: WorkflowTemplateService) { - } + constructor( + private _modalService: SuiModalService, + private _templateService: WorkflowTemplateService + ) { } - show(): void { + show() { const config = new TemplateModalConfig(this.workflowTemplateModal); config.mustScroll = true; this.modal = this._modalService.open(config); - this.loadTemplate(); - this.loadInstance(); + this.load(); } - loadTemplate() { + load() { let s = this.workflow.from_template.split('/'); - if (s.length > 1) { - this._templateService.getWorkflowTemplate(s[0], s.splice(1, s.length - 1).join('/')).subscribe(wt => { - this.workflowTemplate = wt; - }); - } - } - loadInstance() { - this._templateService.getWorkflowTemplateInstance(this.project.key, this.workflow.name).subscribe(wti => { - this.workflowTemplateInstance = wti; - }); + forkJoin(this._templateService.getWorkflowTemplate(s[0], s.splice(1, s.length - 1).join('/')), + this._templateService.getWorkflowTemplateInstance(this.project.key, this.workflow.name)).subscribe(res => { + this.workflowTemplate = res[0]; + this.workflowTemplateInstance = res[1]; + + // load audits since instance version if not latest + if (this.workflowTemplateInstance.workflow_template_version !== this.workflowTemplate.version) { + this._templateService.getAudits(this.workflowTemplate.group.name, this.workflowTemplate.slug, + this.workflowTemplateInstance.workflow_template_version).subscribe(as => { + this.workflowTemplateAuditMessages = as.filter(a => !!a.change_message).map(a => a.change_message); + let before = as[0].data_after ? JSON.parse(as[0].data_after) : null; + this.diffItems = calculateWorkflowTemplateDiff(before, this.workflowTemplate); + }); + } else { + this.workflowTemplateAuditMessages = []; + this.diffItems = []; + } + }); } close() { + this.diffVisible = false; this.modal.approve(true); } + + toggleDiff() { + this.diffVisible = !this.diffVisible; + } } diff --git a/ui/src/app/shared/workflow-template/modal/workflow-template.modal.html b/ui/src/app/shared/workflow-template/modal/workflow-template.modal.html index 2bdf2f849c..63123a476a 100644 --- a/ui/src/app/shared/workflow-template/modal/workflow-template.modal.html +++ b/ui/src/app/shared/workflow-template/modal/workflow-template.modal.html @@ -16,7 +16,7 @@
-
+
@@ -27,16 +27,24 @@
- {{'common_up_to_date' - | translate}} +
{{'common_up_to_date' | translate}}
{{'common_not_up_to_date' | translate }} + class="ui orange label" (click)="toggleDiff()" [title]="'workflow_template_update_info_btn' | translate"> + {{'common_not_up_to_date' | translate }}
+
+
+ {{m}} +
+
+
+ +
+ (apply)="load()">
diff --git a/ui/src/app/views/pipeline/show/audit/pipeline.audit.html b/ui/src/app/views/pipeline/show/audit/pipeline.audit.html index 12108976ae..81ad2f671c 100644 --- a/ui/src/app/views/pipeline/show/audit/pipeline.audit.html +++ b/ui/src/app/views/pipeline/show/audit/pipeline.audit.html @@ -21,7 +21,7 @@ {{a.versionned | date:'short' }}
diff --git a/ui/src/app/views/settings/workflow-template/edit/workflow-template.edit.component.ts b/ui/src/app/views/settings/workflow-template/edit/workflow-template.edit.component.ts index 3917a87c72..d8dea08873 100644 --- a/ui/src/app/views/settings/workflow-template/edit/workflow-template.edit.component.ts +++ b/ui/src/app/views/settings/workflow-template/edit/workflow-template.edit.component.ts @@ -8,6 +8,7 @@ import { WorkflowTemplate } from '../../../../model/workflow-template.model'; import { GroupService } from '../../../../service/services.module'; import { WorkflowTemplateService } from '../../../../service/workflow-template/workflow-template.service'; import { PathItem } from '../../../../shared/breadcrumb/breadcrumb.component'; +import { calculateWorkflowTemplateDiff } from '../../../../shared/diff/diff'; import { Item } from '../../../../shared/diff/list/diff.list.component'; import { Column, ColumnType } from '../../../../shared/table/data-table.component'; import { Tab } from '../../../../shared/tabs/tabs.component'; @@ -24,6 +25,7 @@ export class WorkflowTemplateEditComponent implements OnInit { groups: Array; audits: Array; loading: boolean; + loadingAudits: boolean; path: Array; tabs: Array; selectedTab: Tab; @@ -53,17 +55,26 @@ export class WorkflowTemplateEditComponent implements OnInit { this.columns = [ { - name: this._translate.instant('audit_modification_type'), + name: 'audit_modification_type', + class: 'two', selector: a => a.event_type }, { type: ColumnType.DATE, - name: this._translate.instant('audit_time_author'), + class: 'two', + name: 'audit_time_author', selector: a => a.created }, { - name: this._translate.instant('audit_username'), + name: 'audit_username', + class: 'two', selector: a => a.triggered_by + }, + { + type: ColumnType.MARKDOWN, + class: 'eight', + name: 'common_description', + selector: a => a.change_message } ]; @@ -86,6 +97,21 @@ export class WorkflowTemplateEditComponent implements OnInit { .subscribe(wt => { this.oldWorkflowTemplate = { ...wt }; this.workflowTemplate = wt; + + if (this.workflowTemplate.editable) { + this.columns.push({ + type: ColumnType.CONFIRM_BUTTON, + name: 'common_action', + class: 'two right aligned', + selector: a => { + return { + title: 'common_rollback', + click: _ => { this.clickRollback(a) } + }; + } + }); + } + this.updatePath(); }); } @@ -100,11 +126,11 @@ export class WorkflowTemplateEditComponent implements OnInit { } getAudits(groupName: string, templateSlug: string) { - this.loading = true; + this.loadingAudits = true; this._workflowTemplateService.getAudits(groupName, templateSlug) - .pipe(finalize(() => this.loading = false)) + .pipe(finalize(() => this.loadingAudits = false)) .subscribe(as => { - this.audits = as.sort((a, b) => Date.parse(a.created) >= Date.parse(b.created) ? -1 : 1); + this.audits = as; }); } @@ -156,77 +182,14 @@ export class WorkflowTemplateEditComponent implements OnInit { clickAudit(a: AuditWorkflowTemplate) { let before = a.data_before ? JSON.parse(a.data_before) : null; let after = a.data_after ? JSON.parse(a.data_after) : null; + this.diffItems = calculateWorkflowTemplateDiff(before, after); + } - let beforeTemplate: any; - if (before) { - beforeTemplate = { - name: before.name, - slug: before.slug, - group_id: before.group_id, - description: before.description, - parameters: before.parameters - } - } - - let afterTemplate: any; - if (after) { - afterTemplate = { - name: after.name, - slug: after.slug, - group_id: after.group_id, - description: after.description, - parameters: after.parameters - } - } - - let diffItems = [ - { - name: 'template', - before: beforeTemplate ? JSON.stringify(beforeTemplate) : null, - after: afterTemplate ? JSON.stringify(afterTemplate) : null, - type: 'application/json' - }, - { - name: 'workflow', - before: before ? atob(before.value) : null, - after: after ? atob(after.value) : null, - type: 'text/x-yaml' - } - ]; - - let pipelinesLength = Math.max(before ? before.pipelines.length : 0, after ? after.pipelines.length : 0); - for (let i = 0; i < pipelinesLength; i++) { - diffItems.push( - { - name: 'pipeline ' + i, - before: before && before.pipelines[i] ? atob(before.pipelines[i].value) : null, - after: after && after.pipelines[i] ? atob(after.pipelines[i].value) : null, - type: 'text/x-yaml' - }) - } - - let applicationsLength = Math.max(before ? before.applications.length : 0, after ? after.applications.length : 0); - for (let i = 0; i < applicationsLength; i++) { - diffItems.push( - { - name: 'application ' + i, - before: before && before.applications[i] ? atob(before.applications[i].value) : null, - after: after && after.applications[i] ? atob(after.applications[i].value) : null, - type: 'text/x-yaml' - }) - } - - let environmentsLength = Math.max(before ? before.environments.length : 0, after ? after.environments.length : 0); - for (let i = 0; i < environmentsLength; i++) { - diffItems.push( - { - name: 'environment ' + i, - before: before && before.environments[i] ? atob(before.environments[i].value) : null, - after: after && after.environments[i] ? atob(after.environments[i].value) : null, - type: 'text/x-yaml' - }) + clickRollback(a: AuditWorkflowTemplate) { + this.workflowTemplate = a.data_before ? JSON.parse(a.data_before) : null; + if (!this.workflowTemplate) { + this.workflowTemplate = a.data_after ? JSON.parse(a.data_after) : null; } - - this.diffItems = diffItems; + this.saveWorkflowTemplate(); } } diff --git a/ui/src/app/views/settings/workflow-template/edit/workflow-template.edit.html b/ui/src/app/views/settings/workflow-template/edit/workflow-template.edit.html index fcb3f2d492..acb4a25907 100644 --- a/ui/src/app/views/settings/workflow-template/edit/workflow-template.edit.html +++ b/ui/src/app/views/settings/workflow-template/edit/workflow-template.edit.html @@ -25,9 +25,10 @@

{{ "common_last_modified" | translate }}

- -
+
+
diff --git a/ui/src/app/views/settings/workflow-template/form/workflow-template.form.component.ts b/ui/src/app/views/settings/workflow-template/form/workflow-template.form.component.ts index 776c81567a..bf6b7940b3 100644 --- a/ui/src/app/views/settings/workflow-template/form/workflow-template.form.component.ts +++ b/ui/src/app/views/settings/workflow-template/form/workflow-template.form.component.ts @@ -23,6 +23,7 @@ export class WorkflowTemplateFormComponent { } this._workflowTemplate = wt; + this.changeMessage = null; this.parameterKeys = []; this.parameterValues = {}; @@ -96,6 +97,8 @@ export class WorkflowTemplateFormComponent { user: User; + changeMessage: string; + constructor(private _sharedService: SharedService) { this.templateParameterTypes = ['boolean', 'string', 'repository']; @@ -146,6 +149,10 @@ export class WorkflowTemplateFormComponent { } this.workflowTemplate.group_id = Number(this.workflowTemplate.group_id); + if (this.changeMessage) { + this.workflowTemplate.change_message = this.changeMessage; + } + this.save.emit(); } diff --git a/ui/src/app/views/settings/workflow-template/form/workflow-template.form.html b/ui/src/app/views/settings/workflow-template/form/workflow-template.form.html index d5d1a3b0c5..ddb519583c 100644 --- a/ui/src/app/views/settings/workflow-template/form/workflow-template.form.html +++ b/ui/src/app/views/settings/workflow-template/form/workflow-template.form.html @@ -139,6 +139,10 @@ +
+ +
diff --git a/ui/src/app/views/settings/workflow-template/list/workflow-template.list.component.ts b/ui/src/app/views/settings/workflow-template/list/workflow-template.list.component.ts index 87ab2a3639..cff4206ae7 100644 --- a/ui/src/app/views/settings/workflow-template/list/workflow-template.list.component.ts +++ b/ui/src/app/views/settings/workflow-template/list/workflow-template.list.component.ts @@ -1,5 +1,4 @@ import { Component } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; import { finalize } from 'rxjs/internal/operators/finalize'; import { WorkflowTemplate } from '../../../../model/workflow-template.model'; import { WorkflowTemplateService } from '../../../../service/workflow-template/workflow-template.service'; @@ -19,13 +18,12 @@ export class WorkflowTemplateListComponent { path: Array constructor( - private _workflowTemplateService: WorkflowTemplateService, - private _translate: TranslateService + private _workflowTemplateService: WorkflowTemplateService ) { this.columns = [ { type: ColumnType.ROUTER_LINK, - name: this._translate.instant('common_name'), + name: 'common_name', selector: wt => { return { link: '/settings/workflow-template/' + wt.group.name + '/' + wt.slug, @@ -35,11 +33,11 @@ export class WorkflowTemplateListComponent { }, { type: ColumnType.MARKDOWN, - name: this._translate.instant('common_description'), + name: 'common_description', selector: wt => wt.description }, { - name: this._translate.instant('common_group'), + name: 'common_group', selector: wt => wt.group.name } ]; diff --git a/ui/src/app/views/workflow/workflow.html b/ui/src/app/views/workflow/workflow.html index 4a227daecb..003007ce26 100644 --- a/ui/src/app/views/workflow/workflow.html +++ b/ui/src/app/views/workflow/workflow.html @@ -37,9 +37,10 @@
-

{{ 'workflow_from_template' | translate: {path: workflow.from_template} }} - {{'common_not_up_to_date' - | translate }} +

{{ 'workflow_from_template' | translate}} + {{workflow.from_template}} + {{'common_not_up_to_date' + | translate }}

{{ c.name | translate }} @@ -19,7 +19,7 @@
+
{{ 'common_loading' | translate }}
- - - - {{c.selector(d).value}} + + + + + {{c.selector.value}} - - - {{c.selector(d).value}} + + {{c.selector.value}} + + {{c.selector | amCalendar}} + + + {{c.selector}} - - - {{c.selector(d) | amCalendar}} - {{c.selector(d)}}
- +