[User Attribution] "createdBy" and "modifiedBy" fields for domainObjects (#5741)

* Implementation of user attribution of object changes
* Adds created date to object creation
* Updating remove action to wait for save before navigationg

Co-authored-by: Andrew Henry <akhenry@gmail.com>
This commit is contained in:
Jamie V 2022-09-30 13:47:10 -07:00 committed by GitHub
parent 35bbebbbc7
commit 8c92178895
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 95 additions and 17 deletions

View File

@ -94,7 +94,6 @@ describe("The Annotation API", () => {
openmct.startHeadless(); openmct.startHeadless();
}); });
afterEach(async () => { afterEach(async () => {
openmct.objects.providers = {};
await resetApplicationState(openmct); await resetApplicationState(openmct);
}); });
it("is defined", () => { it("is defined", () => {

View File

@ -96,7 +96,7 @@ export default class ObjectAPI {
this.cache = {}; this.cache = {};
this.interceptorRegistry = new InterceptorRegistry(); this.interceptorRegistry = new InterceptorRegistry();
this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'plan', 'annotation']; this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'restricted-notebook', 'plan', 'annotation'];
this.errors = { this.errors = {
Conflict: ConflictError Conflict: ConflictError
@ -354,7 +354,7 @@ export default class ObjectAPI {
* @returns {Promise} a promise which will resolve when the domain object * @returns {Promise} a promise which will resolve when the domain object
* has been saved, or be rejected if it cannot be saved * has been saved, or be rejected if it cannot be saved
*/ */
save(domainObject) { async save(domainObject) {
let provider = this.getProvider(domainObject.identifier); let provider = this.getProvider(domainObject.identifier);
let savedResolve; let savedResolve;
let savedReject; let savedReject;
@ -372,6 +372,8 @@ export default class ObjectAPI {
savedReject = reject; savedReject = reject;
}); });
domainObject.persisted = persistedTime; domainObject.persisted = persistedTime;
domainObject.created = persistedTime;
domainObject.createdBy = await this.#getCurrentUsername();
const newObjectPromise = provider.create(domainObject); const newObjectPromise = provider.create(domainObject);
if (newObjectPromise) { if (newObjectPromise) {
newObjectPromise.then(response => { newObjectPromise.then(response => {
@ -385,6 +387,7 @@ export default class ObjectAPI {
} }
} else { } else {
domainObject.persisted = persistedTime; domainObject.persisted = persistedTime;
domainObject.modifiedBy = await this.#getCurrentUsername();
this.mutate(domainObject, 'persisted', persistedTime); this.mutate(domainObject, 'persisted', persistedTime);
result = provider.update(domainObject); result = provider.update(domainObject);
} }
@ -399,6 +402,17 @@ export default class ObjectAPI {
}); });
} }
async #getCurrentUsername() {
const user = await this.openmct.user.getCurrentUser();
let username;
if (user !== undefined) {
username = user.getName();
}
return username;
}
/** /**
* After entering into edit mode, creates a new instance of Transaction to keep track of changes in Objects * After entering into edit mode, creates a new instance of Transaction to keep track of changes in Objects
*/ */

View File

@ -8,13 +8,27 @@ describe("The Object API", () => {
let mockDomainObject; let mockDomainObject;
const TEST_NAMESPACE = "test-namespace"; const TEST_NAMESPACE = "test-namespace";
const TEST_KEY = "test-key"; const TEST_KEY = "test-key";
const USERNAME = 'Joan Q Public';
const FIFTEEN_MINUTES = 15 * 60 * 1000; const FIFTEEN_MINUTES = 15 * 60 * 1000;
beforeEach((done) => { beforeEach((done) => {
typeRegistry = jasmine.createSpyObj('typeRegistry', [ typeRegistry = jasmine.createSpyObj('typeRegistry', [
'get' 'get'
]); ]);
const userProvider = {
isLoggedIn() {
return true;
},
getCurrentUser() {
return Promise.resolve({
getName() {
return USERNAME;
}
});
}
};
openmct = createOpenMct(); openmct = createOpenMct();
openmct.user.setProvider(userProvider);
objectAPI = openmct.objects; objectAPI = openmct.objects;
openmct.editor = {}; openmct.editor = {};
@ -63,19 +77,34 @@ describe("The Object API", () => {
mockProvider.update.and.returnValue(Promise.resolve(true)); mockProvider.update.and.returnValue(Promise.resolve(true));
objectAPI.addProvider(TEST_NAMESPACE, mockProvider); objectAPI.addProvider(TEST_NAMESPACE, mockProvider);
}); });
it("Calls 'create' on provider if object is new", () => { it("Adds a 'created' timestamp to new objects", () => {
objectAPI.save(mockDomainObject); objectAPI.save(mockDomainObject);
expect(mockDomainObject.created).not.toBeUndefined();
});
it("Calls 'create' on provider if object is new", async () => {
await objectAPI.save(mockDomainObject);
expect(mockProvider.create).toHaveBeenCalled(); expect(mockProvider.create).toHaveBeenCalled();
expect(mockProvider.update).not.toHaveBeenCalled(); expect(mockProvider.update).not.toHaveBeenCalled();
}); });
it("Calls 'update' on provider if object is not new", () => { it("Calls 'update' on provider if object is not new", async () => {
mockDomainObject.persisted = Date.now() - FIFTEEN_MINUTES; mockDomainObject.persisted = Date.now() - FIFTEEN_MINUTES;
mockDomainObject.modified = Date.now(); mockDomainObject.modified = Date.now();
objectAPI.save(mockDomainObject); await objectAPI.save(mockDomainObject);
expect(mockProvider.create).not.toHaveBeenCalled(); expect(mockProvider.create).not.toHaveBeenCalled();
expect(mockProvider.update).toHaveBeenCalled(); expect(mockProvider.update).toHaveBeenCalled();
}); });
it("Sets the current user for 'createdBy' on new objects", async () => {
await objectAPI.save(mockDomainObject);
expect(mockDomainObject.createdBy).toBe(USERNAME);
});
it("Sets the current user for 'modifedBy' on existing objects", async () => {
mockDomainObject.persisted = Date.now() - FIFTEEN_MINUTES;
mockDomainObject.modified = Date.now();
await objectAPI.save(mockDomainObject);
expect(mockDomainObject.modifiedBy).toBe(USERNAME);
});
it("Does not persist if the object is unchanged", () => { it("Does not persist if the object is unchanged", () => {
mockDomainObject.persisted = mockDomainObject.persisted =

View File

@ -264,7 +264,7 @@ describe('the plugin', function () {
it('provides an inspector view with the version information if available', () => { it('provides an inspector view with the version information if available', () => {
componentObject = component.$root.$children[0]; componentObject = component.$root.$children[0];
const propertiesEls = componentObject.$el.querySelectorAll('.c-inspect-properties__row'); const propertiesEls = componentObject.$el.querySelectorAll('.c-inspect-properties__row');
expect(propertiesEls.length).toEqual(4); expect(propertiesEls.length).toEqual(6);
const found = Array.from(propertiesEls).some((propertyEl) => { const found = Array.from(propertiesEls).some((propertyEl) => {
return (propertyEl.children[0].innerHTML.trim() === 'Version' return (propertyEl.children[0].innerHTML.trim() === 'Version'
&& propertyEl.children[1].innerHTML.trim() === 'v1'); && propertyEl.children[1].innerHTML.trim() === 'v1');

View File

@ -34,8 +34,8 @@ export default class RemoveAction {
invoke(objectPath) { invoke(objectPath) {
let object = objectPath[0]; let object = objectPath[0];
let parent = objectPath[1]; let parent = objectPath[1];
this.showConfirmDialog(object).then(() => { this.showConfirmDialog(object).then(async () => {
this.removeFromComposition(parent, object); await this.removeFromComposition(parent, object);
if (this.inNavigationPath(object)) { if (this.inNavigationPath(object)) {
this.navigateTo(objectPath.slice(1)); this.navigateTo(objectPath.slice(1));
} }
@ -81,7 +81,7 @@ export default class RemoveAction {
this.openmct.router.navigate('#/browse/' + urlPath); this.openmct.router.navigate('#/browse/' + urlPath);
} }
removeFromComposition(parent, child) { async removeFromComposition(parent, child) {
let composition = parent.composition.filter(id => let composition = parent.composition.filter(id =>
!this.openmct.objects.areIdsEqual(id, child.identifier) !this.openmct.objects.areIdsEqual(id, child.identifier)
); );
@ -93,7 +93,7 @@ export default class RemoveAction {
} }
if (!this.isAlias(child, parent)) { if (!this.isAlias(child, parent)) {
this.openmct.objects.mutate(child, 'location', null); await this.openmct.objects.mutate(child, 'location', null);
} }
} }

View File

@ -38,6 +38,8 @@ describe('the inspector', () => {
folderItem = { folderItem = {
name: 'folder', name: 'folder',
type: 'folder', type: 'folder',
createdBy: 'John Q',
modifiedBy: 'Public',
id: 'mock-folder-key', id: 'mock-folder-key',
identifier: { identifier: {
namespace: '', namespace: '',
@ -74,6 +76,8 @@ describe('the inspector', () => {
const [ const [
title, title,
type, type,
createdBy,
modifiedBy,
notes, notes,
timestamp timestamp
] = details; ] = details;
@ -87,6 +91,14 @@ describe('the inspector', () => {
.toEqual('Type'); .toEqual('Type');
expect(type.value.toLowerCase()) expect(type.value.toLowerCase())
.toEqual(folderItem.type); .toEqual(folderItem.type);
expect(createdBy.name)
.toEqual('Created By');
expect(createdBy.value)
.toEqual(folderItem.createdBy);
expect(modifiedBy.name)
.toEqual('Modified By');
expect(modifiedBy.value)
.toEqual(folderItem.modifiedBy);
expect(notes.value) expect(notes.value)
.toEqual('This object should have some notes'); .toEqual('This object should have some notes');

View File

@ -90,10 +90,13 @@ export default {
return; return;
} }
const UNKNOWN_USER = 'Unknown';
const title = this.domainObject.name; const title = this.domainObject.name;
const typeName = this.type ? this.type.definition.name : `Unknown: ${this.domainObject.type}`; const typeName = this.type ? this.type.definition.name : `Unknown: ${this.domainObject.type}`;
const timestampLabel = this.domainObject.modified ? 'Modified' : 'Created'; const createdTimestamp = this.domainObject.created;
const timestamp = this.domainObject.modified ? this.domainObject.modified : this.domainObject.created; const createdBy = this.domainObject.createdBy ? this.domainObject.createdBy : UNKNOWN_USER;
const modifiedBy = this.domainObject.modifiedBy ? this.domainObject.modifiedBy : UNKNOWN_USER;
const modifiedTimestamp = this.domainObject.modified ? this.domainObject.modified : this.domainObject.created;
const notes = this.domainObject.notes; const notes = this.domainObject.notes;
const version = this.domainObject.version; const version = this.domainObject.version;
@ -105,6 +108,14 @@ export default {
{ {
name: 'Type', name: 'Type',
value: typeName value: typeName
},
{
name: 'Created By',
value: createdBy
},
{
name: 'Modified By',
value: modifiedBy
} }
]; ];
@ -115,15 +126,28 @@ export default {
}); });
} }
if (timestamp !== undefined) { if (createdTimestamp !== undefined) {
const formattedTimestamp = Moment.utc(timestamp) const formattedCreatedTimestamp = Moment.utc(createdTimestamp)
.format('YYYY-MM-DD[\n]HH:mm:ss') .format('YYYY-MM-DD[\n]HH:mm:ss')
+ ' UTC'; + ' UTC';
details.push( details.push(
{ {
name: timestampLabel, name: 'Created',
value: formattedTimestamp value: formattedCreatedTimestamp
}
);
}
if (modifiedTimestamp !== undefined) {
const formattedModifiedTimestamp = Moment.utc(modifiedTimestamp)
.format('YYYY-MM-DD[\n]HH:mm:ss')
+ ' UTC';
details.push(
{
name: 'Modified',
value: formattedModifiedTimestamp
} }
); );
} }