diff --git a/.github/labler.yml b/.github/labeler.yml
similarity index 100%
rename from .github/labler.yml
rename to .github/labeler.yml
diff --git a/.github/workflows/labler.yml b/.github/workflows/labeler.yml
similarity index 100%
rename from .github/workflows/labler.yml
rename to .github/workflows/labeler.yml
diff --git a/addons/auth/addon/authenticators/password.js b/addons/auth/addon/authenticators/password.js
index 7403eae65..aa0b13e87 100644
--- a/addons/auth/addon/authenticators/password.js
+++ b/addons/auth/addon/authenticators/password.js
@@ -93,19 +93,24 @@ export default class PasswordAuthenticator extends BaseAuthenticator {
const authEndpointURL = this.buildAuthEndpointURL(options);
const response = await fetch(authEndpointURL, { method: 'post', body });
const json = await response.json();
- return response.status < 400 ? resolve(this.normalizeData(json)) : reject();
+ return response.status < 400
+ ? resolve(this.normalizeData(json, login_name))
+ : reject();
}
/**
* Normalizes the auth data. Adds convenience booleans depending on the
* scope within which the session is authenticated: isGlobal, isOrg.
+ * If a `username` is provided, appends this to the data.
* @param {object} data
+ * @param {string} username
* @return {object}
*/
- normalizeData(data) {
+ normalizeData(data, username) {
// Add booleans indicated the scope type
data.isGlobal = (data?.scope?.type === 'global');
data.isOrg = (data?.scope?.type === 'org');
+ if (username) data.username = username;
return data;
}
diff --git a/addons/rose/addon/components/rose/card/link/index.hbs b/addons/rose/addon/components/rose/card/link/index.hbs
index 7f003a4a6..ead6f0ee9 100644
--- a/addons/rose/addon/components/rose/card/link/index.hbs
+++ b/addons/rose/addon/components/rose/card/link/index.hbs
@@ -4,17 +4,30 @@
@route={{if @route @route "index"}}
@model={{@model}}
>
- {{#if @title}}
-
{{@title}}
- {{/if}}
+
- {{#if @subtitle}}
- {{@subtitle}}
- {{/if}}
+
+ {{#if @description}}
+
{{@description}}
+ {{/if}}
+
- {{#if @description}}
- {{@description}}
- {{/if}}
+
{{yield}}
diff --git a/addons/rose/addon/styles/rose/components/card/_link.scss b/addons/rose/addon/styles/rose/components/card/_link.scss
index a62df13f8..ad33c49c5 100644
--- a/addons/rose/addon/styles/rose/components/card/_link.scss
+++ b/addons/rose/addon/styles/rose/components/card/_link.scss
@@ -1,6 +1,10 @@
+@use 'sass:map';
@use '../../variables/sizing';
+@use '../../variables/typography';
@use '../../utilities/type';
+$xxxs: sizing.rems(xxxs); // 2
+
.rose-card-link {
@include type.type(s);
border: sizing.rems(xxxxs) solid var(--ui-gray-subtler-2);
@@ -9,19 +13,21 @@
display: inline-flex;
flex-direction: column;
margin: 0 sizing.rems(m) sizing.rems(m) 0;
+ box-shadow: 0 $xxxs $xxxs rgba(var(--stark-components), var(--opacity-4)),
+ 0 $xxxs $xxxs rgba(var(--stark-components), var(--opacity-4));
+
+ @media (prefers-color-scheme: dark) {
+ background-color: var(--ui-gray-subtler-6);
+ }
a {
text-decoration: none;
margin: 0;
padding: 0;
- display: flex;
- justify-content: space-between;
- flex-direction: column;
height: 100%;
- padding: sizing.rems(s);
// TODO: Figure out better method of defining card dimensions
- min-height: sizing.rems(m) * 6; //6 rems
- width: sizing.rems(m) * 12; //12 rems
+ min-height: sizing.rems(m) * 8; //6 rems
+ width: sizing.rems(m) * 16; //12 rems
}
&:hover {
@@ -42,27 +48,78 @@
border-color: var(--info);
}
- .rose-card-link-title {
- $truncate-line-count: 3;
- @include type.type(m, semibold);
- color: var(--stark);
- margin: 0;
- display: -webkit-box;
- -webkit-box-orient: vertical;
- -webkit-line-clamp: $truncate-line-count;
- overflow: hidden;
+ .rose-card-link-header,
+ .rose-card-link-body,
+ .rose-card-link-footer {
+ padding-left: sizing.rems(m);
+ padding-right: sizing.rems(m);
}
- .rose-card-link-subtitle {
- color: var(--ui-gray);
+ .rose-card-link-header {
+ padding-top: sizing.rems(m);
+ }
+ .rose-card-link-footer {
+ margin-top: sizing.rems(xs) + sizing.rems(xxs);
+ padding-top: sizing.rems(xs) * 2;
+ padding-bottom: sizing.rems(xs) * 2;
+ }
+
+ .rose-card-link-footer {
+ border-top: sizing.rems(xxxxs) solid var(--ui-gray-subtler-2);
+ }
+
+ .rose-card-link-title {
+ @include type.type(m, semibold);
+ display: flex;
+ color: var(--stark);
margin: 0;
+ margin-bottom: sizing.rems(xs);
+
+ .rose-icon {
+ margin-right: sizing.rems(xs);
+
+ svg {
+ vertical-align: text-bottom;
+ }
+ }
+
+ .rose-card-link-title-text {
+ flex-grow: 1;
+ height: typography.line-height(m) * 2rem;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ }
}
.rose-card-link-description {
- margin: auto 0 0;
color: var(--ui-gray);
- white-space: nowrap;
+ margin: 0;
+ height: typography.line-height(m) * 2rem;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
overflow: hidden;
- text-overflow: ellipsis;
}
+
+ .rose-card-link-footer-id {
+ @include type.type(xs);
+ color: var(--ui-gray);
+ margin: 0;
+
+ &::before {
+ content: "ID";
+ color: var(--stark);
+ padding-right: sizing.rems(xs);
+ }
+ }
+
+ // .rose-card-link-description {
+ // margin: auto 0 0;
+ // color: var(--ui-gray);
+ // white-space: nowrap;
+ // overflow: hidden;
+ // text-overflow: ellipsis;
+ // }
}
diff --git a/addons/rose/addon/styles/rose/components/header/_index.scss b/addons/rose/addon/styles/rose/components/header/_index.scss
index 6ba8aba30..8885c0207 100644
--- a/addons/rose/addon/styles/rose/components/header/_index.scss
+++ b/addons/rose/addon/styles/rose/components/header/_index.scss
@@ -12,7 +12,12 @@
display: grid;
grid-template-areas: 'brand nav utilities';
grid-template-columns: 0.01fr auto 1fr;
- background: var(--black) linear-gradient(90deg, #191a1c 0%, #1b212d 100%);
+ background: var(--black);
+ box-shadow: inset 0 -#{sizing.rems(xxxxs)} 0 var(--ui-border-subtler-1);
+
+ @media (prefers-color-scheme: light) {
+ box-shadow: none;
+ }
}
@media (max-width: media.width(small)) {
diff --git a/addons/rose/addon/styles/rose/components/nav/_sidebar.scss b/addons/rose/addon/styles/rose/components/nav/_sidebar.scss
index 101f3753f..fcc29ca45 100644
--- a/addons/rose/addon/styles/rose/components/nav/_sidebar.scss
+++ b/addons/rose/addon/styles/rose/components/nav/_sidebar.scss
@@ -39,6 +39,7 @@
&.active {
color: var(--action);
border-color: currentColor;
+ background-color: var(--action-subtler-1h);
}
&:hover {
diff --git a/addons/rose/addon/styles/rose/variables/color/_css.scss b/addons/rose/addon/styles/rose/variables/color/_css.scss
index cb000c5df..5c910e93a 100644
--- a/addons/rose/addon/styles/rose/variables/color/_css.scss
+++ b/addons/rose/addon/styles/rose/variables/color/_css.scss
@@ -83,7 +83,7 @@
@media (prefers-color-scheme: dark) {
:root {
// poles
- --subtle: #{darken(color(ui-gray, 900), 3%)};
+ --subtle: var(--black);
--stark: #{color(ui-gray, 100)};
--stark-components: 255, 255, 255;
diff --git a/addons/rose/tests/integration/components/rose/card/link-test.js b/addons/rose/tests/integration/components/rose/card/link-test.js
index 78a02af4b..288679791 100644
--- a/addons/rose/tests/integration/components/rose/card/link-test.js
+++ b/addons/rose/tests/integration/components/rose/card/link-test.js
@@ -10,9 +10,9 @@ module('Integration | Component | rose/card/link', function (hooks) {
await render(hbs``);
assert.ok(find('article'));
assert.ok(find('.rose-card-link'));
- assert.notOk(find('.rose-card-link-title'));
- assert.notOk(find('.rose-card-link-subtitle'));
- assert.notOk(find('.rose-card-link-description'));
+ assert.ok(find('.rose-card-link-header'));
+ assert.ok(find('.rose-card-link-body'));
+ assert.ok(find('.rose-card-link-footer'));
});
test('it renders with @title', async function (assert) {
@@ -24,22 +24,27 @@ module('Integration | Component | rose/card/link', function (hooks) {
);
});
- test('it renders with @subtitle', async function (assert) {
- await render(hbs``);
- assert.equal(
- find('.rose-card-link-subtitle').textContent.trim(),
- 'card subtitle'
- );
- });
-
test('it renders with @description', async function (assert) {
await render(hbs``);
assert.equal(
- find('.rose-card-link-description').textContent.trim(),
+ find('.rose-card-link-body').textContent.trim(),
'card description'
);
});
+ test('it renders with @id', async function (assert) {
+ await render(hbs``);
+ assert.equal(
+ find('.rose-card-link-footer-id').textContent.trim(),
+ 'card_id'
+ );
+ });
+
+ test('it renders with @icon', async function (assert) {
+ await render(hbs``);
+ assert.ok(find('.rose-icon'));
+ });
+
test('it renders with attributes', async function (assert) {
await render(hbs``);
assert.ok(find('#card'));
diff --git a/ui/core/app/routes/scopes/scope/auth-methods.js b/ui/core/app/routes/scopes/scope/auth-methods.js
index f7bbe65c5..0b4072276 100644
--- a/ui/core/app/routes/scopes/scope/auth-methods.js
+++ b/ui/core/app/routes/scopes/scope/auth-methods.js
@@ -2,6 +2,7 @@ import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import loading from 'ember-loading/decorator';
+import { confirm } from '../../../utilities/confirm';
export default class ScopesScopeAuthMethodsRoute extends Route {
// =services
@@ -71,10 +72,11 @@ export default class ScopesScopeAuthMethodsRoute extends Route {
*/
@action
@loading
+ @confirm('questions.delete-confirm')
async delete(authMethod) {
try {
await authMethod.destroyRecord();
- await this.transitionTo('scopes.scope.auth-methods');
+ await this.replaceWith('scopes.scope.auth-methods');
this.refresh();
this.notify.success(this.intl.t('notifications.delete-success'));
} catch (error) {
diff --git a/ui/core/app/routes/scopes/scope/auth-methods/auth-method/accounts.js b/ui/core/app/routes/scopes/scope/auth-methods/auth-method/accounts.js
index fbd28041a..306acb0b8 100644
--- a/ui/core/app/routes/scopes/scope/auth-methods/auth-method/accounts.js
+++ b/ui/core/app/routes/scopes/scope/auth-methods/auth-method/accounts.js
@@ -2,6 +2,7 @@ import Route from '@ember/routing/route';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import loading from 'ember-loading/decorator';
+import { confirm } from '../../../../../utilities/confirm';
export default class ScopesScopeAuthMethodsAuthMethodAccountsRoute extends Route {
// =services
@@ -71,10 +72,11 @@ export default class ScopesScopeAuthMethodsAuthMethodAccountsRoute extends Route
*/
@action
@loading
- async delete(account) {
+ @confirm('questions.delete-confirm')
+ async deleteAccount(account) {
try {
await account.destroyRecord();
- await this.transitionTo('scopes.scope.auth-methods.auth-method.accounts');
+ await this.replaceWith('scopes.scope.auth-methods.auth-method.accounts');
this.refresh();
this.notify.success(this.intl.t('notifications.delete-success'));
} catch (error) {
diff --git a/ui/core/app/routes/scopes/scope/authenticate.js b/ui/core/app/routes/scopes/scope/authenticate.js
index d011a6b45..883bbb704 100644
--- a/ui/core/app/routes/scopes/scope/authenticate.js
+++ b/ui/core/app/routes/scopes/scope/authenticate.js
@@ -9,6 +9,17 @@ export default class ScopesScopeAuthenticateRoute extends Route {
// =methods
+ beforeModel() {
+ if (this.session.isAuthenticated) {
+ const scope = this.modelFor('scopes.scope');
+ if (scope.isGlobal) {
+ this.transitionTo('scopes.scope.orgs');
+ } else {
+ this.transitionTo('scopes.scope.projects');
+ }
+ }
+ }
+
/**
* Returns all auth methods for the current scope, along with the current
* scope and all scopes (for org navigation).
@@ -28,15 +39,4 @@ export default class ScopesScopeAuthenticateRoute extends Route {
// }])
});
}
-
- redirect() {
- if (this.session.isAuthenticated) {
- const scope = this.modelFor('scopes.scope');
- if (scope.isGlobal) {
- this.transitionTo('scopes.scope.orgs');
- } else {
- this.transitionTo('scopes.scope.projects');
- }
- }
- }
}
diff --git a/ui/core/app/routes/scopes/scope/groups.js b/ui/core/app/routes/scopes/scope/groups.js
index 66ac9d8bd..26b5a3f61 100644
--- a/ui/core/app/routes/scopes/scope/groups.js
+++ b/ui/core/app/routes/scopes/scope/groups.js
@@ -2,6 +2,7 @@ import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import loading from 'ember-loading/decorator';
+import { confirm } from '../../../utilities/confirm';
export default class ScopesScopeGroupsRoute extends Route {
// =services
@@ -68,12 +69,13 @@ export default class ScopesScopeGroupsRoute extends Route {
*/
@action
@loading
+ @confirm('questions.delete-confirm')
async delete(group) {
try {
await group.destroyRecord();
+ await this.replaceWith('scopes.scope.groups');
this.refresh();
this.notify.success(this.intl.t('notifications.delete-success'));
- await this.transitionTo('scopes.scope.groups');
} catch (error) {
//TODO: replace with translated strings
this.notify.error(error.message, { closeAfter: null });
diff --git a/ui/core/app/routes/scopes/scope/groups/group/members.js b/ui/core/app/routes/scopes/scope/groups/group/members.js
index 8b3c34bc4..c7f6d52ff 100644
--- a/ui/core/app/routes/scopes/scope/groups/group/members.js
+++ b/ui/core/app/routes/scopes/scope/groups/group/members.js
@@ -3,6 +3,7 @@ import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import { hash, all } from 'rsvp';
import loading from 'ember-loading/decorator';
+import { confirm } from '../../../../../utilities/confirm';
export default class ScopesScopeGroupsGroupMembersRoute extends Route {
// =services
@@ -38,10 +39,11 @@ export default class ScopesScopeGroupsGroupMembersRoute extends Route {
*/
@action
@loading
+ @confirm('questions.remove-confirm')
async removeMember(group, member) {
try {
await group.removeMember(member.id);
- await this.refresh();
+ this.refresh();
this.notify.success(this.intl.t('notifications.remove-success'));
} catch (error) {
// TODO: replace with translated strings
diff --git a/ui/core/app/routes/scopes/scope/projects.js b/ui/core/app/routes/scopes/scope/projects.js
index efd9d71f8..77f57f12d 100644
--- a/ui/core/app/routes/scopes/scope/projects.js
+++ b/ui/core/app/routes/scopes/scope/projects.js
@@ -2,6 +2,7 @@ import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import loading from 'ember-loading/decorator';
+import { confirm } from '../../../utilities/confirm';
export default class ScopesScopeProjectsRoute extends Route {
// =services
@@ -9,6 +10,7 @@ export default class ScopesScopeProjectsRoute extends Route {
@service intl;
@service notify;
@service session;
+ @service confirm;
// =methods
@@ -84,12 +86,13 @@ export default class ScopesScopeProjectsRoute extends Route {
*/
@action
@loading
+ @confirm('questions.delete-confirm')
async delete(project) {
try {
await project.destroyRecord();
+ await this.replaceWith('scopes.scope.projects');
this.refresh();
this.notify.success(this.intl.t('notifications.delete-success'));
- this.transitionTo('scopes.scope.projects');
} catch (error) {
// TODO: replace with translated strings
this.notify.error(error.message, { closeAfter: null });
diff --git a/ui/core/app/routes/scopes/scope/projects/project/host-catalogs.js b/ui/core/app/routes/scopes/scope/projects/project/host-catalogs.js
index 3cc9e212d..28058850f 100644
--- a/ui/core/app/routes/scopes/scope/projects/project/host-catalogs.js
+++ b/ui/core/app/routes/scopes/scope/projects/project/host-catalogs.js
@@ -2,6 +2,7 @@ import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import loading from 'ember-loading/decorator';
+import { confirm } from '../../../../../utilities/confirm';
export default class ScopesScopeProjectsProjectHostCatalogsRoute extends Route {
// =services
@@ -61,16 +62,17 @@ export default class ScopesScopeProjectsProjectHostCatalogsRoute extends Route {
/**
* Deletes the host catalog and redirects to index.
- * @param {ProjectModel} project
+ * @param {HostCatalogModel} hostCatalog
*/
@action
@loading
- async delete(project) {
+ @confirm('questions.delete-confirm')
+ async delete(hostCatalog) {
try {
- await project.destroyRecord();
+ await hostCatalog.destroyRecord();
+ await this.replaceWith('scopes.scope.projects.project.host-catalogs');
this.refresh();
this.notify.success(this.intl.t('notifications.delete-success'));
- this.transitionTo('scopes.scope.projects.project.host-catalogs');
} catch (error) {
// TODO: replace with translated strings
this.notify.error(error.message, { closeAfter: null });
diff --git a/ui/core/app/routes/scopes/scope/projects/project/host-catalogs/host-catalog/host-sets.js b/ui/core/app/routes/scopes/scope/projects/project/host-catalogs/host-catalog/host-sets.js
index affdabbe0..3591b4a4b 100644
--- a/ui/core/app/routes/scopes/scope/projects/project/host-catalogs/host-catalog/host-sets.js
+++ b/ui/core/app/routes/scopes/scope/projects/project/host-catalogs/host-catalog/host-sets.js
@@ -2,6 +2,7 @@ import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import loading from 'ember-loading/decorator';
+import { confirm } from '../../../../../../../utilities/confirm';
export default class ScopesScopeProjectsProjectHostCatalogsHostCatalogHostSetsRoute extends Route {
// =services
@@ -70,13 +71,14 @@ export default class ScopesScopeProjectsProjectHostCatalogsHostCatalogHostSetsRo
*/
@action
@loading
- async delete(hostSet) {
+ @confirm('questions.delete-confirm')
+ async deleteHostSet(hostSet) {
try {
await hostSet.destroyRecord();
- this.notify.success(this.intl.t('notifications.delete-success'));
- this.transitionTo(
+ await this.replaceWith(
'scopes.scope.projects.project.host-catalogs.host-catalog.host-sets'
);
+ this.notify.success(this.intl.t('notifications.delete-success'));
} catch (error) {
// TODO: replace with translated strings
this.notify.error(error.message, { closeAfter: null });
diff --git a/ui/core/app/routes/scopes/scope/projects/project/host-catalogs/host-catalog/host-sets/host-set/hosts.js b/ui/core/app/routes/scopes/scope/projects/project/host-catalogs/host-catalog/host-sets/host-set/hosts.js
index e13798b48..de4743209 100644
--- a/ui/core/app/routes/scopes/scope/projects/project/host-catalogs/host-catalog/host-sets/host-set/hosts.js
+++ b/ui/core/app/routes/scopes/scope/projects/project/host-catalogs/host-catalog/host-sets/host-set/hosts.js
@@ -3,6 +3,7 @@ import { hash, all } from 'rsvp';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import loading from 'ember-loading/decorator';
+import { confirm } from '../../../../../../../../../utilities/confirm';
export default class ScopesScopeProjectsProjectHostCatalogsHostCatalogHostSetsHostSetHostsRoute extends Route {
// =services
@@ -37,6 +38,7 @@ export default class ScopesScopeProjectsProjectHostCatalogsHostCatalogHostSetsHo
*/
@action
@loading
+ @confirm('questions.remove-confirm')
async removeHost(hostSet, host) {
try {
const scopeID = this.modelFor('scopes.scope.projects.project').id;
diff --git a/ui/core/app/routes/scopes/scope/projects/project/host-catalogs/host-catalog/hosts.js b/ui/core/app/routes/scopes/scope/projects/project/host-catalogs/host-catalog/hosts.js
index 031975c46..42dd70019 100644
--- a/ui/core/app/routes/scopes/scope/projects/project/host-catalogs/host-catalog/hosts.js
+++ b/ui/core/app/routes/scopes/scope/projects/project/host-catalogs/host-catalog/hosts.js
@@ -2,6 +2,7 @@ import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import loading from 'ember-loading/decorator';
+import { confirm } from '../../../../../../../utilities/confirm';
export default class ScopesScopeProjectsProjectHostCatalogsHostCatalogHostsRoute extends Route {
// =services
@@ -70,14 +71,15 @@ export default class ScopesScopeProjectsProjectHostCatalogsHostCatalogHostsRoute
*/
@action
@loading
- async delete(host) {
+ @confirm('questions.delete-confirm')
+ async deleteHost(host) {
try {
await host.destroyRecord();
- this.refresh();
- this.notify.success(this.intl.t('notifications.delete-success'));
- this.transitionTo(
+ await this.replaceWith(
'scopes.scope.projects.project.host-catalogs.host-catalog.hosts'
);
+ this.refresh();
+ this.notify.success(this.intl.t('notifications.delete-success'));
} catch (error) {
// TODO: replace with translated strings
this.notify.error(error.message, { closeAfter: null });
diff --git a/ui/core/app/routes/scopes/scope/projects/project/targets.js b/ui/core/app/routes/scopes/scope/projects/project/targets.js
index fa019bdc7..64f254467 100644
--- a/ui/core/app/routes/scopes/scope/projects/project/targets.js
+++ b/ui/core/app/routes/scopes/scope/projects/project/targets.js
@@ -2,6 +2,7 @@ import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import loading from 'ember-loading/decorator';
+import { confirm } from '../../../../../utilities/confirm';
export default class ScopesScopeProjectsProjectTargetsRoute extends Route {
// =services
@@ -62,12 +63,13 @@ export default class ScopesScopeProjectsProjectTargetsRoute extends Route {
*/
@action
@loading
+ @confirm('questions.delete-confirm')
async delete(target) {
try {
await target.destroyRecord();
+ await this.replaceWith('scopes.scope.projects.project.targets');
this.refresh();
this.notify.success(this.intl.t('notifications.delete-success'));
- this.transitionTo('scopes.scope.projects.project.targets');
} catch (error) {
// TODO: replace with translated strings
this.notify.error(error.message, { closeAfter: null });
diff --git a/ui/core/app/routes/scopes/scope/projects/project/targets/target/host-sets.js b/ui/core/app/routes/scopes/scope/projects/project/targets/target/host-sets.js
index 1633c5713..c87abdf0a 100644
--- a/ui/core/app/routes/scopes/scope/projects/project/targets/target/host-sets.js
+++ b/ui/core/app/routes/scopes/scope/projects/project/targets/target/host-sets.js
@@ -3,6 +3,7 @@ import { all, hash } from 'rsvp';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import loading from 'ember-loading/decorator';
+import { confirm } from '../../../../../../../utilities/confirm';
export default class ScopesScopeProjectsProjectTargetsTargetHostSetsRoute extends Route {
// =services
@@ -55,6 +56,7 @@ export default class ScopesScopeProjectsProjectTargetsTargetHostSetsRoute extend
*/
@action
@loading
+ @confirm('questions.remove-confirm')
async removeHostSet(target, hostSet) {
try {
await target.removeHostSet(hostSet.id);
diff --git a/ui/core/app/routes/scopes/scope/roles.js b/ui/core/app/routes/scopes/scope/roles.js
index c9e907069..b7eb93adc 100644
--- a/ui/core/app/routes/scopes/scope/roles.js
+++ b/ui/core/app/routes/scopes/scope/roles.js
@@ -2,6 +2,7 @@ import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import loading from 'ember-loading/decorator';
+import { confirm } from '../../../utilities/confirm';
export default class ScopesScopeRolesRoute extends Route {
// =services
@@ -68,12 +69,13 @@ export default class ScopesScopeRolesRoute extends Route {
*/
@action
@loading
+ @confirm('questions.delete-confirm')
async delete(role) {
try {
await role.destroyRecord();
+ await this.replaceWith('scopes.scope.roles');
this.refresh();
this.notify.success(this.intl.t('notifications.delete-success'));
- this.transitionTo('scopes.scope.roles');
} catch (error) {
// TODO: replace with translated strings
this.notify.error(error.message, { closeAfter: null });
diff --git a/ui/core/app/routes/scopes/scope/roles/role/principals.js b/ui/core/app/routes/scopes/scope/roles/role/principals.js
index 045d568ce..8fde07b08 100644
--- a/ui/core/app/routes/scopes/scope/roles/role/principals.js
+++ b/ui/core/app/routes/scopes/scope/roles/role/principals.js
@@ -3,6 +3,7 @@ import { all, hash } from 'rsvp';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import loading from 'ember-loading/decorator';
+import { confirm } from '../../../../../utilities/confirm';
export default class ScopesScopeRolesRolePrincipalsRoute extends Route {
// =services
@@ -65,6 +66,7 @@ export default class ScopesScopeRolesRolePrincipalsRoute extends Route {
*/
@action
@loading
+ @confirm('questions.remove-confirm')
async removePrincipal(principal) {
try {
const role = this.modelFor('scopes.scope.roles.role');
diff --git a/ui/core/app/routes/scopes/scope/users.js b/ui/core/app/routes/scopes/scope/users.js
index 563cf13d8..5628a5179 100644
--- a/ui/core/app/routes/scopes/scope/users.js
+++ b/ui/core/app/routes/scopes/scope/users.js
@@ -2,6 +2,7 @@ import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import loading from 'ember-loading/decorator';
+import { confirm } from '../../../utilities/confirm';
export default class ScopesScopeUsersRoute extends Route {
// =services
@@ -69,12 +70,13 @@ export default class ScopesScopeUsersRoute extends Route {
*/
@action
@loading
+ @confirm('questions.delete-confirm')
async delete(user) {
try {
await user.destroyRecord();
+ await this.replaceWith('scopes.scope.users');
this.refresh();
this.notify.success(this.intl.t('notifications.delete-success'));
- this.transitionTo('scopes.scope.users');
} catch (error) {
// TODO: replace with translated strings
this.notify.error(error.message, { closeAfter: null });
diff --git a/ui/core/app/services/confirm.js b/ui/core/app/services/confirm.js
new file mode 100644
index 000000000..d3c0e4fe8
--- /dev/null
+++ b/ui/core/app/services/confirm.js
@@ -0,0 +1,53 @@
+import Service from '@ember/service';
+import { getOwner } from '@ember/application';
+import { bind } from '@ember/runloop';
+import { Promise, resolve } from 'rsvp';
+import config from '../config/environment';
+
+export default class ConfirmService extends Service {
+
+ // =attributes
+
+ /**
+ * When the confirm service is disabled, it always returns resolving promises.
+ * @type {boolean}
+ */
+ enabled = config.enableConfirmService;
+
+ /**
+ * The window's confirm function, bound to the window scope.
+ * @type {Function}
+ */
+ get confirmFn() {
+ // The Ember way of accessing globals...
+ const document =
+ getOwner(this).lookup('service:-document').documentElement;
+ // defaultView === window, but without using globals directly
+ const windowRef = document.parentNode.defaultView;
+ const { confirm } = windowRef;
+ return bind(windowRef, confirm);
+ }
+
+ // =methods
+
+ /**
+ * When the service is enabled, returns a promise that resolves if the user
+ * accepts the confirmation, otherwise rejects. If the service is disabled,
+ * this method always returns a resolving promise.
+ * @param {string} text
+ * @return {Promise}
+ */
+ confirm(text) {
+ if (this.enabled) {
+ return new Promise((resolve, reject) => {
+ if (this.confirmFn(text)) {
+ return resolve();
+ } else {
+ return reject();
+ }
+ });
+ } else {
+ return resolve();
+ }
+ }
+}
diff --git a/ui/core/app/styles/app.scss b/ui/core/app/styles/app.scss
index fbdbea206..052ca77c0 100644
--- a/ui/core/app/styles/app.scss
+++ b/ui/core/app/styles/app.scss
@@ -76,7 +76,7 @@
&::after {
animation: gradient 1s linear infinite;
background: repeating-linear-gradient(to right, var(--subtle), var(--subtle), var(--info), var(--subtle), var(--subtle));
- background-position: 0 100%;
+ background-position: -100% 0;
background-size: 200% auto;
content: ' ';
height: 3px;
@@ -86,7 +86,7 @@
top: 100%;
@keyframes gradient {
- 0% { background-position: 0 0; }
+ 0% { background-position: -100% 0; }
100% { background-position: -200% 0; }
}
}
diff --git a/ui/core/app/templates/application.hbs b/ui/core/app/templates/application.hbs
index 80472c19a..29dfe48eb 100644
--- a/ui/core/app/templates/application.hbs
+++ b/ui/core/app/templates/application.hbs
@@ -34,7 +34,7 @@
{{!--
diff --git a/ui/core/app/templates/scopes/scope/auth-methods/auth-method/accounts/account/-actions.hbs b/ui/core/app/templates/scopes/scope/auth-methods/auth-method/accounts/account/-actions.hbs
index ea1b0115d..fb179b954 100644
--- a/ui/core/app/templates/scopes/scope/auth-methods/auth-method/accounts/account/-actions.hbs
+++ b/ui/core/app/templates/scopes/scope/auth-methods/auth-method/accounts/account/-actions.hbs
@@ -2,7 +2,7 @@
{{t 'actions.delete'}}
diff --git a/ui/core/app/templates/scopes/scope/orgs/index.hbs b/ui/core/app/templates/scopes/scope/orgs/index.hbs
index ed79b4e04..30c211450 100644
--- a/ui/core/app/templates/scopes/scope/orgs/index.hbs
+++ b/ui/core/app/templates/scopes/scope/orgs/index.hbs
@@ -30,8 +30,9 @@
{{#each @model as |org|}}
diff --git a/ui/core/app/templates/scopes/scope/projects/index.hbs b/ui/core/app/templates/scopes/scope/projects/index.hbs
index 0d05ddf30..6173aeffb 100644
--- a/ui/core/app/templates/scopes/scope/projects/index.hbs
+++ b/ui/core/app/templates/scopes/scope/projects/index.hbs
@@ -30,8 +30,9 @@
{{#each @model as |project|}}
diff --git a/ui/core/app/templates/scopes/scope/projects/project/host-catalogs/host-catalog/-actions.hbs b/ui/core/app/templates/scopes/scope/projects/project/host-catalogs/host-catalog/-actions.hbs
index 557d572ba..adbf2705f 100644
--- a/ui/core/app/templates/scopes/scope/projects/project/host-catalogs/host-catalog/-actions.hbs
+++ b/ui/core/app/templates/scopes/scope/projects/project/host-catalogs/host-catalog/-actions.hbs
@@ -19,4 +19,4 @@
>
{{t 'resources.host-catalog.actions.delete'}}
-
\ No newline at end of file
+
diff --git a/ui/core/app/templates/scopes/scope/projects/project/host-catalogs/host-catalog/host-sets/-actions.hbs b/ui/core/app/templates/scopes/scope/projects/project/host-catalogs/host-catalog/host-sets/-actions.hbs
index 0b6331854..cbf2d45cc 100644
--- a/ui/core/app/templates/scopes/scope/projects/project/host-catalogs/host-catalog/host-sets/-actions.hbs
+++ b/ui/core/app/templates/scopes/scope/projects/project/host-catalogs/host-catalog/host-sets/-actions.hbs
@@ -5,8 +5,8 @@
{{t 'resources.host-set.actions.delete'}}
-
\ No newline at end of file
+
diff --git a/ui/core/app/templates/scopes/scope/projects/project/host-catalogs/host-catalog/hosts/-actions.hbs b/ui/core/app/templates/scopes/scope/projects/project/host-catalogs/host-catalog/hosts/-actions.hbs
index d01333c17..4c9a91aa3 100644
--- a/ui/core/app/templates/scopes/scope/projects/project/host-catalogs/host-catalog/hosts/-actions.hbs
+++ b/ui/core/app/templates/scopes/scope/projects/project/host-catalogs/host-catalog/hosts/-actions.hbs
@@ -2,8 +2,8 @@
{{t 'resources.host.actions.delete'}}
-
\ No newline at end of file
+
diff --git a/ui/core/app/utilities/confirm.js b/ui/core/app/utilities/confirm.js
new file mode 100644
index 000000000..755ff2f59
--- /dev/null
+++ b/ui/core/app/utilities/confirm.js
@@ -0,0 +1,23 @@
+import { getOwner } from '@ember/application';
+
+/**
+ * Decorates a method whereby calls to the method will first request
+ * confirmation via the confirmation service. If the user accepts, the
+ * original method is executed. If the user denies, it is not.
+ */
+export function confirm(confirmationTextKey) {
+ return function (_target, _propertyKey, desc) {
+ const method = desc.value;
+ desc.value = function () {
+ const owner = getOwner(this);
+ const confirmService = owner.lookup('service:confirm');
+ const intl = owner.lookup('service:intl');
+ const text = intl.t(confirmationTextKey);
+ return confirmService.confirm(text)
+ // If user confirmed, execute the decorated method
+ .then(() => method.apply(this, arguments))
+ // If the user denied, gracefully handle promise rejection with no-op
+ .catch(() => {});
+ };
+ }
+}
diff --git a/ui/core/config/environment.js b/ui/core/config/environment.js
index f2c6362f9..a6f794d02 100644
--- a/ui/core/config/environment.js
+++ b/ui/core/config/environment.js
@@ -34,6 +34,7 @@ module.exports = function (environment) {
appName: APP_NAME,
notifyTimeout: 4000,
+ enableConfirmService: true,
contentSecurityPolicyHeader: 'Content-Security-Policy',
contentSecurityPolicyMeta: true,
@@ -98,6 +99,7 @@ module.exports = function (environment) {
// Notification timeout should be 0 for fast tests
ENV.notifyTimeout = 0;
+ ENV.enableConfirmService = false;
}
if (environment === 'production') {
diff --git a/ui/core/mirage/factories/org.js b/ui/core/mirage/factories/org.js
deleted file mode 100644
index fe119cba6..000000000
--- a/ui/core/mirage/factories/org.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import factory from '../generated/factories/org';
-
-export default factory.extend();
diff --git a/ui/core/mirage/factories/project.js b/ui/core/mirage/factories/project.js
deleted file mode 100644
index 2a3aae2be..000000000
--- a/ui/core/mirage/factories/project.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import factory from '../generated/factories/project';
-
-export default factory.extend();
diff --git a/ui/core/mirage/factories/scope.js b/ui/core/mirage/factories/scope.js
index d60486d15..a14dbc719 100644
--- a/ui/core/mirage/factories/scope.js
+++ b/ui/core/mirage/factories/scope.js
@@ -5,6 +5,16 @@ export default factory.extend({
type: 'global',
+ /**
+ * Generates realistic-ish IDs while still being deterministic.
+ */
+ id(i) {
+ let id = 'tv6fv3yz8p';
+ if (i % 3) id = 'xk1i4dk50v';
+ if (i % 2) id = 's9n819zgko';
+ return `s_${id}${i}`;
+ },
+
withChildren: trait({
afterCreate(record, server) {
if (record.type === 'global') {
diff --git a/ui/core/package.json b/ui/core/package.json
index 6b6547fbd..a22b04aee 100644
--- a/ui/core/package.json
+++ b/ui/core/package.json
@@ -86,7 +86,8 @@
"qunit-dom": "^1.0.0",
"rose": "*",
"sass": "^1.26.5",
- "sass-lint": "^1.13.1"
+ "sass-lint": "^1.13.1",
+ "sinon": "^9.1.0"
},
"engines": {
"node": "10.* || >= 12"
diff --git a/ui/core/tests/acceptance/projects-test.js b/ui/core/tests/acceptance/projects-test.js
index 0483a56c8..9a3830c40 100644
--- a/ui/core/tests/acceptance/projects-test.js
+++ b/ui/core/tests/acceptance/projects-test.js
@@ -10,6 +10,7 @@ import {
//currentSession,
//invalidateSession,
} from 'ember-simple-auth/test-support';
+import sinon from 'sinon';
module('Acceptance | projects', function (hooks) {
setupApplicationTest(hooks);
@@ -141,6 +142,28 @@ module('Acceptance | projects', function (hooks) {
assert.equal(getProjectScopesCount(), initialProjectScopesCount - 1);
});
+ test('can delete project with confirmation acceptance', async function (assert) {
+ assert.expect(2);
+ const confirmService = this.owner.lookup('service:confirm');
+ confirmService.enabled = true;
+ window.confirm = sinon.fake.returns(true);
+ await visit(existingProjectURL);
+ await click('.rose-layout-page-actions .rose-dropdown-button-danger');
+ assert.equal(getProjectScopesCount(), initialProjectScopesCount - 1);
+ assert.ok(window.confirm.calledOnce);
+ });
+
+ test('cannot delete project with confirmation denial', async function (assert) {
+ assert.expect(2);
+ const confirmService = this.owner.lookup('service:confirm');
+ confirmService.enabled = true;
+ window.confirm = sinon.fake.returns(false);
+ await visit(existingProjectURL);
+ await click('.rose-layout-page-actions .rose-dropdown-button-danger');
+ assert.equal(getProjectScopesCount(), initialProjectScopesCount);
+ assert.ok(window.confirm.calledOnce);
+ });
+
test('saving an existing project with invalid fields displays error messages', async function (assert) {
assert.expect(2);
this.server.patch('/scopes/:id', () => {
diff --git a/ui/core/tests/unit/services/confirm-test.js b/ui/core/tests/unit/services/confirm-test.js
new file mode 100644
index 000000000..c573fc2ed
--- /dev/null
+++ b/ui/core/tests/unit/services/confirm-test.js
@@ -0,0 +1,12 @@
+import { module, test } from 'qunit';
+import { setupTest } from 'ember-qunit';
+
+module('Unit | Service | confirm', function(hooks) {
+ setupTest(hooks);
+
+ // Replace this with your real tests.
+ test('it exists', function(assert) {
+ let service = this.owner.lookup('service:confirm');
+ assert.ok(service);
+ });
+});
diff --git a/ui/core/translations/en-us.yaml b/ui/core/translations/en-us.yaml
index 6755b8976..84b842579 100644
--- a/ui/core/translations/en-us.yaml
+++ b/ui/core/translations/en-us.yaml
@@ -18,6 +18,9 @@ titles:
actions: Actions
descriptions:
empty-set: There are no items to display yet. You may be able to add items or try back later.
+questions:
+ delete-confirm: Are you sure you want to delete this resource?
+ remove-confirm: Are you sure you want to remove this association?
hashicorp:
title: HashiCorp
copyright: © 2020 HashiCorp
diff --git a/yarn.lock b/yarn.lock
index f13b0ae16..32b4f960c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2975,6 +2975,42 @@
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd"
integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==
+"@sinonjs/commons@^1", "@sinonjs/commons@^1.6.0", "@sinonjs/commons@^1.7.0", "@sinonjs/commons@^1.7.2":
+ version "1.8.1"
+ resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.1.tgz#e7df00f98a203324f6dc7cc606cad9d4a8ab2217"
+ integrity sha512-892K+kWUUi3cl+LlqEWIDrhvLgdL79tECi8JZUyq6IviKy/DNhuzCRlbHUjxK89f4ypPMMaFnFuR9Ie6DoIMsw==
+ dependencies:
+ type-detect "4.0.8"
+
+"@sinonjs/fake-timers@^6.0.0", "@sinonjs/fake-timers@^6.0.1":
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz#293674fccb3262ac782c7aadfdeca86b10c75c40"
+ integrity sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==
+ dependencies:
+ "@sinonjs/commons" "^1.7.0"
+
+"@sinonjs/formatio@^5.0.1":
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-5.0.1.tgz#f13e713cb3313b1ab965901b01b0828ea6b77089"
+ integrity sha512-KaiQ5pBf1MpS09MuA0kp6KBQt2JUOQycqVG1NZXvzeaXe5LGFqAKueIS0bw4w0P9r7KuBSVdUk5QjXsUdu2CxQ==
+ dependencies:
+ "@sinonjs/commons" "^1"
+ "@sinonjs/samsam" "^5.0.2"
+
+"@sinonjs/samsam@^5.0.2", "@sinonjs/samsam@^5.1.0":
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-5.1.0.tgz#3afe719232b541bb6cf3411a4c399a188de21ec0"
+ integrity sha512-42nyaQOVunX5Pm6GRJobmzbS7iLI+fhERITnETXzzwDZh+TtDr/Au3yAvXVjFmZ4wEUaE4Y3NFZfKv0bV0cbtg==
+ dependencies:
+ "@sinonjs/commons" "^1.6.0"
+ lodash.get "^4.4.2"
+ type-detect "^4.0.8"
+
+"@sinonjs/text-encoding@^0.7.1":
+ version "0.7.1"
+ resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5"
+ integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==
+
"@storybook/addon-actions@^6.0.21":
version "6.0.21"
resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-6.0.21.tgz#0de1d109d4b1eb99f644bbe84e74c25cfd2b1b6b"
@@ -8286,7 +8322,7 @@ dezalgo@^1.0.0:
asap "^2.0.0"
wrappy "1"
-diff@^4.0.1:
+diff@^4.0.1, diff@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
@@ -13501,6 +13537,11 @@ junk@^3.1.0:
resolved "https://registry.yarnpkg.com/junk/-/junk-3.1.0.tgz#31499098d902b7e98c5d9b9c80f43457a88abfa1"
integrity sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==
+just-extend@^4.0.2:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.1.1.tgz#158f1fdb01f128c411dc8b286a7b4837b3545282"
+ integrity sha512-aWgeGFW67BP3e5181Ep1Fv2v8z//iBJfrvyTnq8wG86vEESwmonn1zPBJ0VfmT9CJq2FIT0VsETtrNFm2a+SHA==
+
keyv@3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.0.0.tgz#44923ba39e68b12a7cec7df6c3268c031f2ef373"
@@ -14833,6 +14874,17 @@ nice-try@^1.0.4:
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
+nise@^4.0.4:
+ version "4.0.4"
+ resolved "https://registry.yarnpkg.com/nise/-/nise-4.0.4.tgz#d73dea3e5731e6561992b8f570be9e363c4512dd"
+ integrity sha512-bTTRUNlemx6deJa+ZyoCUTRvH3liK5+N6VQZ4NIw90AgDXY6iPnsqplNFf6STcj+ePk0H/xqxnP75Lr0J0Fq3A==
+ dependencies:
+ "@sinonjs/commons" "^1.7.0"
+ "@sinonjs/fake-timers" "^6.0.0"
+ "@sinonjs/text-encoding" "^0.7.1"
+ just-extend "^4.0.2"
+ path-to-regexp "^1.7.0"
+
no-case@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.3.tgz#c21b434c1ffe48b39087e86cfb4d2582e9df18f8"
@@ -15703,6 +15755,13 @@ path-to-regexp@0.1.7:
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
+path-to-regexp@^1.7.0:
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a"
+ integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==
+ dependencies:
+ isarray "0.0.1"
+
path-type@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"
@@ -17745,6 +17804,19 @@ single-line-log@^1.1.2:
dependencies:
string-width "^1.0.1"
+sinon@^9.1.0:
+ version "9.1.0"
+ resolved "https://registry.yarnpkg.com/sinon/-/sinon-9.1.0.tgz#4afc90707c8e360fe051398eed2d3b197980ffc3"
+ integrity sha512-9zQShgaeylYH6qtsnNXlTvv0FGTTckuDfHBi+qhgj5PvW2r2WslHZpgc3uy3e/ZAoPkqaOASPi+juU6EdYRYxA==
+ dependencies:
+ "@sinonjs/commons" "^1.7.2"
+ "@sinonjs/fake-timers" "^6.0.1"
+ "@sinonjs/formatio" "^5.0.1"
+ "@sinonjs/samsam" "^5.1.0"
+ diff "^4.0.2"
+ nise "^4.0.4"
+ supports-color "^7.1.0"
+
slash@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
@@ -19076,6 +19148,11 @@ type-check@~0.3.2:
dependencies:
prelude-ls "~1.1.2"
+type-detect@4.0.8, type-detect@^4.0.8:
+ version "4.0.8"
+ resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
+ integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
+
type-fest@^0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1"