Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable user to select which component to excel export #610

Merged
merged 4 commits into from
Sep 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading