Skip to content

Commit

Permalink
Enable user to select which component to excel export (#610)
Browse files Browse the repository at this point in the history
* Enable user to select which component to excel export

Signed-off-by: Vanessa Fotso <[email protected]>

* Updated excel export tests

Signed-off-by: Vanessa Fotso <[email protected]>

* cleanup

Signed-off-by: Vanessa Fotso <[email protected]>

* Updated excel export modal to also display components version and release

Signed-off-by: Vanessa Fotso <[email protected]>

---------

Signed-off-by: Vanessa Fotso <[email protected]>
  • Loading branch information
vanessuniq authored Sep 26, 2023
1 parent 5043383 commit 3b00a2d
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 72 deletions.
6 changes: 3 additions & 3 deletions app/controllers/projects_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,12 @@ def destroy
end

def export
# Using class variable @@components_to_export here to save params[:components_type] value in memory,
# Using class variable @@components_to_export here to save params[:component_ids] value in memory,
# because format.html below triggers a redirect to this same action controller
# causing to lose the :components_type param.
# causing to lose the :component_ids param.

# rubocop:disable Style/ClassVars
@@components_to_export = params[:components_type] || @@components_to_export
@@components_to_export = params[:component_ids] || @@components_to_export
# rubocop:enable Style/ClassVars

export_type = params[:type]&.to_sym
Expand Down
5 changes: 3 additions & 2 deletions app/helpers/export_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,11 @@ module ExportHelper # rubocop:todo Metrics/ModuleLength
artifact_description: 16
}.freeze

