mirror of
https://github.com/nasa/openmct.git
synced 2025-06-06 01:11:41 +00:00
[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:
parent
35bbebbbc7
commit
8c92178895
@ -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", () => {
|
||||||
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
@ -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 =
|
||||||
|
@ -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');
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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');
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user