diff --git a/apps/backend/src/evaluations/evaluations.controller.spec.ts b/apps/backend/src/evaluations/evaluations.controller.spec.ts index 41dff92155..eb53b89736 100644 --- a/apps/backend/src/evaluations/evaluations.controller.spec.ts +++ b/apps/backend/src/evaluations/evaluations.controller.spec.ts @@ -102,7 +102,13 @@ describe('EvaluationsController', () => { evaluation.id, {user: user} ); - expect(foundEvaluation).toEqual(new EvaluationDto(evaluation)); + + expect(foundEvaluation).toEqual( + // The evaluation is created with the current user ID above + // so the expectation is that user should be able to edit + // which is the 2nd parameter to EvaluationDto. + new EvaluationDto(evaluation, true) + ); }); it('should return an evaluations tags', async () => { diff --git a/apps/backend/src/evaluations/evaluations.controller.ts b/apps/backend/src/evaluations/evaluations.controller.ts index 158d1f6a0f..977a7e58a9 100644 --- a/apps/backend/src/evaluations/evaluations.controller.ts +++ b/apps/backend/src/evaluations/evaluations.controller.ts @@ -45,7 +45,7 @@ export class EvaluationsController { const abac = this.authz.abac.createForUser(request.user); const evaluation = await this.evaluationsService.findById(id); ForbiddenError.from(abac).throwUnlessCan(Action.Read, evaluation); - return new EvaluationDto(evaluation); + return new EvaluationDto(evaluation, abac.can(Action.Update, evaluation)); } @Get(':id/groups') @@ -126,8 +126,14 @@ export class EvaluationsController { const evaluationToUpdate = await this.evaluationsService.findById(id); ForbiddenError.from(abac).throwUnlessCan(Action.Update, evaluationToUpdate); + const updatedEvaluation = await this.evaluationsService.update( + id, + updateEvaluationDto + ); + return new EvaluationDto( - await this.evaluationsService.update(id, updateEvaluationDto) + updatedEvaluation, + abac.can(Action.Update, updatedEvaluation) ); } diff --git a/apps/frontend/src/components/cards/EvaluationInfo.vue b/apps/frontend/src/components/cards/EvaluationInfo.vue index dddc4ae2a9..92687c804d 100644 --- a/apps/frontend/src/components/cards/EvaluationInfo.vue +++ b/apps/frontend/src/components/cards/EvaluationInfo.vue @@ -39,7 +39,9 @@ export default class EvaluationInfo extends Vue { } get filename(): string { - return this.file_object.filename; + // Filename can be updated by a user when filename is loaded from the database. This causes any changes + // to the name to show up right away. + return this.evaluation?.filename || this.file_object.filename; } get inspec_version(): string | undefined { @@ -55,13 +57,9 @@ export default class EvaluationInfo extends Vue { } get evaluation(): IEvaluation | undefined { - let result: IEvaluation | undefined; - EvaluationModule.allEvaluations.forEach((e) => { - if(e.id === this.file_object.database_id?.toString()) { - result = e - } + return EvaluationModule.allEvaluations.find((e) => { + return e.id === this.file_object.database_id?.toString() }) - return result } } diff --git a/apps/frontend/src/components/global/sidebaritems/SidebarFileList.vue b/apps/frontend/src/components/global/sidebaritems/SidebarFileList.vue index 4ef8174017..fefc26dca1 100644 --- a/apps/frontend/src/components/global/sidebaritems/SidebarFileList.vue +++ b/apps/frontend/src/components/global/sidebaritems/SidebarFileList.vue @@ -39,12 +39,14 @@ import {Prop} from 'vue-property-decorator'; import {ICreateEvaluation} from '@heimdall/interfaces'; import _ from 'lodash'; import RouteMixin from '@/mixins/RouteMixin'; +import {EvaluationModule} from '@/store/evaluations'; @Component export default class FileItem extends mixins(ServerMixin, RouteMixin) { @Prop({type: Object}) readonly file!: EvaluationFile | ProfileFile; - saving: boolean = false; + saving = false; + select_file() { if (this.file.hasOwnProperty('evaluation')) { @@ -119,6 +121,9 @@ export default class FileItem extends mixins(ServerMixin, RouteMixin) { .then((response) => { SnackbarModule.notify('File saved successfully'); file.database_id = parseInt(response.data.id); + EvaluationModule.loadEvaluation(response.data.id); + const loadedDatabaseIds = InspecDataModule.loadedDatabaseIds.join(','); + this.navigateWithNoErrors(`/${this.current_route}/${loadedDatabaseIds}`); }) .catch((error) => { SnackbarModule.failure(error.response.data.message); @@ -126,7 +131,6 @@ export default class FileItem extends mixins(ServerMixin, RouteMixin) { this.saving = false; }); } - //gives different icons for a file if it is just a profile get icon(): string { if (this.file.hasOwnProperty('profile')) { diff --git a/apps/frontend/src/components/global/upload_tabs/EditEvaluationModal.vue b/apps/frontend/src/components/global/upload_tabs/EditEvaluationModal.vue index 3ef1e13f20..127773a196 100644 --- a/apps/frontend/src/components/global/upload_tabs/EditEvaluationModal.vue +++ b/apps/frontend/src/components/global/upload_tabs/EditEvaluationModal.vue @@ -158,7 +158,6 @@ export default class EditEvaluationModal extends Vue { async update(): Promise { Promise.all([EvaluationModule.updateEvaluation(this.activeEvaluation), this.updateGroups()]).then(() => { SnackbarModule.notify('Evaluation Updated Successfully'); - this.$emit('updateEvaluations') }) this.visible = false; } diff --git a/apps/frontend/src/store/evaluations.ts b/apps/frontend/src/store/evaluations.ts index 0172777fc3..d21c90db24 100644 --- a/apps/frontend/src/store/evaluations.ts +++ b/apps/frontend/src/store/evaluations.ts @@ -47,16 +47,14 @@ export class Evaluation extends VuexModule { ); return Promise.all( unloadedIds.map(async (id) => { - return axios - .get(`/evaluations/${id}`) - .then(async ({data}) => { - this.addEvaluation(data); + return this.loadEvaluation(id) + .then(async (evaluation) => { return InspecIntakeModule.loadText({ - text: JSON.stringify(data.data), - filename: data.filename, - database_id: data.id, - createdAt: data.createdAt, - updatedAt: data.updatedAt, + text: JSON.stringify(evaluation.data), + filename: evaluation.filename, + database_id: evaluation.id, + createdAt: evaluation.createdAt, + updatedAt: evaluation.updatedAt, tags: [] // Tags are not yet implemented, so for now the value is passed in empty }).catch((err) => { SnackbarModule.failure(err); @@ -70,13 +68,21 @@ export class Evaluation extends VuexModule { } @Action - async addEvaluation(evaluation: IEvaluation) { - this.context.commit('ADD_EVALUATION', evaluation); + async loadEvaluation(id: string) { + return axios.get(`/evaluations/${id}`).then(({data}) => { + this.context.commit('SAVE_EVALUTION', data); + return data; + }); } @Action async updateEvaluation(evaluation: IEvaluation) { - return axios.put(`/evaluations/${evaluation.id}`, evaluation); + return axios + .put(`/evaluations/${evaluation.id}`, evaluation) + .then(({data}) => { + this.context.commit('SAVE_EVALUTION', data); + return data; + }); } @Action @@ -106,9 +112,20 @@ export class Evaluation extends VuexModule { this.allEvaluations = evaluations; } + // Save an evaluation or update it if it is already saved. @Mutation - ADD_EVALUATION(evaluation: IEvaluation) { - this.allEvaluations.push(evaluation); + SAVE_EVALUTION(evaluationToSave: IEvaluation) { + let found = false; + for (const [index, evaluation] of this.allEvaluations.entries()) { + if (evaluationToSave.id === evaluation.id) { + this.allEvaluations.splice(index, 1, evaluationToSave); + found = true; + break; + } + } + if (!found) { + this.allEvaluations.push(evaluationToSave); + } } @Mutation diff --git a/apps/frontend/src/views/Results.vue b/apps/frontend/src/views/Results.vue index dd570d690a..be777a9c27 100644 --- a/apps/frontend/src/views/Results.vue +++ b/apps/frontend/src/views/Results.vue @@ -65,9 +65,32 @@ @click="toggle_profile(file)" > - + +
+ + + +
+
+ File Info ↓ @@ -198,6 +221,7 @@ import StatusChart from '@/components/cards/StatusChart.vue'; import SeverityChart from '@/components/cards/SeverityChart.vue'; import ComplianceChart from '@/components/cards/ComplianceChart.vue'; import UploadButton from '@/components/generic/UploadButton.vue'; +import EditEvaluationModal from '@/components/global/upload_tabs/EditEvaluationModal.vue'; import ExportCaat from '@/components/global/ExportCaat.vue'; import ExportNist from '@/components/global/ExportNist.vue'; @@ -206,7 +230,7 @@ import EvaluationInfo from '@/components/cards/EvaluationInfo.vue'; import {FilteredDataModule, Filter, TreeMapState} from '@/store/data_filters'; import {ControlStatus, Severity} from 'inspecjs'; -import {FileID, SourcedContextualizedEvaluation, SourcedContextualizedProfile} from '@/store/report_intake'; +import {EvaluationFile, FileID, ProfileFile, SourcedContextualizedEvaluation, SourcedContextualizedProfile} from '@/store/report_intake'; import {InspecDataModule} from '@/store/data_store'; import ProfileData from '@/components/cards/ProfileData.vue'; @@ -214,9 +238,11 @@ import ProfileData from '@/components/cards/ProfileData.vue'; import {ServerModule} from '@/store/server'; import {capitalize} from 'lodash'; import {compare_times} from '../utilities/delta_util'; +import {EvaluationModule} from '../store/evaluations'; import RouteMixin from '@/mixins/RouteMixin'; import {StatusCountModule} from '../store/status_counts'; import ServerMixin from '../mixins/ServerMixin'; +import {IEvaluation} from '@heimdall/interfaces'; @Component({ components: { @@ -232,7 +258,8 @@ import ServerMixin from '../mixins/ServerMixin'; ExportJson, EvaluationInfo, ProfileData, - UploadButton + UploadButton, + EditEvaluationModal } }) export default class Results extends mixins(RouteMixin, ServerMixin) { @@ -295,6 +322,18 @@ export default class Results extends mixins(RouteMixin, ServerMixin) { return this.is_result_view ? this.evaluationFiles : this.profiles; } + getFile(fileID: FileID) { + return InspecDataModule.allFiles.find( + (f) => f.unique_id === fileID + ); + } + + getDbFile(file: EvaluationFile | ProfileFile): IEvaluation | undefined { + return EvaluationModule.allEvaluations.find((e) => { + return e.id === file.database_id?.toString() + }) + } + /** * Returns true if we're showing results */ @@ -400,11 +439,10 @@ export default class Results extends mixins(RouteMixin, ServerMixin) { get curr_title(): string { let returnText = `${capitalize(this.current_route_name.slice(0, -1))} View`; if (this.file_filter.length == 1) { - let file = InspecDataModule.allFiles.find( - (f) => f.unique_id === this.file_filter[0] - ); + const file = this.getFile(this.file_filter[0]) if (file) { - returnText += ` (${file.filename} selected)`; + const dbFile = this.getDbFile(file); + returnText += ` (${dbFile?.filename || file.filename} selected)`; } } else { returnText += ` (${this.file_filter.length} ${this.current_route_name} selected)`; @@ -446,4 +484,14 @@ export default class Results extends mixins(RouteMixin, ServerMixin) { top: 4px; z-index: 5; } +.bottom-right { + position: absolute; + bottom: 0; + right: 0; +} +.top-right { + position: absolute; + top: 0; + right: 0; +} diff --git a/test/integration/database-results.spec.ts b/test/integration/database-results.spec.ts index 104f66bdcd..95711c8609 100644 --- a/test/integration/database-results.spec.ts +++ b/test/integration/database-results.spec.ts @@ -29,7 +29,6 @@ context('Database results', () => { describe('CRUD', () => { it('allows a user to save a result', () => { uploadModal.loadSample(sampleToLoad); - sidebar.open(); sidebar.save(sampleToLoad); toastVerifier.toastTextContains('File saved successfully'); uploadModal.activate(); @@ -39,7 +38,6 @@ context('Database results', () => { it('allows a user to load a result', () => { uploadModal.loadSample(sampleToLoad); - sidebar.open(); sidebar.save(sampleToLoad); sidebar.close(sampleToLoad); uploadModal.activate(); @@ -50,7 +48,6 @@ context('Database results', () => { it('allows a user to update a result', () => { const updatedName = 'Updated Filename'; uploadModal.loadSample(sampleToLoad); - sidebar.open(); sidebar.save(sampleToLoad); sidebar.close(sampleToLoad); uploadModal.activate(); @@ -61,7 +58,6 @@ context('Database results', () => { it('allows a user to delete a result', () => { uploadModal.loadSample(sampleToLoad); - sidebar.open(); sidebar.save(sampleToLoad); sidebar.close(sampleToLoad); uploadModal.activate(); diff --git a/test/support/components/Sidebar.ts b/test/support/components/Sidebar.ts index 135ce77775..7d4b6cd607 100644 --- a/test/support/components/Sidebar.ts +++ b/test/support/components/Sidebar.ts @@ -1,17 +1,17 @@ export default class Sidebar { - open(): void { + openClose(): void { cy.get('[data-cy=openSidebar]').click({force: true}); } save(name: string): void { - cy.get(`[title="${name}"]`).within(() => { - cy.get('[data-cy=saveFile]').click(); - }); + this.openClose(); + cy.get(`[title="${name}"] [data-cy=saveFile]`).click(); + this.openClose(); } close(name: string): void { - cy.get(`[title="${name}"]`).within(() => { - cy.get('[data-cy=closeFile]').click(); - }); + this.openClose(); + cy.get(`[title="${name}"] [data-cy=closeFile]`).click(); + this.openClose(); } }