def export_excel(project, components_type, is_disa_export)
def export_excel(project, component_ids, is_disa_export)
components_to_export = project.components.where(id: component_ids.split(','))
# One file for all data types, each data type in a different tab
workbook = FastExcel.open(constant_memory: true)
components_to_export = components_type == 'all' ? project.components : project.components.where(released: true)
# components_to_export = components_type == 'all' ? project.components : project.components.where(released: true)
components_to_export.eager_load(
rules: [:reviews, :disa_rule_descriptions, :rule_descriptions, :checks,
:additional_answers, :satisfies, :satisfied_by, {
Expand Down
135 changes: 97 additions & 38 deletions app/javascript/components/project/Project.vue
Original file line number Diff line number Diff line change
Expand Up @@ -74,50 +74,60 @@
@projectUpdated="refreshProject"
/>
<b-dropdown right text="Download" variant="secondary" class="px-2 m2">
<b-dropdown-item v-b-modal.disa-excel-export-modal>
<b-dropdown-item
v-b-modal.excel-export-modal
@click="excelExportType = 'disa_excel'"
>
DISA Excel Export
</b-dropdown-item>
<b-dropdown-item v-b-modal.excel-export-modal>Excel Export</b-dropdown-item>
<b-dropdown-item v-b-modal.excel-export-modal @click="excelExportType = 'excel'">
Excel Export
</b-dropdown-item>
<b-dropdown-item @click="downloadExport('inspec')">InSpec Profile</b-dropdown-item>
<b-dropdown-item @click="downloadExport('xccdf')">Xccdf Export</b-dropdown-item>
</b-dropdown>

<b-modal
id="excel-export-modal"
ref="excel-export-modal"
title="Excel Export"
:title="excelExportType === 'excel' ? 'Excel Export' : 'DISA Excel Export'"
centered
>
<p class="my-2">
Would you like to export all components in this project or just the released
components?
</p>
<template #modal-footer>
<b-button @click="downloadExport('excel', 'all')">
Export all components
</b-button>
<b-button @click="downloadExport('excel', 'released')">
Export released Components
</b-button>
</template>
</b-modal>
<b-form-group>
<template #label>
<h5>Select components to export:</h5>
<b-form-checkbox
v-model="allComponentsSelected"
:indeterminate="indeterminate"
aria-describedby="allComponents"
aria-controls="allComponents"
@change="toggleComponents"
>
{{ allComponentsSelected ? "Un-select All" : "Select All" }}
</b-form-checkbox>
<b-form-checkbox
v-model="releasedComponentsSelected"
aria-describedby="releasedComponents"
aria-controls="releasedComponents"
:disabled="releasedComponents.length === 0"
@change="toggleComponents"
>
Select Released Components
</b-form-checkbox>
</template>
<template #default="{ ariaDescribedby }">
<b-form-checkbox-group
v-model="selectedComponentsToExport"
:options="excelExportComponentOptions"
:aria-describedby="ariaDescribedby"
class="mb-2"
/>
</template>
</b-form-group>

<b-modal
id="disa-excel-export-modal"
ref="disa-excel-export-modal"
title="DISA Excel Export"
centered
>
<p class="my-2">
Would you like to export all components in this project or just the released
components?
</p>
<template #modal-footer>
<b-button @click="downloadExport('disa_excel', 'all')">
Export all components
</b-button>
<b-button @click="downloadExport('disa_excel', 'released')">
Export released Components
<b-button @click="downloadExport(excelExportType)">
Export Selected Components
</b-button>
</template>
</b-modal>
Expand Down Expand Up @@ -364,9 +374,20 @@ export default {
project: this.initialProjectState,
visible: this.initialProjectState.visibility === "discoverable",
projectTabIndex: 0,
excelExportType: "",
selectedComponentsToExport: [],
allComponentsSelected: false,
releasedComponentsSelected: false,
indeterminate: false,
};
},
computed: {
excelExportComponentOptions: function () {
return this.sortedComponents().map((c) => {
const versionRelease = c.version && c.release ? ` - V${c.version}R${c.release}` : "";
return { text: `${c.name}${versionRelease}`, value: c.id };
});
},
sortedAvailableComponents: function () {
return _.sortBy(this.project.available_components, ["child_project_name"], ["asc"]);
},
Expand Down Expand Up @@ -399,6 +420,29 @@ export default {
JSON.stringify(this.projectTabIndex)
);
},
selectedComponentsToExport: function (newValue, oldValue) {
// Handle changes in individual component checkboxes
if (newValue.length === 0) {
this.indeterminate = false;
this.allComponentsSelected = false;
this.releasedComponentsSelected = false;
} else if (newValue.length === this.project.components.length) {
this.indeterminate = false;
this.allComponentsSelected = true;
this.releasedComponentsSelected = true;
} else if (
this.releasedComponents().lenght > 0 &&
this.releasedComponents().every((element) => newValue.includes(element))
) {
this.indeterminate = true;
this.allComponentsSelected = false;
this.releasedComponentsSelected = true;
} else {
this.indeterminate = true;
this.allComponentsSelected = false;
this.releasedComponentsSelected = false;
}
},
},
mounted: function () {
// Persist `currentTab` across page loads
Expand All @@ -416,6 +460,18 @@ export default {
}
},
methods: {
toggleComponents: function () {
if (this.allComponentsSelected) {
this.selectedComponentsToExport = this.project.components.map((comp) => comp.id);
} else if (this.releasedComponentsSelected) {
this.selectedComponentsToExport = this.releasedComponents();
} else {
this.selectedComponentsToExport = [];
}
},
releasedComponents: function () {
return this.project.components.filter((comp) => comp.released).map((comp) => comp.id);
},
sortedComponents: function () {
return _.orderBy(
this.project.components,
Expand Down Expand Up @@ -457,20 +513,23 @@ export default {
})
.catch(this.alertOrNotifyResponse);
},
downloadExport: function (type, componentsToExport) {
if (type === "excel") {
this.$refs["excel-export-modal"].hide();
} else if (type === "disa_excel") {
this.$refs["disa-excel-export-modal"].hide();
}
downloadExport: function (type) {
axios
.get(`/projects/${this.project.id}/export/${type}?components_type=${componentsToExport}`)
.get(
`/projects/${this.project.id}/export/${type}?component_ids=${this.selectedComponentsToExport}`
)
.then((_res) => {
// Once it is validated that there is content to download, prompt
// the user to save the file
window.open(`/projects/${this.project.id}/export/${type}`);
})
.catch(this.alertOrNotifyResponse);
if (type === "excel" || type === "disa_excel") {
this.$refs["excel-export-modal"].hide();
this.excelExportType = "";
this.selectedComponentsToExport = [];
}
},
},
};
Expand Down
87 changes: 58 additions & 29 deletions spec/helpers/export_helper_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,25 @@

before(:all) do
@component = FactoryBot.create(:component)
@released_component = FactoryBot.create(:released_component)
@project = @component.project
@project_with_released_comp = @released_component.project
@component_ids = @project.components.pluck(:id).join(',')
end

describe '#export_excel' do
before(:all) do
@workbook = export_excel(@project, 'released', false)
@workbook_release_only = export_excel(@project_with_released_comp, 'released', false)
@workbook_release_all = export_excel(@project_with_released_comp, 'all', false)
@workbook = export_excel(@project, @component_ids, false)
@workbook_disa_export = export_excel(@project, @component_ids, true)

@workbook_disa_export = export_excel(@project, 'released', true)
@workbook_disa_export_release_only = export_excel(@project_with_released_comp, 'released', true)
@workbook_disa_export_release_all = export_excel(@project_with_released_comp, 'all', true)

[@workbook, @workbook_release_only, @workbook_release_all,
@workbook_disa_export, @workbook_disa_export_release_only,
@workbook_disa_export_release_all].each_with_index do |item, index|
[@workbook, @workbook_disa_export].each_with_index do |item, index|
file_name = ''
if index == 0
file_name = "./#{@project.name}.xlsx"
File.binwrite(file_name, item.read_string)
@xlsx = Roo::Spreadsheet.open(file_name)
else
file_name = "./#{@project_with_released_comp.name}.xlsx"
file_name = "./#{@project.name}_DISA.xlsx"
File.binwrite(file_name, item.read_string)
@xlsx_release_only = Roo::Spreadsheet.open(file_name) if index == 1
@xlsx_release_all = Roo::Spreadsheet.open(file_name) if index == 2
@xlsx_disa = Roo::Spreadsheet.open(file_name)
end
File.delete(file_name)
end
Expand All @@ -44,29 +35,67 @@
it 'creates an excel format of a given project' do
expect(@workbook).to be_present
expect(@workbook.filename).to end_with 'xlsx'
expect(@workbook_disa_export).to be_present
expect(@workbook_disa_export.filename).to end_with 'xlsx'
end
it 'creates an excel file with the # sheets == # of components that was requested for export' do
expect(@xlsx.sheets.size).to eq @component_ids.split(',').size
expect(@xlsx_disa.sheets.size).to eq @component_ids.split(',').size
end
end

context 'when project has released component(s) and user requested only released components' do
it 'creates an excel file with the # of sheets == # of released components' do
expect(@xlsx_release_only.sheets.size).to eq @project_with_released_comp.components.where(released: true).size
context 'When a user request a DISA excel export' do
it 'does not include a column for "InSpec Control Body"' do
parsed = @xlsx_disa.sheet(0).parse(headers: true).drop(1)
expect(parsed.first).not_to include 'InSpec Control Body'
end
it 'returns empty values for "Status Justification", "Mitigation",and "Artifact Description" columns if
the rule status is "Applicable - Configurable"' do
parsed = @xlsx_disa.sheet(0).parse(headers: true).drop(1)
status_justifications = parsed.filter_map do |row|
next if row['Status'] != 'Applicable - Configurable'

row['Status Justification']
end
mitigations = parsed.filter_map do |row|
next if row['Status'] != 'Applicable - Configurable'

it 'creates an excel file with correct format for worksheet name' do
expect(@xlsx_release_only.sheets).to all(match(/\w+-V[0-9]+R[0-9]+-[0-9]+/))
row['Mitigation']
end
artifact_descriptions = parsed.filter_map do |row|
next if row['Status'] != 'Applicable - Configurable'

row['Artifact Description']
end
expect(status_justifications).to be_empty
expect(mitigations).to be_empty
expect(artifact_descriptions).to be_empty
end
end

context 'When project has released component(s) and user requested to download all components' do
it 'creates an excel file with the # of sheets == total # of components' do
expect(@xlsx_release_all.sheets.size).to eq @project_with_released_comp.components.size
it 'returns emty values for "Mitigation" column if rule (row) status is "Applicable - Inherently Meets"' do
parsed = @xlsx_disa.sheet(0).parse(headers: true).drop(1)
mitigations = parsed.filter_map do |row|
next if row['Status'] != 'Applicable - Inherently Meets'

row['Mitigation']
end
expect(mitigations).to be_empty
end
end

context 'when project has no released component and user requested only released components' do
it 'creates an empty spreadsheet' do
expect(@xlsx.sheets.size).to eq 1
expect(@xlsx.sheets.first).to eq 'Sheet1'
it 'returns emty values for "Mitigation, Artifact Description" column if rule (row) status is "Not Applicable"' do
parsed = @xlsx_disa.sheet(0).parse(headers: true).drop(1)
mitigations = parsed.filter_map do |row|
next if row['Status'] != 'Not Applicable'

row['Mitigation']
end
artifact_descriptions = parsed.filter_map do |row|
next if row['Status'] != 'Not Applicable'

row['Artifact Description']
end
expect(mitigations).to be_empty
expect(artifact_descriptions).to be_empty
end
end
end
Expand Down

0 comments on commit 3b00a2d

Please sign in to comment.