diff --git a/changelog/26485.txt b/changelog/26485.txt new file mode 100644 index 0000000000..6cc54cfb99 --- /dev/null +++ b/changelog/26485.txt @@ -0,0 +1,3 @@ +```release-note:bug +ui: fixes undefined start time in filename for downloaded client count attribution csv +``` diff --git a/ui/app/components/clients/attribution.hbs b/ui/app/components/clients/attribution.hbs index 788418236c..eb77ba4887 100644 --- a/ui/app/components/clients/attribution.hbs +++ b/ui/app/components/clients/attribution.hbs @@ -99,13 +99,17 @@

SELECTED DATE {{if this.formattedEndDate " RANGE"}}

- {{this.parseAPITimestamp @startTimestamp "MMMM yyyy"}} + {{this.formattedStartDate}} {{if this.formattedEndDate "-"}} {{this.formattedEndDate}}

- + {{#if @upgradesDuringActivity}} diff --git a/ui/app/components/clients/attribution.js b/ui/app/components/clients/attribution.js index 4729a164d5..e13e8d41a9 100644 --- a/ui/app/components/clients/attribution.js +++ b/ui/app/components/clients/attribution.js @@ -43,11 +43,8 @@ import { format, isSameMonth } from 'date-fns'; export default class Attribution extends Component { @service download; - @tracked showCSVDownloadModal = false; - parseAPITimestamp = (time, format) => parseAPITimestamp(time, format); - get attributionLegend() { const attributionLegend = [ { key: 'entity_clients', label: 'entity clients' }, @@ -60,6 +57,11 @@ export default class Attribution extends Component { return attributionLegend; } + get formattedStartDate() { + if (!this.args.startTimestamp) return null; + return parseAPITimestamp(this.args.startTimestamp, 'MMMM yyyy'); + } + get formattedEndDate() { if (!this.args.startTimestamp && !this.args.endTimestamp) return null; // displays on CSV export modal, no need to display duplicate months and years @@ -73,9 +75,6 @@ export default class Attribution extends Component { } get isSingleNamespace() { - if (!this.args.totalClientAttribution) { - return 'no data'; - } // if a namespace is selected, then we're viewing top 10 auth methods (mounts) return !!this.args.selectedNamespace; } @@ -100,6 +99,9 @@ export default class Attribution extends Component { } get chartText() { + if (!this.args.totalClientAttribution) { + return { description: 'There is a problem gathering data' }; + } const dateText = this.formattedEndDate ? 'date range' : 'month'; switch (this.isSingleNamespace) { case true: @@ -121,10 +123,6 @@ export default class Attribution extends Component { }`, totalCopy: `The total clients in the namespace for this ${dateText}. This number is useful for identifying overall usage volume.`, }; - case 'no data': - return { - description: 'There is a problem gathering data', - }; default: return ''; } @@ -157,15 +155,15 @@ export default class Attribution extends Component { const csvData = []; // added to clarify that the row of namespace totals without an auth method (blank) are not additional clients // but indicate the total clients for that ns, including its auth methods - const upgrade = this.args.upgradesDuringActivity.length + const upgrade = this.args.upgradesDuringActivity?.length ? `\n **data contains an upgrade, mount summation may not equal namespace totals` : ''; const descriptionOfBlanks = this.isSingleNamespace ? '' - : `\n *namespace totals, inclusive of mount clients ${upgrade}`; + : `\n *namespace totals, inclusive of mount clients${upgrade}`; const csvHeader = [ 'Namespace path', - `"Mount path ${descriptionOfBlanks}"`, + `Mount path${descriptionOfBlanks}`, 'Total clients', 'Entity clients', 'Non-entity clients', @@ -216,10 +214,10 @@ export default class Attribution extends Component { get formattedCsvFileName() { const endRange = this.formattedEndDate ? `-${this.formattedEndDate}` : ''; - const csvDateRange = this.formattedStartDate + endRange; + const csvDateRange = this.formattedStartDate ? `_${this.formattedStartDate + endRange}` : ''; return this.isSingleNamespace - ? `clients_by_mount_path_${csvDateRange}` - : `clients_by_namespace_${csvDateRange}`; + ? `clients_by_mount_path${csvDateRange}` + : `clients_by_namespace${csvDateRange}`; } get modalExportText() { diff --git a/ui/tests/integration/components/clients/attribution-test.js b/ui/tests/integration/components/clients/attribution-test.js index 204bd361a7..c126c6585d 100644 --- a/ui/tests/integration/components/clients/attribution-test.js +++ b/ui/tests/integration/components/clients/attribution-test.js @@ -12,6 +12,7 @@ import { endOfMonth, formatRFC3339 } from 'date-fns'; import { click } from '@ember/test-helpers'; import subMonths from 'date-fns/subMonths'; import timestamp from 'core/utils/timestamp'; +import { GENERAL } from 'vault/tests/helpers/general-selectors'; module('Integration | Component | clients/attribution', function (hooks) { setupRenderingTest(hooks); @@ -19,7 +20,9 @@ module('Integration | Component | clients/attribution', function (hooks) { hooks.before(function () { sinon.stub(timestamp, 'now').callsFake(() => new Date('2018-04-03T14:15:30')); }); + hooks.beforeEach(function () { + this.csvDownloadStub = sinon.stub(this.owner.lookup('service:download'), 'csv'); const mockNow = timestamp.now(); this.mockNow = mockNow; this.set('startTimestamp', formatRFC3339(subMonths(mockNow, 6))); @@ -40,8 +43,10 @@ module('Integration | Component | clients/attribution', function (hooks) { { label: 'auth2/', clients: 2, entity_clients: 1, non_entity_clients: 1 }, ]); }); + hooks.after(function () { timestamp.now.restore(); + this.csvDownloadStub.restore(); }); test('it renders empty state with no data', async function (assert) { @@ -227,4 +232,92 @@ module('Integration | Component | clients/attribution', function (hooks) { .hasText('Export attribution data', 'modal appears to export csv'); assert.dom('[ data-test-export-date-range]').includesText('June 2022 - December 2022'); }); + + test('it downloads csv data for date range', async function (assert) { + assert.expect(2); + + await render(hbs` + + `); + await click('[data-test-attribution-export-button]'); + await click(GENERAL.confirmButton); + const [filename, content] = this.csvDownloadStub.lastCall.args; + assert.strictEqual(filename, 'clients_by_namespace_June 2022-December 2022', 'csv has expected filename'); + assert.strictEqual( + content, + `Namespace path,Mount path\n *namespace totals, inclusive of mount clients,Total clients,Entity clients,Non-entity clients\nsecond,*,10,7,3\nfirst,*,5,3,2`, + 'csv has expected content' + ); + }); + + test('it downloads csv data for a single month', async function (assert) { + assert.expect(2); + await render(hbs` + + `); + await click('[data-test-attribution-export-button]'); + await click(GENERAL.confirmButton); + const [filename, content] = this.csvDownloadStub.lastCall.args; + assert.strictEqual(filename, 'clients_by_namespace_June 2022', 'csv has single month in filename'); + assert.strictEqual( + content, + `Namespace path,Mount path\n *namespace totals, inclusive of mount clients,Total clients,Entity clients,Non-entity clients\nsecond,*,10,7,3\nfirst,*,5,3,2`, + 'csv has expected content' + ); + }); + + test('it downloads csv data when a namespace is selected', async function (assert) { + assert.expect(2); + this.selectedNamespace = 'second'; + + await render(hbs` + + `); + + await click('[data-test-attribution-export-button]'); + await click(GENERAL.confirmButton); + const [filename, content] = this.csvDownloadStub.lastCall.args; + assert.strictEqual( + filename, + 'clients_by_mount_path_June 2022-December 2022', + 'csv has expected filename for a selected namespace' + ); + assert.strictEqual( + content, + `Namespace path,Mount path,Total clients,Entity clients,Non-entity clients\nsecond,auth1/,3,2,1\nsecond,auth2/,2,1,1`, + 'csv has expected content for a selected namespace' + ); + }); + + test('csv filename omits date if no start/end timestamp', async function (assert) { + assert.expect(1); + + await render(hbs` + + `); + + await click('[data-test-attribution-export-button]'); + await click(GENERAL.confirmButton); + const [filename, ,] = this.csvDownloadStub.lastCall.args; + assert.strictEqual(filename, 'clients_by_namespace'); + }); });