mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-01 19:17:58 +00:00
Sync Enable Feature Workflow (#25739)
* adds modal for enabling sync in landing page cta workflow * adds config endpoint to sync mirage handler * update checkbox copy * handle adapter error and modify endpoints * address pr changes * add banner for when not opted in and update tests * change adapterError with verb to clarify boolean * update small tests changes * fix linting js errors * remove empty payload and update banner text * fix problematic test solve for another day * fix test --------- Co-authored-by: Angel Garbarino <Monkeychip@users.noreply.github.com> Co-authored-by: Angel Garbarino <argarbarino@gmail.com>
This commit is contained in:
@@ -3,6 +3,15 @@
|
|||||||
SPDX-License-Identifier: BUSL-1.1
|
SPDX-License-Identifier: BUSL-1.1
|
||||||
~}}
|
~}}
|
||||||
|
|
||||||
|
<SyncHeader @title="Secrets Sync">
|
||||||
|
<:actions>
|
||||||
|
{{! Only allow users to create a destination if secrets-sync is activated }}
|
||||||
|
{{#if (and this.version.isEnterprise @isActivated)}}
|
||||||
|
<Hds::Button @text="Create first destination" @route="secrets.destinations.create" data-test-cta-button />
|
||||||
|
{{/if}}
|
||||||
|
</:actions>
|
||||||
|
</SyncHeader>
|
||||||
|
|
||||||
<div class="box is-fullwidth is-sideless is-flex-between is-shadowless" data-test-cta-container>
|
<div class="box is-fullwidth is-sideless is-flex-between is-shadowless" data-test-cta-container>
|
||||||
{{#if this.version.isEnterprise}}
|
{{#if this.version.isEnterprise}}
|
||||||
<p>
|
<p>
|
||||||
|
@@ -3,15 +3,27 @@
|
|||||||
SPDX-License-Identifier: BUSL-1.1
|
SPDX-License-Identifier: BUSL-1.1
|
||||||
~}}
|
~}}
|
||||||
|
|
||||||
<SyncHeader @title="Secrets Sync">
|
{{#unless this.isActivated}}
|
||||||
<:actions>
|
<Hds::Alert @type="inline" @color="warning" data-test-secrets-sync-opt-in-banner as |A|>
|
||||||
{{#if (and this.version.isEnterprise (not @destinations))}}
|
<A.Title>Enable secrets sync feature</A.Title>
|
||||||
<Hds::Button @text="Create first destination" @route="secrets.destinations.create" data-test-cta-button />
|
<A.Description>To use this feature, specific activation is required. Please review the feature documentation and enable
|
||||||
{{/if}}
|
it. If you're upgrading from beta, your previous data will be accessible after activation.</A.Description>
|
||||||
</:actions>
|
<A.Button
|
||||||
</SyncHeader>
|
@text="Enable"
|
||||||
|
@color="secondary"
|
||||||
|
{{on "click" (fn (mut this.showActivateSecretsSyncModal) true)}}
|
||||||
|
data-test-secrets-sync-opt-in-banner-enable
|
||||||
|
/>
|
||||||
|
</Hds::Alert>
|
||||||
|
{{/unless}}
|
||||||
|
{{! show error if call to activated endpoint fails }}
|
||||||
|
{{#if @isAdapterError}}
|
||||||
|
<MessageError @errorMessage={{@activatedFeatures.message}} />
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
{{#if @destinations}}
|
{{#if @destinations}}
|
||||||
|
<SyncHeader @title="Secrets Sync" />
|
||||||
|
|
||||||
<div class="tabs-container box is-bottomless is-marginless is-paddingless">
|
<div class="tabs-container box is-bottomless is-marginless is-paddingless">
|
||||||
<nav class="tabs" aria-label="destination tabs">
|
<nav class="tabs" aria-label="destination tabs">
|
||||||
<ul>
|
<ul>
|
||||||
@@ -162,5 +174,39 @@
|
|||||||
</OverviewCard>
|
</OverviewCard>
|
||||||
</div>
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
<Secrets::LandingCta />
|
<Secrets::LandingCta @isActivated={{this.isActivated}} />
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if this.showActivateSecretsSyncModal}}
|
||||||
|
<Hds::Modal @onClose={{fn (mut this.showActivateSecretsSyncModal) false}} data-test-secrets-sync-opt-in-modal as |M|>
|
||||||
|
<M.Header @icon="alert-triangle">
|
||||||
|
Enable secrets sync feature
|
||||||
|
</M.Header>
|
||||||
|
<M.Body>
|
||||||
|
<p class="has-bottom-margin-m">
|
||||||
|
Before using this feature, we want to make sure you’ve carefully read the document around the billing and client
|
||||||
|
count impact.
|
||||||
|
<DocLink @path="/vault/docs/sync">Docs here.</DocLink>
|
||||||
|
</p>
|
||||||
|
<Hds::Form::Checkbox::Field {{on "change" this.onDocsConfirmChange}} data-test-opt-in-check as |F|>
|
||||||
|
<F.Label>I've read the above linked document</F.Label>
|
||||||
|
</Hds::Form::Checkbox::Field>
|
||||||
|
</M.Body>
|
||||||
|
<M.Footer>
|
||||||
|
<Hds::ButtonSet>
|
||||||
|
<Hds::Button
|
||||||
|
data-test-opt-in-confirm
|
||||||
|
@text="Confirm"
|
||||||
|
disabled={{this.confirmDisabled}}
|
||||||
|
{{on "click" (perform this.onFeatureConfirm)}}
|
||||||
|
/>
|
||||||
|
<Hds::Button
|
||||||
|
data-test-save-opt-in-cancel
|
||||||
|
@text="Cancel"
|
||||||
|
@color="secondary"
|
||||||
|
{{on "click" (fn (mut this.showActivateSecretsSyncModal) false)}}
|
||||||
|
/>
|
||||||
|
</Hds::ButtonSet>
|
||||||
|
</M.Footer>
|
||||||
|
</Hds::Modal>
|
||||||
{{/if}}
|
{{/if}}
|
@@ -7,28 +7,36 @@ import Component from '@glimmer/component';
|
|||||||
import { tracked } from '@glimmer/tracking';
|
import { tracked } from '@glimmer/tracking';
|
||||||
import { service } from '@ember/service';
|
import { service } from '@ember/service';
|
||||||
import { task } from 'ember-concurrency';
|
import { task } from 'ember-concurrency';
|
||||||
|
import { waitFor } from '@ember/test-waiters';
|
||||||
|
import { action } from '@ember/object';
|
||||||
|
import errorMessage from 'vault/utils/error-message';
|
||||||
import Ember from 'ember';
|
import Ember from 'ember';
|
||||||
|
|
||||||
import type FlashMessageService from 'vault/services/flash-messages';
|
import type FlashMessageService from 'vault/services/flash-messages';
|
||||||
import type RouterService from '@ember/routing/router-service';
|
|
||||||
import type StoreService from 'vault/services/store';
|
import type StoreService from 'vault/services/store';
|
||||||
|
import type RouterService from '@ember/routing/router-service';
|
||||||
import type VersionService from 'vault/services/version';
|
import type VersionService from 'vault/services/version';
|
||||||
import type { SyncDestinationAssociationMetrics } from 'vault/vault/adapters/sync/association';
|
import type { SyncDestinationAssociationMetrics } from 'vault/vault/adapters/sync/association';
|
||||||
import type SyncDestinationModel from 'vault/vault/models/sync/destination';
|
import type SyncDestinationModel from 'vault/vault/models/sync/destination';
|
||||||
|
import type { HTMLElementEvent } from 'vault/forms';
|
||||||
|
|
||||||
interface Args {
|
interface Args {
|
||||||
destinations: Array<SyncDestinationModel>;
|
destinations: Array<SyncDestinationModel>;
|
||||||
totalAssociations: number;
|
totalVaultSecrets: number;
|
||||||
|
activatedFeatures: Array<string>;
|
||||||
|
isAdapterError: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class SyncSecretsDestinationsPageComponent extends Component<Args> {
|
export default class SyncSecretsDestinationsPageComponent extends Component<Args> {
|
||||||
@service declare readonly flashMessages: FlashMessageService;
|
@service declare readonly flashMessages: FlashMessageService;
|
||||||
@service declare readonly router: RouterService;
|
|
||||||
@service declare readonly store: StoreService;
|
@service declare readonly store: StoreService;
|
||||||
|
@service declare readonly router: RouterService;
|
||||||
@service declare readonly version: VersionService;
|
@service declare readonly version: VersionService;
|
||||||
|
|
||||||
@tracked destinationMetrics: SyncDestinationAssociationMetrics[] = [];
|
@tracked destinationMetrics: SyncDestinationAssociationMetrics[] = [];
|
||||||
@tracked page = 1;
|
@tracked page = 1;
|
||||||
|
@tracked showActivateSecretsSyncModal = false;
|
||||||
|
@tracked confirmDisabled = true;
|
||||||
|
|
||||||
pageSize = Ember.testing ? 3 : 5; // lower in tests to test pagination without seeding more data
|
pageSize = Ember.testing ? 3 : 5; // lower in tests to test pagination without seeding more data
|
||||||
|
|
||||||
@@ -39,6 +47,13 @@ export default class SyncSecretsDestinationsPageComponent extends Component<Args
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isActivated() {
|
||||||
|
if (this.args.isAdapterError) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return this.args.activatedFeatures.includes('secrets-sync');
|
||||||
|
}
|
||||||
|
|
||||||
fetchAssociationsForDestinations = task(this, {}, async (page = 1) => {
|
fetchAssociationsForDestinations = task(this, {}, async (page = 1) => {
|
||||||
try {
|
try {
|
||||||
const total = page * this.pageSize;
|
const total = page * this.pageSize;
|
||||||
@@ -51,4 +66,23 @@ export default class SyncSecretsDestinationsPageComponent extends Component<Args
|
|||||||
this.destinationMetrics = [];
|
this.destinationMetrics = [];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@action
|
||||||
|
onDocsConfirmChange(event: HTMLElementEvent<HTMLInputElement>) {
|
||||||
|
this.confirmDisabled = !event.target.checked;
|
||||||
|
}
|
||||||
|
|
||||||
|
@task
|
||||||
|
@waitFor
|
||||||
|
*onFeatureConfirm() {
|
||||||
|
try {
|
||||||
|
yield this.store
|
||||||
|
.adapterFor('application')
|
||||||
|
.ajax('/v1/sys/activation-flags/secrets-sync/activate', 'POST');
|
||||||
|
this.showActivateSecretsSyncModal = false;
|
||||||
|
this.router.transitionTo('vault.cluster.sync.secrets.overview');
|
||||||
|
} catch (error) {
|
||||||
|
this.flashMessages.danger(`Error enabling feature \n ${errorMessage(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
45
ui/lib/sync/addon/routes/secrets.ts
Normal file
45
ui/lib/sync/addon/routes/secrets.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Route from '@ember/routing/route';
|
||||||
|
import { service } from '@ember/service';
|
||||||
|
import { hash } from 'rsvp';
|
||||||
|
|
||||||
|
import type RouterService from '@ember/routing/router-service';
|
||||||
|
import type StoreService from 'vault/services/store';
|
||||||
|
import type AdapterError from '@ember-data/adapter';
|
||||||
|
|
||||||
|
interface ActivationFlagsResponse {
|
||||||
|
data: {
|
||||||
|
activated: Array<string>;
|
||||||
|
unactivated: Array<string>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class SyncSecretsRoute extends Route {
|
||||||
|
@service declare readonly router: RouterService;
|
||||||
|
@service declare readonly store: StoreService;
|
||||||
|
|
||||||
|
model() {
|
||||||
|
return hash({
|
||||||
|
activatedFeatures: this.store
|
||||||
|
.adapterFor('application')
|
||||||
|
.ajax('/v1/sys/activation-flags', 'GET')
|
||||||
|
.then((resp: ActivationFlagsResponse) => {
|
||||||
|
return resp.data.activated;
|
||||||
|
})
|
||||||
|
.catch((error: AdapterError) => {
|
||||||
|
// we break out this error while passing args to the component and handle the error in the overview template
|
||||||
|
return error;
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
afterModel(model: { activatedFeatures: Array<string> | AdapterError }) {
|
||||||
|
if (!model.activatedFeatures) {
|
||||||
|
this.router.transitionTo('vault.cluster.sync.secrets.overview');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -8,17 +8,22 @@ import { service } from '@ember/service';
|
|||||||
import { hash } from 'rsvp';
|
import { hash } from 'rsvp';
|
||||||
|
|
||||||
import type StoreService from 'vault/services/store';
|
import type StoreService from 'vault/services/store';
|
||||||
|
import type AdapterError from '@ember-data/adapter';
|
||||||
|
|
||||||
export default class SyncSecretsOverviewRoute extends Route {
|
export default class SyncSecretsOverviewRoute extends Route {
|
||||||
@service declare readonly store: StoreService;
|
@service declare readonly store: StoreService;
|
||||||
|
|
||||||
async model() {
|
async model() {
|
||||||
|
const { activatedFeatures } = this.modelFor('secrets') as {
|
||||||
|
activatedFeatures: Array<string> | AdapterError;
|
||||||
|
};
|
||||||
return hash({
|
return hash({
|
||||||
destinations: this.store.query('sync/destination', {}).catch(() => []),
|
destinations: this.store.query('sync/destination', {}).catch(() => []),
|
||||||
associations: this.store
|
associations: this.store
|
||||||
.adapterFor('sync/association')
|
.adapterFor('sync/association')
|
||||||
.queryAll()
|
.queryAll()
|
||||||
.catch(() => []),
|
.catch(() => []),
|
||||||
|
activatedFeatures,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -6,4 +6,6 @@
|
|||||||
<Secrets::Page::Overview
|
<Secrets::Page::Overview
|
||||||
@destinations={{this.model.destinations}}
|
@destinations={{this.model.destinations}}
|
||||||
@totalVaultSecrets={{this.model.associations.total_secrets}}
|
@totalVaultSecrets={{this.model.associations.total_secrets}}
|
||||||
|
@activatedFeatures={{this.model.activatedFeatures}}
|
||||||
|
@isAdapterError={{this.model.activatedFeatures.isAdapterError}}
|
||||||
/>
|
/>
|
@@ -116,6 +116,16 @@ const createOrUpdateDestination = (schema, req) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function (server) {
|
export default function (server) {
|
||||||
|
// default to activated
|
||||||
|
server.get('/sys/activation-flags', () => {
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
activated: ['secrets-sync'],
|
||||||
|
unactivated: [''],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const base = '/sys/sync/destinations';
|
const base = '/sys/sync/destinations';
|
||||||
const uri = `${base}/:type/:name`;
|
const uri = `${base}/:type/:name`;
|
||||||
|
|
||||||
|
@@ -26,6 +26,27 @@ module('Acceptance | sync | destinations', function (hooks) {
|
|||||||
return authPage.login();
|
return authPage.login();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('it should show opt-in banner and modal if secrets-sync is not activated', async function (assert) {
|
||||||
|
assert.expect(3);
|
||||||
|
server.get('/sys/activation-flags', () => {
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
activated: [''],
|
||||||
|
unactivated: ['secrets-sync'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
await visit('vault/sync/secrets/overview');
|
||||||
|
assert.dom(ts.overview.optInBanner).exists('Opt-in banner is shown');
|
||||||
|
await click(ts.overview.optInBannerEnable);
|
||||||
|
assert.dom(ts.overview.optInModal).exists('Opt-in modal is shown');
|
||||||
|
assert.dom(ts.overview.optInConfirm).isDisabled('Confirm button is disabled when checkbox is unchecked');
|
||||||
|
await click(ts.overview.optInCheck);
|
||||||
|
await click(ts.overview.optInConfirm);
|
||||||
|
// ARG TODO improve test coverage and try and use API to check if the opt-in was successful
|
||||||
|
});
|
||||||
|
|
||||||
test('it should create new destination', async function (assert) {
|
test('it should create new destination', async function (assert) {
|
||||||
// remove destinations from mirage so cta shows when 404 is returned
|
// remove destinations from mirage so cta shows when 404 is returned
|
||||||
this.server.db.syncDestinations.remove();
|
this.server.db.syncDestinations.remove();
|
||||||
|
@@ -51,6 +51,11 @@ export const PAGE = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
overview: {
|
overview: {
|
||||||
|
optInBanner: '[data-test-secrets-sync-opt-in-banner]',
|
||||||
|
optInBannerEnable: '[data-test-secrets-sync-opt-in-banner-enable]',
|
||||||
|
optInModal: '[data-test-secrets-sync-opt-in-modal]',
|
||||||
|
optInCheck: '[data-test-opt-in-check]',
|
||||||
|
optInConfirm: '[data-test-opt-in-confirm]',
|
||||||
createDestination: '[data-test-create-destination]',
|
createDestination: '[data-test-create-destination]',
|
||||||
table: {
|
table: {
|
||||||
row: '[data-test-overview-table-row]',
|
row: '[data-test-overview-table-row]',
|
||||||
|
@@ -6,47 +6,53 @@
|
|||||||
import { module, test } from 'qunit';
|
import { module, test } from 'qunit';
|
||||||
import { setupRenderingTest } from 'ember-qunit';
|
import { setupRenderingTest } from 'ember-qunit';
|
||||||
import { setupEngine } from 'ember-engines/test-support';
|
import { setupEngine } from 'ember-engines/test-support';
|
||||||
|
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||||
import hbs from 'htmlbars-inline-precompile';
|
import hbs from 'htmlbars-inline-precompile';
|
||||||
import { render } from '@ember/test-helpers';
|
import { render } from '@ember/test-helpers';
|
||||||
import { PAGE } from 'vault/tests/helpers/sync/sync-selectors';
|
import { PAGE } from 'vault/tests/helpers/sync/sync-selectors';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
const { cta } = PAGE;
|
||||||
|
|
||||||
module('Integration | Component | sync | Secrets::LandingCta', function (hooks) {
|
module('Integration | Component | sync | Secrets::LandingCta', function (hooks) {
|
||||||
setupRenderingTest(hooks);
|
setupRenderingTest(hooks);
|
||||||
setupEngine(hooks, 'sync');
|
setupEngine(hooks, 'sync');
|
||||||
|
setupMirage(hooks);
|
||||||
|
|
||||||
hooks.beforeEach(function () {
|
hooks.beforeEach(function () {
|
||||||
this.version = this.owner.lookup('service:version');
|
this.version = this.owner.lookup('service:version');
|
||||||
|
this.transitionStub = sinon.stub(this.owner.lookup('service:router'), 'transitionTo');
|
||||||
|
|
||||||
|
this.renderComponent = () =>
|
||||||
|
render(
|
||||||
|
hbs`
|
||||||
|
<Secrets::LandingCta @isActivated={{true}}/>
|
||||||
|
`,
|
||||||
|
{ owner: this.engine }
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it should render promotional copy for community version', async function (assert) {
|
test('it should render promotional copy for community version', async function (assert) {
|
||||||
await render(
|
await this.renderComponent();
|
||||||
hbs`
|
|
||||||
<Secrets::LandingCta />
|
|
||||||
`,
|
|
||||||
{ owner: this.engine }
|
|
||||||
);
|
|
||||||
|
|
||||||
assert
|
assert
|
||||||
.dom(PAGE.cta.summary)
|
.dom(cta.summary)
|
||||||
.hasText(
|
.hasText(
|
||||||
'This enterprise feature allows you to sync secrets to platforms and tools across your stack to get secrets when and where you need them. Learn more about secrets sync'
|
'This enterprise feature allows you to sync secrets to platforms and tools across your stack to get secrets when and where you need them. Learn more about secrets sync'
|
||||||
);
|
);
|
||||||
assert.dom(PAGE.cta.link).hasText('Learn more about secrets sync');
|
assert.dom(cta.link).hasText('Learn more about secrets sync');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it should render enterprise copy', async function (assert) {
|
test('it should render enterprise copy and action', async function (assert) {
|
||||||
this.version.type = 'enterprise';
|
this.version.type = 'enterprise';
|
||||||
await render(
|
|
||||||
hbs`
|
await this.renderComponent();
|
||||||
<Secrets::LandingCta />
|
|
||||||
`,
|
|
||||||
{ owner: this.engine }
|
|
||||||
);
|
|
||||||
|
|
||||||
assert
|
assert
|
||||||
.dom(PAGE.cta.summary)
|
.dom(cta.summary)
|
||||||
.hasText(
|
.hasText(
|
||||||
'Sync secrets to platforms and tools across your stack to get secrets when and where you need them. Secrets sync tutorial'
|
'Sync secrets to platforms and tools across your stack to get secrets when and where you need them. Secrets sync tutorial'
|
||||||
);
|
);
|
||||||
assert.dom(PAGE.cta.link).hasText('Secrets sync tutorial');
|
assert.dom(cta.link).hasText('Secrets sync tutorial');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -41,32 +41,40 @@ module('Integration | Component | sync | Page::Overview', function (hooks) {
|
|||||||
|
|
||||||
const store = this.owner.lookup('service:store');
|
const store = this.owner.lookup('service:store');
|
||||||
this.destinations = await store.query('sync/destination', {});
|
this.destinations = await store.query('sync/destination', {});
|
||||||
|
this.activatedFeatures = ['secrets-sync'];
|
||||||
|
|
||||||
await render(
|
this.renderComponent = () =>
|
||||||
hbs`<Secrets::Page::Overview @destinations={{this.destinations}} @totalVaultSecrets={{7}} />`,
|
render(
|
||||||
{
|
hbs`<Secrets::Page::Overview @destinations={{this.destinations}} @totalVaultSecrets={{7}} @activatedFeatures={{this.activatedFeatures}} @isAdapterError={{false}} />`,
|
||||||
owner: this.engine,
|
{
|
||||||
}
|
owner: this.engine,
|
||||||
);
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it should render landing cta component for community', async function (assert) {
|
test('it should render landing cta component for community', async function (assert) {
|
||||||
this.version.type = 'community';
|
this.version.type = 'community';
|
||||||
this.set('destinations', []);
|
this.destinations = [];
|
||||||
await settled();
|
|
||||||
|
await this.renderComponent();
|
||||||
|
|
||||||
assert.dom(title).hasText('Secrets Sync Enterprise feature', 'Page title renders');
|
assert.dom(title).hasText('Secrets Sync Enterprise feature', 'Page title renders');
|
||||||
assert.dom(cta.button).doesNotExist('Create first destination button does not render');
|
assert.dom(cta.button).doesNotExist('Create first destination button does not render');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it should render landing cta component for enterprise', async function (assert) {
|
test('it should render landing cta component for enterprise', async function (assert) {
|
||||||
this.set('destinations', []);
|
this.destinations = [];
|
||||||
await settled();
|
|
||||||
|
await this.renderComponent();
|
||||||
|
|
||||||
assert.dom(title).hasText('Secrets Sync', 'Page title renders');
|
assert.dom(title).hasText('Secrets Sync', 'Page title renders');
|
||||||
assert.dom(cta.button).hasText('Create first destination', 'CTA action renders');
|
assert.dom(cta.button).hasText('Create first destination', 'CTA action renders');
|
||||||
assert.dom(cta.summary).exists('CTA renders');
|
assert.dom(cta.summary).exists('CTA renders');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it should render header, tabs and toolbar for overview state', async function (assert) {
|
test('it should render header, tabs and toolbar for overview state', async function (assert) {
|
||||||
|
await this.renderComponent();
|
||||||
|
|
||||||
assert.dom(title).hasText('Secrets Sync', 'Page title renders');
|
assert.dom(title).hasText('Secrets Sync', 'Page title renders');
|
||||||
assert.dom(breadcrumb).exists({ count: 1 }, 'Correct number of breadcrumbs render');
|
assert.dom(breadcrumb).exists({ count: 1 }, 'Correct number of breadcrumbs render');
|
||||||
assert.dom(breadcrumb).includesText('Secrets Sync', 'Top level breadcrumb renders');
|
assert.dom(breadcrumb).includesText('Secrets Sync', 'Top level breadcrumb renders');
|
||||||
@@ -82,6 +90,9 @@ module('Integration | Component | sync | Page::Overview', function (hooks) {
|
|||||||
[new Date('2023-09-20T10:51:53.961861096-04:00'), 'MMMM do yyyy, h:mm:ss a'],
|
[new Date('2023-09-20T10:51:53.961861096-04:00'), 'MMMM do yyyy, h:mm:ss a'],
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await this.renderComponent();
|
||||||
|
|
||||||
assert
|
assert
|
||||||
.dom(overviewCard.title('Secrets by destination'))
|
.dom(overviewCard.title('Secrets by destination'))
|
||||||
.hasText('Secrets by destination', 'Overview card title renders for table');
|
.hasText('Secrets by destination', 'Overview card title renders for table');
|
||||||
@@ -110,6 +121,8 @@ module('Integration | Component | sync | Page::Overview', function (hooks) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('it should paginate secrets by destination table', async function (assert) {
|
test('it should paginate secrets by destination table', async function (assert) {
|
||||||
|
await this.renderComponent();
|
||||||
|
|
||||||
const { name, row } = overview.table;
|
const { name, row } = overview.table;
|
||||||
assert.dom(row).exists({ count: 3 }, 'Correct number of table rows render based on page size');
|
assert.dom(row).exists({ count: 3 }, 'Correct number of table rows render based on page size');
|
||||||
assert.dom(name(0)).hasText('destination-aws', 'First destination renders on page 1');
|
assert.dom(name(0)).hasText('destination-aws', 'First destination renders on page 1');
|
||||||
@@ -124,9 +137,9 @@ module('Integration | Component | sync | Page::Overview', function (hooks) {
|
|||||||
this.server.get('/sys/sync/destinations/:type/:name/associations', () => {
|
this.server.get('/sys/sync/destinations/:type/:name/associations', () => {
|
||||||
return new Response(403, {}, { errors: ['Permission denied'] });
|
return new Response(403, {}, { errors: ['Permission denied'] });
|
||||||
});
|
});
|
||||||
// since the request resolved trigger a page change and return an error from the associations endpoint
|
|
||||||
await click(pagination.next);
|
await this.renderComponent();
|
||||||
await settled();
|
|
||||||
assert.dom(emptyStateTitle).hasText('Error fetching information', 'Empty state title renders');
|
assert.dom(emptyStateTitle).hasText('Error fetching information', 'Empty state title renders');
|
||||||
assert
|
assert
|
||||||
.dom(emptyStateMessage)
|
.dom(emptyStateMessage)
|
||||||
@@ -134,6 +147,8 @@ module('Integration | Component | sync | Page::Overview', function (hooks) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('it should render totals cards', async function (assert) {
|
test('it should render totals cards', async function (assert) {
|
||||||
|
await this.renderComponent();
|
||||||
|
|
||||||
const { title, description, action, content } = overviewCard;
|
const { title, description, action, content } = overviewCard;
|
||||||
const cardData = [
|
const cardData = [
|
||||||
{
|
{
|
||||||
|
Reference in New Issue
Block a user