Llb/side nav refactor (#2763)

* feat: 🎸 scope picker component (#2646)

* feat: 🎸 scope picker component
---------

Co-authored-by: lisbet-alvarez <lisbet-alvarez@users.noreply.github.com>
Co-authored-by: Zhihe Li <zhihe.li@hashicorp.com>

* refactor: 💡 use hds side nav w/ hds frame instead of rose (#2688)

* refactor: 💡 use hds side nav w/ hds frame instead of rose

* test: 💍 clean up selectors

* fix: 🐛 specificity of style override

* refactor: 💡 use hds side nav list instead of target and portal

* refactor: 💡 revert to using portal component & update scss

* docs: ✏️ update comment

* refactor: 💡 use custom class names for containers

* refactor: 💡 pr feedback (use @hasSidebar arg)

* refactor: 💡 fix onboarding page & fix settings title

* refactor: 💡 remove unneeded template file (pr feedback)

* test: 💍 fix tests due to merge conflicts

* test: 💍 add missing test selector due to merge conflict

* test: 💍 fix more broken tests due to merge conflicts

* chore: 🤖 update yarn.lock & update hds version to 4.18.1

* chore: 🤖 fix e2e test cases due to side nav refactor (#2751)

* chore: 🤖 fix e2e test cases due to side nav refactor

* test: 💍 fix e2e test for desktop

---------

Co-authored-by: lisbet-alvarez <lisbet-alvarez@users.noreply.github.com>
Co-authored-by: Zhihe Li <zhihe.li@hashicorp.com>
This commit is contained in:
authored and GitHub committed 2025-04-14 14:27:02 -07:00
1 parent ab3140be0a
commit 3dded0a7bc
76 files changed
+1621 -711

No files matched your search

+8 -2
View File
@@ -18,9 +18,7 @@ titles:
home: Home
iam: IAM
new: New
resources: Resources
details: Details
general: General
overview: Overview
settings: Settings
empty-set: No Items Yet
@@ -46,6 +44,11 @@ titles:
days: '{days, plural, =1 {# day} other {# days}}'
workers: Workers
no-worker-filter: No worker filter added
scope-level: '{scope} Level'
scope-iam: '{scope} IAM'
project-actions: Project Actions
user-menu: User Menu
back-link: 'Back to {scope}'
descriptions:
empty-set: There are no items to display yet. You may be able to add items or try back later.
cluster-url-initialization: To get started, please enter your cluster URL
@@ -57,6 +60,9 @@ descriptions:
provider: This is the provider for this host catalog. The format of the host set filters are specific to each provider.
no-search-results: Sorry, we weren't able to find any {resource} for "{query}".
no-filter-results: Sorry, we weren’t able to find any {resource} within the filtered parameters.
and-more: ...and more
view-all-orgs: 'View all orgs ({total})'
view-all-projects: 'View all projects ({total})'
questions:
delete-confirm: Are you sure you want to delete this resource?
remove-confirm: Are you sure you want to remove this association?
@@ -53,3 +53,20 @@ form {
color: inherit;
}
}
// TODO: This is a temporary fix that ensures changing the color theme won't alter the dropdown colors.
.app-frame-sidebar-style-overrides .side-nav-style-overrides {
.hds-dropdown-toggle-button,
.hds-dropdown-toggle-icon {
background: var(--token-side-nav-color-surface-primary);
&:hover {
background: var(--token-side-nav-color-surface-interactive-hover);
}
&:active,
&:focus {
background: var(--token-side-nav-color-surface-primary);
}
}
}
@@ -7,7 +7,7 @@
@use '../utilities/type';
@use '../variables/media';
$l: sizing.rems(l); // 16
$l: sizing.rems(l); // 24
$m: sizing.rems(m); // 16
$s: sizing.rems(s); // 12
$xs: sizing.rems(xs); // 8
@@ -36,11 +36,22 @@ $xxxxs: sizing.rems(xxxxs); // 1
border-bottom: $xxxxs solid var(--header-border-color);
border-top-left-radius: $xxxs;
border-top-right-radius: $xxxs;
display: flex;
align-items: center;
column-gap: $xs;
h2 {
margin: $m 0;
}
}
.rose-frame-body {
$body-padding: $l;
padding: $body-padding;
p {
margin-bottom: $m;
}
}
}
+1 -1
View File
@@ -30,7 +30,7 @@
},
"dependencies": {
"@embroider/test-setup": "^2.1.1",
"@hashicorp/design-system-components": "^4.13.0",
"@hashicorp/design-system-components": "^4.18.1",
"codemirror": "5.65.7",
"ember-auto-import": "^2.10.0",
"ember-cli-babel": "^8.2.0",
+5 -2
View File
@@ -15,10 +15,13 @@ export class AliasesPage extends BaseResourcePage {
* @param {string} targetId ID of the target
* @returns Name of the alias
*/
async createAliasForTarget(alias, targetId) {
async createAliasForTarget(alias, targetId, orgName) {
const aliasName = 'Alias ' + nanoid();
await this.page.getByRole('link', { name: 'Orgs', exact: true }).click();
await this.page
.getByRole('link', { name: `Back to ${orgName}`, exact: true })
.click();
await this.page.getByRole('link', { name: 'Orgs' }).click();
await this.page.getByRole('link', { name: 'Aliases' }).click();
await expect(
this.page
+3 -3
View File
@@ -16,7 +16,7 @@ export class AuthMethodsPage extends BaseResourcePage {
async createPasswordAuthMethod() {
const authMethodName = 'Auth Method ' + nanoid();
await this.page
.getByRole('navigation', { name: 'IAM' })
.getByRole('navigation', { name: 'Application local navigation' })
.getByRole('link', { name: 'Auth Methods' })
.click();
await this.page.getByRole('button', { name: 'New' }).click();
@@ -43,7 +43,7 @@ export class AuthMethodsPage extends BaseResourcePage {
async createOidcAuthMethod(issuer, clientId, clientSecret, boundaryAddr) {
const authMethodName = 'Auth Method ' + nanoid();
await this.page
.getByRole('navigation', { name: 'IAM' })
.getByRole('navigation', { name: 'Application local navigation' })
.getByRole('link', { name: 'Auth Methods' })
.click();
await this.page.getByRole('button', { name: 'New' }).click();
@@ -130,7 +130,7 @@ export class AuthMethodsPage extends BaseResourcePage {
*/
async makeAuthMethodPrimary() {
await this.page
.getByRole('navigation', { name: 'IAM' })
.getByRole('navigation', { name: 'Application local navigation' })
.getByRole('link', { name: 'Auth Methods' })
.click();
await this.page.getByLabel('Manage').click();
+3 -3
View File
@@ -17,7 +17,7 @@ export class CredentialStoresPage extends BaseResourcePage {
async createStaticCredentialStore() {
const credentialStoreName = 'Credential Store ' + nanoid();
await this.page
.getByRole('navigation', { name: 'Resources' })
.getByRole('navigation', { name: 'Application local navigation' })
.getByRole('link', { name: 'Credential Stores' })
.click();
await this.page.getByRole('link', { name: 'New', exact: true }).click();
@@ -47,7 +47,7 @@ export class CredentialStoresPage extends BaseResourcePage {
async createVaultCredentialStore(vaultAddr, clientToken) {
const credentialStoreName = 'Credential Store ' + nanoid();
await this.page
.getByRole('navigation', { name: 'Resources' })
.getByRole('navigation', { name: 'Application local navigation' })
.getByRole('link', { name: 'Credential Stores' })
.click();
await this.page.getByRole('link', { name: 'New', exact: true }).click();
@@ -84,7 +84,7 @@ export class CredentialStoresPage extends BaseResourcePage {
) {
const credentialStoreName = 'Credential Store ' + nanoid();
await this.page
.getByRole('navigation', { name: 'Resources' })
.getByRole('navigation', { name: 'Application local navigation' })
.getByRole('link', { name: 'Credential Stores' })
.click();
await this.page.getByRole('link', { name: 'New', exact: true }).click();
+1 -1
View File
@@ -16,7 +16,7 @@ export class GroupsPage extends BaseResourcePage {
async createGroup() {
const groupName = 'Group ' + nanoid();
await this.page
.getByRole('navigation', { name: 'IAM' })
.getByRole('navigation', { name: 'Application local navigation' })
.getByRole('link', { name: 'Groups' })
.click();
await this.page.getByRole('link', { name: 'New', exact: true }).click();
+1 -1
View File
@@ -16,7 +16,7 @@ export class HostCatalogsPage extends BaseResourcePage {
async createHostCatalog() {
const hostCatalogName = 'Host Catalog ' + nanoid();
await this.page
.getByRole('navigation', { name: 'Resources' })
.getByRole('navigation', { name: 'Application local navigation' })
.getByRole('link', { name: 'Host Catalogs' })
.click();
await expect(
+4 -1
View File
@@ -18,7 +18,10 @@ export class LoginPage extends BaseResourcePage {
await this.page.getByLabel('Password', { exact: true }).fill(password);
await this.page.getByRole('button', { name: 'Sign In' }).click();
await expect(
this.page.getByRole('navigation', { name: 'General' }),
this.page.getByRole('navigation', {
// TODO: Uncomment line below once both UIs have been refactored to use HDS side nav.
// name: 'Application local navigation',
}),
).toBeVisible();
await expect(this.page.getByText(loginName)).toBeEnabled();
}
+1 -1
View File
@@ -16,7 +16,7 @@ export class OrgsPage extends BaseResourcePage {
async createOrg() {
const orgName = 'Org ' + nanoid();
await this.page
.getByRole('navigation', { name: 'General' })
.getByRole('navigation', { name: 'Application local navigation' })
.getByRole('link', { name: 'Orgs' })
.click();
await this.page.getByRole('link', { name: 'New Org' }).click();
+1 -1
View File
@@ -16,7 +16,7 @@ export class ProjectsPage extends BaseResourcePage {
async createProject() {
const projectName = 'Project ' + nanoid();
await this.page
.getByRole('navigation', { name: 'General' })
.getByRole('navigation', { name: 'Application local navigation' })
.getByRole('link', { name: 'Projects' })
.click();
await this.page.getByRole('link', { name: 'New', exact: true }).click();
+1 -1
View File
@@ -16,7 +16,7 @@ export class RolesPage extends BaseResourcePage {
const roleName = 'Role ' + nanoid();
await this.page
.getByRole('navigation', { name: 'IAM' })
.getByRole('navigation', { name: 'Application local navigation' })
.getByRole('link', { name: 'Roles' })
.click();
await this.page.getByRole('link', { name: 'New Role' }).click();
+1 -1
View File
@@ -14,7 +14,7 @@ export class SessionsPage extends BasePage {
*/
async waitForSessionToBeVisible(targetName) {
await this.page
.getByRole('navigation', { name: 'Resources' })
.getByRole('navigation', { name: 'Application local navigation' })
.getByRole('link', { name: 'Sessions' })
.click();
let i = 0;
+9 -9
View File
@@ -17,7 +17,7 @@ export class TargetsPage extends BaseResourcePage {
async createTarget(port) {
const targetName = 'Target ' + nanoid();
await this.page
.getByRole('navigation', { name: 'Resources' })
.getByRole('navigation', { name: 'Application local navigation' })
.getByRole('link', { name: 'Targets' })
.click();
await this.page.getByRole('link', { name: 'New', exact: true }).click();
@@ -44,7 +44,7 @@ export class TargetsPage extends BaseResourcePage {
async createTargetWithAddress(address, port) {
const targetName = 'Target ' + nanoid();
await this.page
.getByRole('navigation', { name: 'Resources' })
.getByRole('navigation', { name: 'Application local navigation' })
.getByRole('link', { name: 'Targets' })
.click();
await this.page.getByRole('link', { name: 'New', exact: true }).click();
@@ -73,7 +73,7 @@ export class TargetsPage extends BaseResourcePage {
async createTargetWithAddressAndAlias(address, port, alias) {
const targetName = 'Target ' + nanoid();
await this.page
.getByRole('navigation', { name: 'Resources' })
.getByRole('navigation', { name: 'Application local navigation' })
.getByRole('link', { name: 'Targets' })
.click();
await this.page.getByRole('link', { name: 'New', exact: true }).click();
@@ -109,7 +109,7 @@ export class TargetsPage extends BaseResourcePage {
async createTcpTargetWithAddressEnt(address, port) {
const targetName = 'Target ' + nanoid();
await this.page
.getByRole('navigation', { name: 'Resources' })
.getByRole('navigation', { name: 'Application local navigation' })
.getByRole('link', { name: 'Targets' })
.click();
await this.page.getByRole('link', { name: 'New', exact: true }).click();
@@ -140,7 +140,7 @@ export class TargetsPage extends BaseResourcePage {
async createSshTargetEnt(port) {
const targetName = 'Target ' + nanoid();
await this.page
.getByRole('navigation', { name: 'Resources' })
.getByRole('navigation', { name: 'Application local navigation' })
.getByRole('link', { name: 'Targets' })
.click();
await this.page.getByRole('link', { name: 'New', exact: true }).click();
@@ -172,7 +172,7 @@ export class TargetsPage extends BaseResourcePage {
async createSshTargetWithAddressEnt(address, port) {
const targetName = 'Target ' + nanoid();
await this.page
.getByRole('navigation', { name: 'Resources' })
.getByRole('navigation', { name: 'Application local navigation' })
.getByRole('link', { name: 'Targets' })
.click();
await this.page.getByRole('link', { name: 'New', exact: true }).click();
@@ -206,7 +206,7 @@ export class TargetsPage extends BaseResourcePage {
async createSshTargetWithAddressAndAlias(address, port, alias) {
const targetName = 'Target ' + nanoid();
await this.page
.getByRole('navigation', { name: 'Resources' })
.getByRole('navigation', { name: 'Application local navigation' })
.getByRole('link', { name: 'Targets' })
.click();
await this.page.getByRole('link', { name: 'New', exact: true }).click();
@@ -349,7 +349,7 @@ export class TargetsPage extends BaseResourcePage {
*/
async addBrokeredCredentialsToTarget(targetName, credentialName) {
await this.page
.getByRole('navigation', { name: 'Resources' })
.getByRole('navigation', { name: 'Application local navigation' })
.getByRole('link', { name: 'Targets' })
.click();
await expect(
@@ -397,7 +397,7 @@ export class TargetsPage extends BaseResourcePage {
*/
async addInjectedCredentialsToTarget(targetName, credentialName) {
await this.page
.getByRole('navigation', { name: 'Resources' })
.getByRole('navigation', { name: 'Application local navigation' })
.getByRole('link', { name: 'Targets' })
.click();
await expect(
+1 -1
View File
@@ -16,7 +16,7 @@ export class UsersPage extends BaseResourcePage {
async createUser() {
const userName = 'User ' + nanoid();
await this.page
.getByRole('navigation', { name: 'IAM' })
.getByRole('navigation', { name: 'Application local navigation' })
.getByRole('link', { name: 'Users' })
.click();
await this.page.getByRole('link', { name: 'New', exact: true }).click();
+3 -3
View File
@@ -92,7 +92,7 @@ test.describe('Aliases (Enterprise)', () => {
// Clear destination from alias
await page
.getByRole('navigation', { name: 'Resources' })
.getByRole('navigation', { name: 'Application local navigation' })
.getByRole('link', { name: 'Targets' })
.click();
await page.getByRole('link', { name: targetName }).click();
@@ -270,10 +270,10 @@ test.describe('Aliases (Enterprise)', () => {
alias = 'example.alias.' + nanoid();
const aliasesPage = new AliasesPage(page);
await aliasesPage.createAliasForTarget(alias, targetId);
await aliasesPage.createAliasForTarget(alias, targetId, orgName);
connect = await boundaryCli.connectSshToAlias(alias);
await page
.getByRole('navigation', { name: 'General' })
.getByRole('navigation', { name: 'Application local navigation' })
.getByRole('link', { name: 'Orgs' })
.click();
await page.getByRole('link', { name: orgName }).click();
+2 -2
View File
@@ -75,7 +75,7 @@ test.describe('Aliases', () => {
// Clear destination from alias
await page
.getByRole('navigation', { name: 'Resources' })
.getByRole('navigation', { name: 'Application local navigation' })
.getByRole('link', { name: 'Targets' })
.click();
await page.getByRole('link', { name: targetName }).click();
@@ -230,7 +230,7 @@ test.describe('Aliases', () => {
alias = 'example.alias.' + nanoid();
const aliasesPage = new AliasesPage(page);
await aliasesPage.createAliasForTarget(alias, targetId);
await aliasesPage.createAliasForTarget(alias, targetId, orgName);
await page.getByRole('link', { name: 'Orgs', exact: true }).click();
await expect(page.getByRole('heading', { name: 'Orgs' })).toBeVisible();
await page.getByRole('link', { name: orgName }).click();
@@ -52,7 +52,7 @@ test(
const orgsPage = new OrgsPage(page);
orgName = await orgsPage.createOrg();
await page
.getByRole('navigation', { name: 'IAM' })
.getByRole('navigation', { name: 'Application local navigation' })
.getByRole('link', { name: 'Auth Methods' })
.click();
await page.getByRole('button', { name: 'New' }).click();
@@ -191,7 +191,7 @@ test(
// View the LDAP account and verify account attributes
await page.getByRole('link', { name: orgName }).click();
await page
.getByRole('navigation', { name: 'IAM' })
.getByRole('navigation', { name: 'Application local navigation' })
.getByRole('link', { name: 'Auth Methods' })
.click();
await page.getByRole('link', { name: ldapAuthMethodName }).click();
@@ -280,7 +280,7 @@ test(
// View the User account and verify attributes
await page
.getByRole('navigation', { name: 'IAM' })
.getByRole('navigation', { name: 'Application local navigation' })
.getByRole('link', { name: 'Users' })
.click();
await page.getByRole('link', { name: userName }).click();
@@ -32,7 +32,6 @@ test(
async ({
page,
context,
baseURL,
controllerAddr,
adminAuthMethodId,
adminLoginName,
@@ -52,7 +51,7 @@ test(
userName,
password,
email,
baseURL,
controllerAddr,
);
policyName = authPolicyName;
@@ -71,7 +70,7 @@ test(
issuer,
clientId,
clientSecret,
baseURL,
controllerAddr,
);
// Change OIDC Auth Method state to active-public
@@ -134,9 +133,7 @@ test(
).toBeVisible();
// Log back in as an admin
// WORKAROUND: Currently, users logging using OIDC don't have a username
// displayed in the UI, so there's no simple locator to access this menu.
await page.locator('details').filter({ hasText: 'Sign Out' }).click();
await page.getByRole('button', { name: 'User Menu' }).click();
await page.getByRole('button', { name: 'Sign Out' }).click();
await expect(page.getByRole('button', { name: 'Sign In' })).toBeVisible();
await loginPage.login(adminLoginName, adminPassword);
@@ -152,7 +149,7 @@ test(
.getByText(orgName),
).toBeVisible();
await page
.getByRole('navigation', { name: 'IAM' })
.getByRole('navigation', { name: 'Application local navigation' })
.getByRole('link', { name: 'Auth Methods' })
.click();
await page.getByRole('link', { name: oidcAuthMethodName }).click();
@@ -241,7 +238,7 @@ test(
// View the User account and verify attributes
await page
.getByRole('navigation', { name: 'IAM' })
.getByRole('navigation', { name: 'Application local navigation' })
.getByRole('link', { name: 'Users' })
.click();
await page
@@ -75,7 +75,7 @@ test(
// Set a new password on the account
await page
.getByRole('navigation', { name: 'General' })
.getByRole('navigation', { name: 'Application local navigation' })
.getByRole('link', { name: 'Orgs' })
.click();
await page.getByRole('link', { name: orgName }).click();
@@ -85,7 +85,7 @@ test(
.getByText(orgName),
).toBeVisible();
await page
.getByRole('navigation', { name: 'IAM' })
.getByRole('navigation', { name: 'Application local navigation' })
.getByRole('link', { name: 'Auth Methods' })
.click();
await page.getByRole('link', { name: authMethodName }).click();
@@ -49,7 +49,7 @@ test.describe('AWS', () => {
// Create host catalog
const hostCatalogName = 'Host Catalog ' + nanoid();
await page
.getByRole('navigation', { name: 'Resources' })
.getByRole('navigation', { name: 'Application local navigation' })
.getByRole('link', { name: 'Host Catalogs' })
.click();
await page.getByRole('link', { name: 'New', exact: true }).click();
@@ -164,7 +164,7 @@ test.describe('AWS', () => {
await hostCatalogsPage.createHostCatalog();
const newHostSetName = await hostCatalogsPage.createHostSet();
await page
.getByRole('navigation', { name: 'Resources' })
.getByRole('navigation', { name: 'Application local navigation' })
.getByRole('link', { name: 'Targets' })
.click();
await page.getByRole('link', { name: targetName }).click();
@@ -49,7 +49,7 @@ test.describe('AWS', () => {
// Create host catalog
const hostCatalogName = 'Host Catalog ' + nanoid();
await page
.getByRole('navigation', { name: 'Resources' })
.getByRole('navigation', { name: 'Application local navigation' })
.getByRole('link', { name: 'Host Catalogs' })
.click();
await page.getByRole('link', { name: 'New', exact: true }).click();
@@ -164,7 +164,7 @@ test.describe('AWS', () => {
await hostCatalogsPage.createHostCatalog();
const newHostSetName = await hostCatalogsPage.createHostSet();
await page
.getByRole('navigation', { name: 'Resources' })
.getByRole('navigation', { name: 'Application local navigation' })
.getByRole('link', { name: 'Targets' })
.click();
await page.getByRole('link', { name: targetName }).click();
@@ -42,7 +42,7 @@ test(
// Navigate to orgs page when done
await page
.getByRole('navigation', { name: 'General' })
.getByRole('navigation', { name: 'Application local navigation' })
.getByRole('link', { name: 'Orgs' })
.click();
await expect(page.getByRole('heading', { name: 'Orgs' })).toBeVisible();
+1 -1
View File
@@ -34,7 +34,7 @@ test(
await page.getByRole('link', { name: org.name }).click();
await page.getByRole('link', { name: project.name }).click();
await page
.getByRole('navigation', { name: 'Resources' })
.getByRole('navigation', { name: 'Application local navigation' })
.getByRole('link', { name: 'Targets' })
.click();
await expect(
@@ -62,6 +62,9 @@ test(
const projectName = await projectsPage.createProject();
// Create storage bucket
await page
.getByRole('link', { name: `Back to ${orgName}`, exact: true })
.click();
await page.getByRole('link', { name: 'Orgs', exact: true }).click();
const storageBucketsPage = new StorageBucketsPage(page);
const storageBucketName = await storageBucketsPage.createStorageBucketAws(
@@ -107,6 +110,9 @@ test(
await targetsPage.enableSessionRecording(storageBucketName);
// Create storage policy in org scope: keep session recordings forever
await page
.getByRole('link', { name: `Back to ${orgName}`, exact: true })
.click();
await page.getByRole('link', { name: 'Orgs', exact: true }).click();
await expect(page.getByRole('heading', { name: 'Orgs' })).toBeVisible();
await page.getByRole('link', { name: orgName }).click();
@@ -146,9 +152,12 @@ test(
await boundaryCli.waitForSessionRecording(storageBucket.id);
// Play back session recording
await page
.getByRole('link', { name: `Back to ${orgName}`, exact: true })
.click();
await page.getByRole('link', { name: 'Orgs', exact: true }).click();
await page
.getByRole('navigation', { name: 'General' })
.getByRole('navigation', { name: 'Application local navigation' })
.getByRole('link', { name: 'Session Recordings', exact: true })
.click();
await page
@@ -115,6 +115,9 @@ test(
await targetsPage.enableSessionRecording(storageBucketName);
// Create storage policy in org scope: keep session recordings forever
await page
.getByRole('link', { name: `Back to ${orgName}`, exact: true })
.click();
await page.getByRole('link', { name: 'Orgs', exact: true }).click();
await expect(page.getByRole('heading', { name: 'Orgs' })).toBeVisible();
await page.getByRole('link', { name: orgName }).click();
@@ -154,9 +157,12 @@ test(
await boundaryCli.waitForSessionRecording(storageBucket.id);
// Play back session recording
await page
.getByRole('link', { name: `Back to ${orgName}`, exact: true })
.click();
await page.getByRole('link', { name: 'Orgs', exact: true }).click();
await page
.getByRole('navigation', { name: 'General' })
.getByRole('navigation', { name: 'Application local navigation' })
.getByRole('link', { name: 'Session Recordings', exact: true })
.click();
await page
+1 -1
View File
@@ -190,7 +190,7 @@ test(
// Create another host set
await page
.getByRole('navigation', { name: 'Resources' })
.getByRole('navigation', { name: 'Application local navigation' })
.getByRole('link', { name: 'Host Catalogs' })
.click();
await page.getByRole('link', { name: hostCatalogName }).click();
+1 -1
View File
@@ -49,7 +49,7 @@ test(
// Create another host set
await page
.getByRole('navigation', { name: 'Resources' })
.getByRole('navigation', { name: 'Application local navigation' })
.getByRole('link', { name: 'Host Catalogs' })
.click();
await page.getByRole('link', { name: hostCatalogName }).click();
+1 -1
View File
@@ -14,7 +14,7 @@ test(
await page.goto('/');
await page
.getByRole('navigation', { name: 'General' })
.getByRole('navigation', { name: 'Application local navigation' })
.getByRole('link', { name: 'Workers' })
.click();
await page.getByRole('link', { name: 'New', exact: true }).click();
+2 -2
View File
@@ -17,7 +17,7 @@ test(
await page.goto('/');
await page
.getByRole('navigation', { name: 'General' })
.getByRole('navigation', { name: 'Application local navigation' })
.getByRole('link', { name: 'Workers' })
.click();
await expect(
@@ -102,7 +102,7 @@ test(
// Check tag in worker dialog
await page
.getByRole('navigation', { name: 'General' })
.getByRole('navigation', { name: 'Application local navigation' })
.getByRole('link', { name: 'Workers' })
.click();
await expect(
+1 -1
View File
@@ -14,7 +14,7 @@ test(
await page.goto('/');
await page
.getByRole('navigation', { name: 'General' })
.getByRole('navigation', { name: 'Application local navigation' })
.getByRole('link', { name: 'Workers' })
.click();
await page.getByRole('link', { name: 'New', exact: true }).click();
@@ -8,7 +8,7 @@
{{#if (can 'addPrincipals role' @model)}}
<dd.Interactive
@route='scopes.scope.roles.role.add-principals'
data-test-manage-dropdown-principals
data-test-manage-role-principals
>
{{t 'resources.role.principal.actions.add-principals'}}
</dd.Interactive>
@@ -0,0 +1,99 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
}}
<Hds::Dropdown
@enableCollisionDetection={{true}}
@matchToggleWidth={{true}}
{{! HDS provided class to automatically fade in/out }}
class='hds-side-nav-hide-when-minimized scope-picker'
as |D|
>
{{#let this.currentScope as |scope|}}
<D.ToggleButton
@text={{scope.name}}
@icon={{scope.icon}}
class='scope-picker__toggle-button'
/>
{{/let}}
<D.Checkmark
@icon='globe'
@route='scopes.scope'
@model='global'
@selected={{this.scope.org.isGlobal}}
>
{{t 'titles.global'}}
</D.Checkmark>
{{#if this.scope.orgsList}}
<D.Separator />
<D.Title @text={{t 'resources.org.title_plural'}} />
{{/if}}
{{#each this.truncatedOrgsList as |org|}}
<D.Checkmark
@icon='org'
@route='scopes.scope'
@model={{org.id}}
@selected={{and (eq this.scope.org.id org.id) (not this.scope.project)}}
data-test-scope-picker-org-item
>
{{org.displayName}}
</D.Checkmark>
{{/each}}
{{#if (gt this.scope.orgsList.length 5)}}
<D.Description
@text={{t 'descriptions.and-more'}}
data-test-scope-picker-org-and-more-text
/>
<D.Interactive
@route='scopes.scope.scopes'
@model='global'
data-test-scope-picker-org-count
>{{t
'descriptions.view-all-orgs'
total=this.scope.orgsList.length
}}</D.Interactive>
{{/if}}
{{! All project scopes in org are only loaded when current scope is of project type. }}
{{#if this.scope.project}}
<D.Title @text={{t 'resources.project.title_plural'}} class='indentation' />
{{#each this.truncatedProjectsList as |project|}}
<D.Checkmark
@icon='grid'
@route='scopes.scope'
@model={{project.id}}
@selected={{eq this.scope.project.id project.id}}
class='indentation'
data-test-scope-picker-project-item
>
{{project.displayName}}
</D.Checkmark>
{{/each}}
{{#if (gt this.scope.projectsList.length 5)}}
<D.Description
@text={{t 'descriptions.and-more'}}
class='indentation'
data-test-scope-picker-project-and-more-text
/>
<D.Interactive
@route='scopes.scope.scopes'
@model={{this.scope.org.id}}
class='indentation'
data-test-scope-picker-project-count
>{{t
'descriptions.view-all-projects'
total=this.scope.projectsList.length
}}</D.Interactive>
{{/if}}
{{/if}}
</Hds::Dropdown>
@@ -0,0 +1,48 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import orderBy from 'lodash/orderBy';
export default class ScopePickerIndexComponent extends Component {
// =services
@service intl;
@service scope;
@service session;
// =attributes
/**
* Returns display name and icon for current scope.
* @type {object}
*/
get currentScope() {
if (this.scope.project) {
return { name: this.scope.project.displayName, icon: 'grid' };
} else if (this.scope.org.isOrg) {
return { name: this.scope.org.displayName, icon: 'org' };
} else {
return { name: this.intl.t('titles.global'), icon: 'globe' };
}
}
/**
* Returns first five orgs ordered by most recently updated.
* @type {[ScopeModel]}
*/
get truncatedOrgsList() {
return orderBy(this.scope.orgsList, 'updated_time', 'desc').slice(0, 5);
}
/**
* Returns first five projects ordered by most recently updated.
* @type {[ScopeModel]}
*/
get truncatedProjectsList() {
return orderBy(this.scope.projectsList, 'updated_time', 'desc').slice(0, 5);
}
}
+8
View File
@@ -30,6 +30,7 @@ export default class ApplicationController extends Controller {
@service features;
@service featureEdition;
@service flashMessages;
@service router;
/**
* Returns available themes
@@ -39,6 +40,13 @@ export default class ApplicationController extends Controller {
return THEMES;
}
get showSideNav() {
return (
this.router.currentRouteName.startsWith('scopes.scope') &&
this.session.isAuthenticated
);
}
// =actions
/**
-13
View File
@@ -1,13 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Controller from '@ember/controller';
import { service } from '@ember/service';
export default class ScopesIndexController extends Controller {
// =services
@service session;
}
+1
View File
@@ -10,4 +10,5 @@ export default class ScopesScopeController extends Controller {
// =services
@service session;
@service scope;
}
+7 -2
View File
@@ -15,9 +15,14 @@ export default class AccountRoute extends Route {
// =methods
/**
* If arriving here unauthenticated, redirect to index for further processing.
* If arriving here unauthenticated, redirect to index for further processing
* else redirect to change-password route since account route is empty.
*/
beforeModel() {
if (!this.session.isAuthenticated) this.router.transitionTo('index');
if (!this.session.isAuthenticated) {
this.router.transitionTo('index');
} else {
this.router.transitionTo('account.change-password');
}
}
}
-24
View File
@@ -72,29 +72,5 @@ export default class ScopesScopeRoute extends Route {
this.scope.project = selectedProject;
this.scope.orgsList = orgs;
this.scope.projectsList = projects;
this.scopes = { orgs, projects, selectedOrg, selectedProject };
// Update the controller (if exists), since setupController is only
// called once the first time the route is activated. It is not called
// again on route refreshes.
/* eslint-disable-next-line ember/no-controller-access-in-routes */
if (this.controller) this.setControllerProperties(this.scopes);
}
/**
* Adds the scopes hash to the controller context (see `afterModel`).
* @param {Controller} controller
*/
setupController(/* controller */) {
super.setupController(...arguments);
this.setControllerProperties(this.scopes);
}
/**
* Updates the controller's `scopes`.
* @param {array} scopes
*/
setControllerProperties(scopes) {
/* eslint-disable-next-line ember/no-controller-access-in-routes */
this.controller.setProperties({ scopes });
}
}
+12 -3
View File
@@ -173,8 +173,6 @@
// Onboarding styles
.create-resources {
display: flex;
align-items: flex-start;
width: sizing.rems(xl) * 15;
.create-resources-form {
@@ -185,7 +183,6 @@
.hds-form-group {
margin-top: sizing.rems(l);
justify-content: center;
.image-container {
display: flex;
@@ -1083,3 +1080,15 @@
margin-bottom: 0;
}
}
// scope picker
.scope-picker {
&__toggle-button {
width: 100%;
}
li:has(.indentation),
li.indentation {
margin-left: 1.5rem;
}
}
@@ -5,32 +5,26 @@
{{page-title (t 'actions.change-password')}}
<Rose::Layout::Sidebar as |sidebarLayout|>
<sidebarLayout.body>
<main>
<Rose::Layout::Centered>
<Rose::Layout::Page as |page|>
<Rose::Layout::Centered>
<Rose::Layout::Page as |page|>
<page.header>
<h2>{{t 'actions.change-password'}}</h2>
</page.header>
<page.body>
<Rose::Frame>
<:header>
<Hds::Icon @name='boundary-color' @size='24' @isInline={{true}} />
<Hds::Text::Display @tag='h2' @size='300' @weight='bold'>{{t
'actions.change-password'
}}</Hds::Text::Display>
</:header>
<:body>
<Form::Account::Password::ChangePassword
@model={{@model}}
@submit={{fn this.changePassword @model}}
@cancel={{fn this.cancel @model}}
/>
</:body>
</Rose::Frame>
</page.body>
<page.body>
<Form::Account::Password::ChangePassword
@model={{@model}}
@submit={{fn this.changePassword @model}}
@cancel={{fn this.cancel @model}}
/>
</page.body>
</Rose::Layout::Page>
</Rose::Layout::Centered>
</main>
</sidebarLayout.body>
<sidebarLayout.footer>
<Hds::AppFooter @theme={{this.session.data.theme}} as |AF|>
<AF.Link @href={{(doc-url)}}>{{t 'titles.documentation'}}</AF.Link>
<AF.LegalLinks />
</Hds::AppFooter>
</sidebarLayout.footer>
</Rose::Layout::Sidebar>
</Rose::Layout::Page>
</Rose::Layout::Centered>
+98 -95
View File
@@ -5,113 +5,116 @@
{{page-title (app-name)}}
<Rose::Layout::Global as |layout|>
<Hds::AppFrame @hasHeader={{false}} @hasSidebar={{this.showSideNav}} as |Frame|>
<Frame.Sidebar class='app-frame-sidebar-style-overrides'>
<Hds::SideNav class='side-nav-style-overrides'>
<:header>
<Hds::SideNav::Header>
<:logo>
<Hds::SideNav::Header::HomeLink
@icon='boundary'
@color='var(--token-color-boundary-brand)'
@ariaLabel={{(app-name)}}
@route='index'
/>
</:logo>
<:actions>
{{#if (feature-flag 'dev-edition-toggle')}}
<Hds::Dropdown @enableCollisionDetection={{true}} as |dd|>
<dd.ToggleIcon
@icon='layers'
@text={{t
'titles.feature-edition'
edition=this.featureEdition.edition
}}
data-test-dev-edition-toggle
/>
<layout.header>
<Rose::Header class={{if (is-loading) ' loading'}} as |header|>
<header.brand @logo='logo-app' @text={{(app-name)}} />
{{#if this.session.isAuthenticated}}
<header.nav>
<HeaderNav />
</header.nav>
<header.utilities as |utilities|>
<utilities.dropdown
@text={{this.session.data.authenticated.username}}
@icon='flight-icons/svg/user-circle-16'
as |dropdown|
>
<dropdown.link @route='account.change-password'>
{{t 'actions.change-password'}}
</dropdown.link>
<dropdown.separator />
<dropdown.button {{on 'click' this.invalidateSession}}>
{{t 'actions.deauthenticate'}}
</dropdown.button>
<dropdown.separator />
<dropdown.section @title={{t 'actions.toggle-color-theme'}}>
<Hds::Form::Radio::Group
@name='theme'
class='dropdown-radio-btn'
as |G|
>
{{#each this.themes as |theme|}}
<G.RadioField
@value={{theme.value}}
checked={{eq this.session.data.theme theme.value}}
{{on 'change' (fn this.toggleTheme theme.value)}}
as |F|
<dd.Title @text={{t 'actions.toggle-feature-edition'}} />
{{#each this.featureEdition.editions as |edition|}}
<dd.Radio
@value={{edition}}
checked={{eq this.featureEdition.edition edition}}
{{on 'change' (fn this.toggleEdition edition)}}
>
<F.Label>{{t (concat 'themes.' theme.label)}}</F.Label>
</G.RadioField>
{{edition}}
</dd.Radio>
{{/each}}
</Hds::Form::Radio::Group>
</dropdown.section>
<dd.Separator />
</utilities.dropdown>
<dd.Title @text={{t 'actions.toggle-licensed-features'}} />
{{#each this.featureEdition.licensedFeatures as |feature|}}
<dd.Checkbox
@value={{feature}}
checked={{feature-flag feature}}
{{on 'change' (fn this.toggleFeature feature)}}
>
{{feature}}
</dd.Checkbox>
{{/each}}
</Hds::Dropdown>
{{/if}}
{{#if (feature-flag 'dev-edition-toggle')}}
<utilities.dropdown
data-test-dev-edition-toggle
@text={{t
'titles.feature-edition'
edition=this.featureEdition.edition
}}
@icon='flight-icons/svg/layers-16'
as |dropdown|
<Hds::Dropdown
@enableCollisionDetection={{true}}
data-test-side-nav-user-dropdown
as |dd|
>
<dropdown.section @title={{t 'actions.toggle-feature-edition'}}>
<Hds::Form::Radio::Group
@name='edition'
class='dropdown-radio-btn'
as |G|
{{#if this.session.data.authenticated.username}}
<dd.ToggleButton
@icon='user'
@text={{this.session.data.authenticated.username}}
/>
{{else}}
<dd.ToggleIcon @icon='user' @text={{t 'titles.user-menu'}} />
{{/if}}
<dd.Interactive @route='account.change-password'>{{t
'actions.change-password'
}}</dd.Interactive>
<dd.Separator />
<dd.Interactive {{on 'click' this.invalidateSession}}>{{t
'actions.deauthenticate'
}}</dd.Interactive>
<dd.Separator />
<dd.Title @text={{t 'actions.toggle-color-theme'}} />
{{#each this.themes as |theme|}}
<dd.Radio
@value={{theme.value}}
checked={{eq this.session.data.theme theme.value}}
{{on 'change' (fn this.toggleTheme theme.value)}}
>
{{#each this.featureEdition.editions as |edition|}}
<G.RadioField
@value={{edition}}
checked={{eq this.featureEdition.edition edition}}
{{on 'change' (fn this.toggleEdition edition)}}
as |F|
>
<F.Label>{{edition}}</F.Label>
</G.RadioField>
{{/each}}
</Hds::Form::Radio::Group>
{{t (concat 'themes.' theme.label)}}
</dd.Radio>
{{/each}}
</Hds::Dropdown>
</:actions>
</Hds::SideNav::Header>
</:header>
</dropdown.section>
<dropdown.separator />
<dropdown.section @title={{t 'actions.toggle-licensed-features'}}>
<Rose::Form as |form|>
{{#each this.featureEdition.licensedFeatures as |feature|}}
<dropdown.item>
<form.checkbox
@label={{feature}}
@checked={{feature-flag feature}}
@onChange={{fn this.toggleFeature feature}}
/>
</dropdown.item>
{{/each}}
</Rose::Form>
</dropdown.section>
</utilities.dropdown>
{{/if}}
<:body>
<Hds::SideNav::Portal::Target />
</:body>
</header.utilities>
<:footer>
<ScopePicker />
</:footer>
</Hds::SideNav>
</Frame.Sidebar>
{{/if}}
</Rose::Header>
</layout.header>
<layout.body>
<Frame.Main>
{{outlet}}
</layout.body>
</Frame.Main>
</Rose::Layout::Global>
<Frame.Footer>
<Hds::AppFooter @theme={{this.session.data.theme}} as |AF|>
<AF.Link @href={{(doc-url)}}>{{t 'titles.documentation'}}</AF.Link>
<AF.LegalLinks />
</Hds::AppFooter>
</Frame.Footer>
</Hds::AppFrame>
{{#if this.flashMessages.queue}}
<div class='ember-notify-cn'>
+6 -3
View File
@@ -3,10 +3,13 @@
SPDX-License-Identifier: BUSL-1.1
}}
<Rose::Layout::Centered class='create-resources'>
<Rose::Frame>
<Rose::Layout::Centered>
<Rose::Frame class='create-resources'>
<:header>
<h2>{{t 'onboarding.add-target.titles.have-a-target'}}</h2>
<Hds::Icon @name='boundary-color' @size='24' @isInline={{true}} />
<Hds::Text::Display @tag='h2' @size='300' @weight='bold'>{{t
'onboarding.add-target.titles.have-a-target'
}}</Hds::Text::Display>
</:header>
<:body>
<p>{{t 'onboarding.add-target.descriptions.setup-first-target'}}</p>
@@ -4,10 +4,14 @@
}}
{{page-title (t 'titles.success')}}
<Rose::Layout::Centered class='create-resources'>
<Rose::Frame>
<Rose::Layout::Centered>
<Rose::Frame class='create-resources'>
<:header>
<h2>{{t 'onboarding.success.titles.connect'}}</h2>
<Hds::Icon @name='boundary-color' @size='24' @isInline={{true}} />
<Hds::Text::Display @tag='h2' @size='300' @weight='bold'>{{t
'onboarding.success.titles.connect'
}}</Hds::Text::Display>
</:header>
<:body>
{{! We have created }}
@@ -27,7 +31,6 @@
{{! Download Desktop client }}
<div class='create-resources-item'>
<span class='create-resources-item-header'>
<Hds::Icon @name='boundary-color' @size='24' @isInline={{true}} />
<h3>{{t 'onboarding.success.titles.desktop'}}</h3>
</span>
<p>{{t 'onboarding.success.descriptions.download'}}</p>
-30
View File
@@ -1,30 +0,0 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
}}
{{outlet}}
{{#unless @model}}
<Rose::Layout::Sidebar as |sidebarLayout|>
<sidebarLayout.body>
<main>
<Rose::Layout::Centered>
<Hds::ApplicationState as |A|>
<A.Header
@title={{t 'auth.titles.no-scopes'}}
@icon='alert-circle'
/>
<A.Body @text={{t 'auth.descriptions.no-scopes'}} />
</Hds::ApplicationState>
</Rose::Layout::Centered>
</main>
</sidebarLayout.body>
<sidebarLayout.footer>
<Hds::AppFooter @theme={{this.session.data.theme}} as |AF|>
<AF.Link @href={{(doc-url)}}>{{t 'titles.documentation'}}</AF.Link>
<AF.LegalLinks />
</Hds::AppFooter>
</sidebarLayout.footer>
</Rose::Layout::Sidebar>
{{/unless}}
+208 -209
View File
@@ -16,14 +16,14 @@
{{#if @model.isProject}}
{{page-title (t 'resources.org.title_plural')}}
{{page-title this.scopes.selectedOrg.displayName}}
{{page-title this.scope.org.displayName}}
{{page-title (t 'resources.project.title_plural')}}
{{page-title @model.displayName}}
<Breadcrumbs::Item
@text={{this.scopes.selectedOrg.displayName}}
@text={{this.scope.org.displayName}}
@icon='org'
@route='scopes.scope.edit'
@model={{this.scopes.selectedOrg.id}}
@model={{this.scope.org.id}}
/>
<Breadcrumbs::Item
@text={{@model.displayName}}
@@ -33,218 +33,217 @@
/>
{{/if}}
<Rose::Layout::Sidebar as |sidebarLayout|>
<Hds::SideNav::Portal as |Nav|>
{{#if @model.isGlobal}}
<Nav.Title>{{t 'titles.scope-level' scope=(t 'titles.global')}}</Nav.Title>
{{#if this.session.isAuthenticated}}
<sidebarLayout.sidebar>
<Nav.Link
@icon='org'
@text={{t 'resources.org.title_plural'}}
@route='scopes.scope.scopes'
/>
{{#if @model.isGlobal}}
<Rose::Nav::Sidebar @title={{t 'titles.general'}} as |nav|>
<nav.link @route='scopes.scope.scopes'>
<Rose::Icon @name='flight-icons/svg/org-16' @size='medium' />
{{t 'resources.org.title_plural'}}
</nav.link>
{{#if (can 'navigate scope' @model collection='session-recordings')}}
<Nav.Link
@icon='film'
@text={{t 'resources.session-recording.title_plural'}}
@route='scopes.scope.session-recordings'
/>
{{/if}}
{{#if (can 'navigate scope' @model collection='session-recordings')}}
<nav.link @route='scopes.scope.session-recordings'>
<Rose::Icon @name='flight-icons/svg/film-16' @size='medium' />
{{t 'resources.session-recording.title_plural'}}
</nav.link>
{{/if}}
{{#if (can 'navigate scope' @model collection='storage-buckets')}}
<nav.link @route='scopes.scope.storage-buckets'>
<Rose::Icon
@name='flight-icons/svg/cloud-upload-16'
@size='medium'
/>
{{t 'resources.storage-bucket.title_plural'}}
</nav.link>
{{/if}}
{{#if (can 'navigate scope' @model collection='policies')}}
<nav.link @route='scopes.scope.policies'>
<Rose::Icon
@name='flight-icons/svg/hourglass-16'
@size='medium'
/>
{{t 'resources.policy.title_plural'}}
</nav.link>
{{/if}}
{{#if (can 'navigate scope' @model collection='aliases')}}
<nav.link @route='scopes.scope.aliases'>
<Rose::Icon @name='flight-icons/svg/at-sign-16' @size='medium' />
{{t 'resources.alias.title_plural'}}
</nav.link>
{{/if}}
{{#if
(and
(can 'navigate worker' @model collection='workers')
(feature-flag 'byow')
)
}}
<nav.link @route='scopes.scope.workers'>
<Rose::Icon @name='flight-icons/svg/cpu-16' @size='medium' />
{{t 'titles.workers'}}
</nav.link>
{{/if}}
</Rose::Nav::Sidebar>
{{/if}}
{{#if (can 'navigate scope' @model collection='storage-buckets')}}
<Nav.Link
@icon='cloud-upload'
@text={{t 'resources.storage-bucket.title_plural'}}
@route='scopes.scope.storage-buckets'
/>
{{/if}}
{{#if @model.isOrg}}
<Rose::Nav::Sidebar @title={{t 'titles.general'}} as |nav|>
{{#if this.session.data.authenticated.isGlobal}}
<nav.link @route='scopes.scope.scopes' @model='global'>
<Rose::Icon @name='flight-icons/svg/org-16' @size='medium' />
{{t 'resources.org.title_plural'}}
</nav.link>
{{/if}}
<nav.link @route='scopes.scope.scopes'>
<Rose::Icon @name='flight-icons/svg/grid-16' @size='medium' />
{{t 'resources.project.title_plural'}}
</nav.link>
{{#if (can 'navigate scope' @model collection='policies')}}
<Nav.Link
@icon='hourglass'
@text={{t 'resources.policy.title_plural'}}
@route='scopes.scope.policies'
/>
{{/if}}
{{#if (can 'navigate scope' @model collection='policies')}}
<nav.link @route='scopes.scope.policies'>
<Rose::Icon
@name='flight-icons/svg/hourglass-16'
@size='medium'
/>
{{t 'resources.policy.title_plural'}}
</nav.link>
{{/if}}
</Rose::Nav::Sidebar>
{{#if (can 'navigate scope' @model collection='aliases')}}
<Nav.Link
@icon='at-sign'
@text={{t 'resources.alias.title_plural'}}
@route='scopes.scope.aliases'
/>
{{/if}}
{{/if}}
{{#if
(and
(can 'navigate worker' @model collection='workers')
(feature-flag 'byow')
)
}}
<Nav.Link
@icon='cpu'
@text={{t 'titles.workers'}}
@route='scopes.scope.workers'
/>
{{/if}}
{{#if @model.isProject}}
<Rose::Nav::Sidebar @title={{t 'titles.general'}} as |nav|>
{{#if this.session.data.authenticated.isGlobal}}
<nav.link @route='scopes.scope.scopes' @model='global'>
<Rose::Icon @name='flight-icons/svg/org-16' @size='medium' />
{{t 'resources.org.title_plural'}}
</nav.link>
{{/if}}
<nav.link @route='scopes.scope.scopes' @model={{@model.scopeID}}>
<Rose::Icon @name='flight-icons/svg/grid-16' @size='medium' />
{{t 'resources.project.title_plural'}}
</nav.link>
</Rose::Nav::Sidebar>
<Rose::Nav::Sidebar @title={{t 'titles.resources'}} as |nav|>
{{#if (can 'navigate model' @model collection='sessions')}}
<nav.link @route='scopes.scope.sessions'>
<Rose::Icon
@name='flight-icons/svg/entry-point-16'
@size='medium'
/>
{{t 'resources.session.title_plural'}}
</nav.link>
{{/if}}
{{#if (can 'navigate model' @model collection='targets')}}
<nav.link @route='scopes.scope.targets'>
<Rose::Icon @name='flight-icons/svg/target-16' @size='medium' />
{{t 'resources.target.title_plural'}}
</nav.link>
{{/if}}
{{#if (can 'navigate model' @model collection='host-catalogs')}}
<nav.link @route='scopes.scope.host-catalogs'>
<Rose::Icon
@name='flight-icons/svg/collections-16'
@size='medium'
/>
{{t 'resources.host-catalog.title_plural'}}
</nav.link>
{{/if}}
{{#if (can 'navigate model' @model collection='credential-stores')}}
<nav.link @route='scopes.scope.credential-stores'>
<Rose::Icon @name='flight-icons/svg/key-16' @size='medium' />
{{t 'resources.credential-store.title_plural'}}
</nav.link>
{{/if}}
</Rose::Nav::Sidebar>
{{/if}}
<Rose::Nav::Sidebar @title={{t 'titles.iam'}} as |nav|>
{{! Users are available only for org/global scopes }}
{{#unless @model.isProject}}
{{#if (can 'navigate model' @model collection='users')}}
<nav.link @route='scopes.scope.users'>
<Rose::Icon @name='flight-icons/svg/users-16' @size='medium' />
{{t 'resources.user.title_plural'}}
</nav.link>
{{/if}}
{{/unless}}
{{! Groups and roles are available for all scopes }}
{{#if (can 'navigate model' @model collection='groups')}}
<nav.link @route='scopes.scope.groups'>
<Rose::Icon
@name='flight-icons/svg/folder-users-16'
@size='medium'
/>
{{t 'resources.group.title_plural'}}
</nav.link>
{{/if}}
{{#if (can 'navigate model' @model collection='roles')}}
<nav.link @route='scopes.scope.roles'>
<Rose::Icon
@name='flight-icons/svg/identity-user-16'
@size='medium'
/>
{{t 'resources.role.title_plural'}}
</nav.link>
{{/if}}
{{! Auth methods are available only for org/global scopes }}
{{#unless @model.isProject}}
{{#if (can 'navigate model' @model collection='auth-methods')}}
<nav.link @route='scopes.scope.auth-methods'>
<Rose::Icon @name='flight-icons/svg/lock-16' @size='medium' />
{{t 'resources.auth-method.title_plural'}}
</nav.link>
{{/if}}
{{/unless}}
</Rose::Nav::Sidebar>
{{#if (and @model.isGlobal (feature-flag 'ssh-session-recording'))}}
{{#if this.session.data.authenticated.isGlobal}}
<Rose::Nav::Sidebar @title={{t 'titles.settings'}} as |nav|>
<nav.link @route='scopes.scope.edit'>
{{t 'resources.global.messages.settings.title'}}
</nav.link>
</Rose::Nav::Sidebar>
{{/if}}
{{/if}}
{{#if @model.isOrg}}
{{#if this.session.data.authenticated.isGlobal}}
<Rose::Nav::Sidebar @title={{t 'titles.settings'}} as |nav|>
<nav.link @route='scopes.scope.edit'>
{{t 'resources.org.messages.settings.title'}}
</nav.link>
</Rose::Nav::Sidebar>
{{/if}}
{{/if}}
{{#if @model.isProject}}
<Rose::Nav::Sidebar @title={{t 'titles.settings'}} as |nav|>
<nav.link @route='scopes.scope.edit'>
{{t 'resources.project.messages.settings.title'}}
</nav.link>
</Rose::Nav::Sidebar>
{{/if}}
</sidebarLayout.sidebar>
<Nav.Title>{{t 'titles.scope-iam' scope=(t 'titles.global')}}</Nav.Title>
{{/if}}
<sidebarLayout.body>
<main>
{{outlet}}
</main>
</sidebarLayout.body>
<sidebarLayout.footer>
<Hds::AppFooter @theme={{this.session.data.theme}} as |AF|>
<AF.Link @href={{(doc-url)}}>{{t 'titles.documentation'}}</AF.Link>
<AF.LegalLinks />
</Hds::AppFooter>
</sidebarLayout.footer>
</Rose::Layout::Sidebar>
{{#if @model.isOrg}}
<Nav.Title>{{t
'titles.scope-level'
scope=(t 'resources.org.title')
}}</Nav.Title>
{{#if this.session.data.authenticated.isGlobal}}
<Nav.Link
@icon='org'
@text={{t 'resources.org.title_plural'}}
@route='scopes.scope.scopes'
@model='global'
/>
{{/if}}
<Nav.Link
@icon='grid'
@text={{t 'resources.project.title_plural'}}
@route='scopes.scope.scopes'
/>
{{#if (can 'navigate scope' @model collection='policies')}}
<Nav.Link
@icon='hourglass'
@text={{t 'resources.policy.title_plural'}}
@route='scopes.scope.policies'
/>
{{/if}}
<Nav.Title>{{t
'titles.scope-iam'
scope=(t 'resources.org.title')
}}</Nav.Title>
{{/if}}
{{#if @model.isProject}}
<Nav.BackLink
@text={{t 'titles.back-link' scope=this.scope.org.displayName}}
@route='scopes.scope.scopes'
@model={{@model.scopeID}}
/>
<Nav.Title>{{t 'titles.project-actions'}}</Nav.Title>
{{#if (can 'navigate model' @model collection='sessions')}}
<Nav.Link
@icon='entry-point'
@text={{t 'resources.session.title_plural'}}
@route='scopes.scope.sessions'
/>
{{/if}}
{{#if (can 'navigate model' @model collection='targets')}}
<Nav.Link
@icon='target'
@text={{t 'resources.target.title_plural'}}
@route='scopes.scope.targets'
/>
{{/if}}
{{#if (can 'navigate model' @model collection='host-catalogs')}}
<Nav.Link
@icon='collections'
@text={{t 'resources.host-catalog.title_plural'}}
@route='scopes.scope.host-catalogs'
/>
{{/if}}
{{#if (can 'navigate model' @model collection='credential-stores')}}
<Nav.Link
@icon='key'
@text={{t 'resources.credential-store.title_plural'}}
@route='scopes.scope.credential-stores'
/>
{{/if}}
<Nav.Title>{{t
'titles.scope-iam'
scope=(t 'resources.project.title')
}}</Nav.Title>
{{/if}}
{{! Users are available only for org/global scopes }}
{{#unless @model.isProject}}
{{#if (can 'navigate model' @model collection='users')}}
<Nav.Link
@icon='users'
@text={{t 'resources.user.title_plural'}}
@route='scopes.scope.users'
/>
{{/if}}
{{/unless}}
{{! Groups and roles are available for all scopes }}
{{#if (can 'navigate model' @model collection='groups')}}
<Nav.Link
@icon='folder-users'
@text={{t 'resources.group.title_plural'}}
@route='scopes.scope.groups'
/>
{{/if}}
{{#if (can 'navigate model' @model collection='roles')}}
<Nav.Link
@icon='identity-user'
@text={{t 'resources.role.title_plural'}}
@route='scopes.scope.roles'
/>
{{/if}}
{{! Auth methods are available only for org/global scopes }}
{{#unless @model.isProject}}
{{#if (can 'navigate model' @model collection='auth-methods')}}
<Nav.Link
@icon='lock'
@text={{t 'resources.auth-method.title_plural'}}
@route='scopes.scope.auth-methods'
/>
{{/if}}
{{/unless}}
{{#if (and @model.isGlobal (feature-flag 'ssh-session-recording'))}}
{{#if this.session.data.authenticated.isGlobal}}
<Nav.Title>{{t 'titles.settings'}}</Nav.Title>
<Nav.Link
@icon='settings'
@text={{t 'resources.global.messages.settings.title'}}
@route='scopes.scope.edit'
/>
{{/if}}
{{/if}}
{{#if @model.isOrg}}
{{#if this.session.data.authenticated.isGlobal}}
<Nav.Title>{{t 'titles.settings'}}</Nav.Title>
<Nav.Link
@icon='settings'
@text={{t 'resources.org.messages.settings.title'}}
@route='scopes.scope.edit'
/>
{{/if}}
{{/if}}
{{#if @model.isProject}}
<Nav.Title>{{t 'titles.settings'}}</Nav.Title>
<Nav.Link
@icon='settings'
@text={{t 'resources.project.messages.settings.title'}}
@route='scopes.scope.edit'
/>
{{/if}}
</Hds::SideNav::Portal>
{{outlet}}
@@ -1,23 +0,0 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
}}
<Rose::Nav::Sidebar @title={{t 'titles.iam'}} as |nav|>
<nav.link @route='scopes.scope.users'>
<Rose::Icon @name='flight-icons/svg/users-16' @size='medium' />
{{t 'resources.user.title_plural'}}
</nav.link>
<nav.link @route='scopes.scope.groups'>
<Rose::Icon @name='flight-icons/svg/folder-users-16' @size='medium' />
{{t 'resources.group.title_plural'}}
</nav.link>
<nav.link @route='scopes.scope.roles'>
<Rose::Icon @name='flight-icons/svg/identity-user-16' @size='medium' />
{{t 'resources.role.title_plural'}}
</nav.link>
<nav.link @route='scopes.scope.auth-methods'>
<Rose::Icon @name='flight-icons/svg/lock-16' @size='medium' />
{{t 'resources.auth-method.title_plural'}}
</nav.link>
</Rose::Nav::Sidebar>
@@ -33,7 +33,7 @@
<page.actions>
{{#if (or perms.canUpdate perms.canDelete)}}
<Hds::Dropdown as |dd|>
<Hds::Dropdown data-test-manage-alias as |dd|>
<dd.ToggleButton @text={{t 'actions.manage'}} @color='secondary' />
{{#if (and perms.canUpdate @model.destination_id)}}
<dd.Interactive
@@ -12,13 +12,16 @@
<Rose::Frame>
<:header>
<h2>{{t 'actions.authenticate'}}</h2>
<Hds::Icon @name='boundary-color' @size='24' @isInline={{true}} />
<Hds::Text::Display @tag='h2' @size='300' @weight='bold'>{{t
'actions.authenticate'
}}</Hds::Text::Display>
</:header>
<:body>
<p>{{t
<Hds::Text::Body @tag='p'>{{t
'auth.descriptions.authenticating-with-org'
name=@model.scope.displayName
}}</p>
}}</Hds::Text::Body>
<Rose::Dropdown
@text={{t 'actions.choose-different-org'}}
@@ -37,7 +37,7 @@
<page.actions>
{{#if (or perms.canUpdate perms.canDelete)}}
<Hds::Dropdown as |dd|>
<Hds::Dropdown data-test-manage-target-alias as |dd|>
<dd.ToggleButton
data-test-manage-dropdown
@text={{t 'actions.manage'}}
@@ -58,22 +58,13 @@ module('Acceptance | accounts | change password', function (hooks) {
test('visiting account change password', async function (assert) {
await visit(urls.orgScope);
await click(commonSelectors.SIDEBAR_USER_DROPDOWN);
await click(commonSelectors.HREF(urls.changePassword));
await a11yAudit();
assert.strictEqual(currentURL(), urls.changePassword);
});
test('visiting account change password from header', async function (assert) {
await visit(urls.orgScope);
// Open header utilities dropdown
await click(selectors.APP_HEADER_DROPDOWN);
// Find first element in dropdown - should be change password link
await click(commonSelectors.HREF(urls.changePassword));
assert.strictEqual(currentURL(), urls.changePassword);
});
test('can change password for account', async function (assert) {
assert.expect(2);
// TODO: address issue with ICU-15021
@@ -145,6 +136,7 @@ module('Acceptance | accounts | change password', function (hooks) {
});
await visit(urls.orgScope);
await click(commonSelectors.SIDEBAR_USER_DROPDOWN);
await click(commonSelectors.HREF(urls.changePassword));
await fillIn(
selectors.FIELD_CURRENT_PASSWORD,
@@ -3,9 +3,6 @@
* SPDX-License-Identifier: BUSL-1.1
*/
export const APP_HEADER_DROPDOWN =
'.rose-header-utilities details:first-child summary';
export const MANAGE_DROPDOWN_AUTH_METHOD =
'[data-test-manage-auth-method] button';
export const MANAGE_DROPDOWN_ACCOUNT =
@@ -17,7 +17,8 @@ module('Acceptance | aliases | delete', function (hooks) {
let aliasCount;
const DROPDOWN_BUTTON_SELECTOR = '.hds-dropdown-toggle-icon';
const DROPDOWN_BUTTON_SELECTOR =
'tbody tr td:last-child .hds-dropdown-toggle-icon';
const DELETE_DROPDOWN_SELECTOR =
'.hds-dropdown-list-item--color-critical button';
@@ -35,7 +36,6 @@ module('Acceptance | aliases | delete', function (hooks) {
globalScope: null,
aliases: null,
alias: null,
target: null,
};
hooks.beforeEach(async function () {
@@ -9,6 +9,7 @@ import { setupApplicationTest } from 'admin/tests/helpers';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import { setupIndexedDb } from 'api/test-support/helpers/indexed-db';
import { authenticateSession } from 'ember-simple-auth/test-support';
import * as commonSelectors from 'admin/tests/helpers/selectors';
module('Acceptance | aliases | list', function (hooks) {
setupApplicationTest(hooks);
@@ -116,7 +117,7 @@ module('Acceptance | aliases | list', function (hooks) {
),
);
assert
.dom('[title="General"] a:nth-of-type(1)')
.dom(commonSelectors.SIDEBAR_NAV_CONTENT)
.doesNotIncludeText(ALIAS_TITLE);
await visit(urls.aliases);
@@ -62,7 +62,7 @@ module('Acceptance | aliases | update', function (hooks) {
await click(commonSelectors.EDIT_BTN);
await fillIn(commonSelectors.FIELD_NAME, commonSelectors.FIELD_NAME_VALUE);
await click(commonSelectors.SAVE_BTN);
assert.strictEqual(instances.alias.name, commonSelectors.FIELD_NAME_VALUE);
});
@@ -86,7 +86,6 @@ module('Acceptance | aliases | update', function (hooks) {
assert.true(instances.alias.authorized_actions.includes('update'));
await visit(urls.globalScope);
await click(commonSelectors.HREF(urls.aliases));
await click(commonSelectors.TABLE_FIRST_ROW_ACTION_DROPDOWN);
@@ -27,6 +27,7 @@ import {
TYPE_AUTH_METHOD_PASSWORD,
TYPE_AUTH_METHOD_OIDC,
} from 'api/models/auth-method';
import * as commonSelectors from 'admin/tests/helpers/selectors';
module('Acceptance | authentication', function (hooks) {
setupApplicationTest(hooks);
@@ -342,8 +343,11 @@ module('Acceptance | authentication', function (hooks) {
assert.ok(getRootElement().classList.contains('rose-theme-light'));
assert.notOk(getRootElement().classList.contains('rose-theme-dark'));
// open dropdown
await click(commonSelectors.SIDEBAR_USER_DROPDOWN);
// toggle system default
await click('[name="theme"][value="system-default-theme"]');
await click('[value="system-default-theme"]');
assert.strictEqual(
currentSession().get('data.theme'),
@@ -353,14 +357,14 @@ module('Acceptance | authentication', function (hooks) {
assert.notOk(getRootElement().classList.contains('rose-theme-dark'));
// toggle dark mode
await click('[name="theme"][value="dark"]');
await click('[value="dark"]');
assert.strictEqual(currentSession().get('data.theme'), 'dark');
assert.notOk(getRootElement().classList.contains('rose-theme-light'));
assert.ok(getRootElement().classList.contains('rose-theme-dark'));
// toggle system default
await click('[name="theme"][value="system-default-theme"]');
await click('[value="system-default-theme"]');
assert.strictEqual(
currentSession().get('data.theme'),
+10 -1
View File
@@ -7,17 +7,26 @@ import { module, test } from 'qunit';
import { visit, getContext } from '@ember/test-helpers';
import { setupApplicationTest } from 'admin/tests/helpers';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import { setupIndexedDb } from 'api/test-support/helpers/indexed-db';
import { authenticateSession } from 'ember-simple-auth/test-support';
module('Acceptance | develop', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
setupIndexedDb(hooks);
let featuresService;
const DEV_TOGGLE_SELECTOR = '[data-test-dev-edition-toggle]';
hooks.beforeEach(function () {
hooks.beforeEach(async function () {
const { owner } = getContext();
featuresService = owner.lookup('service:features');
this.server.create('scope', { id: 'global' });
this.server.create('scope', {
type: 'org',
scope: { id: 'global', type: 'global' },
});
await authenticateSession({});
});
test('edition toggle is hidden when `dev-edition-toggle` is false', async function (assert) {
@@ -90,7 +90,7 @@ module('Acceptance | policies | list', function (hooks) {
].includes('create'),
);
assert
.dom(commonSelectors.GENERAL_SIDEBAR_NAV_LINK(urls.policies))
.dom(commonSelectors.SIDEBAR_NAV_LINK(urls.policies))
.doesNotExist();
await visit(urls.policies);
@@ -85,7 +85,7 @@ module('Acceptance | roles | create', function (hooks) {
),
);
assert.dom(selectors.NEW_ROLE_BTN).isVisible();
assert.dom(commonSelectors.IAM_SIDEBAR_NAV_LINK(urls.roles)).isVisible();
assert.dom(commonSelectors.SIDEBAR_NAV_LINK(urls.roles)).isVisible();
assert.dom(commonSelectors.HREF(urls.newRole)).isVisible();
});
@@ -99,7 +99,7 @@ module('Acceptance | roles | create', function (hooks) {
),
);
assert.dom(selectors.NEW_ROLE_BTN).doesNotExist();
assert.dom(commonSelectors.IAM_SIDEBAR_NAV_LINK(urls.roles)).doesNotExist();
assert.dom(commonSelectors.SIDEBAR_NAV_LINK(urls.roles)).doesNotExist();
assert.dom(commonSelectors.HREF(urls.newRole)).doesNotExist();
});
@@ -18,8 +18,8 @@ module('Acceptance | roles | principals', function (hooks) {
setupMirage(hooks);
setupIndexedDb(hooks);
const MANAGE_DROPDOWN_SELECTOR = '.hds-dropdown-toggle-button';
const ADD_PRINCIPALS_SELECTOR = '[data-test-manage-dropdown-principals]';
const MANAGE_DROPDOWN_SELECTOR = '[data-test-manage-roles-dropdown] button';
const ADD_PRINCIPALS_SELECTOR = '[data-test-manage-role-principals]';
const instances = {
scopes: {
@@ -70,7 +70,7 @@ module('Acceptance | roles | principals', function (hooks) {
test('principal can be removed from a role', async function (assert) {
await visit(urls.rolePrincipals);
assert.strictEqual(findAll('tbody tr').length, principalsCount);
await click('.hds-dropdown-toggle-icon');
await click('tbody tr td:last-child .hds-dropdown-toggle-icon');
await click('tbody tr .hds-dropdown-list-item button');
assert.strictEqual(findAll('tbody tr').length, principalsCount - 1);
});
@@ -99,7 +99,7 @@ module('Acceptance | roles | principals', function (hooks) {
});
await visit(urls.rolePrincipals);
assert.strictEqual(findAll('tbody tr').length, principalsCount);
await click('.hds-dropdown-toggle-icon');
await click('tbody tr td:last-child .hds-dropdown-toggle-icon');
await click('tbody tr .hds-dropdown-list-item button');
assert
.dom(commonSelectors.ALERT_TOAST_BODY)
@@ -135,7 +135,7 @@ module('Acceptance | roles | principals', function (hooks) {
await visit(urls.rolePrincipals);
assert.strictEqual(findAll('tbody tr').length, principalsCount);
// Remove a principal to populate association view
await click('.hds-dropdown-toggle-icon');
await click('tbody tr td:last-child .hds-dropdown-toggle-icon');
await click('tbody tr .hds-dropdown-list-item button');
assert.strictEqual(findAll('tbody tr').length, principalsCount - 1);
await click(MANAGE_DROPDOWN_SELECTOR);
@@ -18,7 +18,7 @@ module('Acceptance | roles | project-scope', function (hooks) {
const GRANT_SCOPE_ROW_SELECTOR = (id) =>
`tbody [data-test-grant-scope-row="${id}"]`;
const TABLE_ROW_SELECTOR = 'tbody tr';
const MANAGE_DROPDOWN_SELECTOR = '.hds-dropdown-toggle-button';
const MANAGE_DROPDOWN_SELECTOR = '[data-test-manage-roles-dropdown] button';
const MANAGE_SCOPES_SELECTOR = '[data-test-manage-dropdown-scopes]';
const SEARCH_INPUT_SELECTOR = '.search-filtering [type="search"]';
const NO_SCOPES_MSG_SELECTOR = '.role-grant-scopes div';
+10 -9
View File
@@ -18,10 +18,8 @@ module('Acceptance | scopes', function (hooks) {
setupMirage(hooks);
setupIndexedDb(hooks);
const ORG_SCOPES_DROPDOWN_SELECTOR = (url) =>
`.rose-header-nav .rose-dropdown [href="${url}"]`;
const PROJECT_SCOPES_DROPDOWN_SELECTOR = (url) =>
`.rose-header-nav .rose-dropdown + .rose-dropdown [href="${url}"]`;
const SCOPES_DROPDOWN_BTN = '.scope-picker button';
const SCOPES_DROPDOWN_SELECTOR = (url) => `.scope-picker [href="${url}"]`;
const SAVE_BUTTON_SELECTOR = '[type="submit"]';
const CANCEL_BUTTON_SELECTOR = '.rose-form-actions [type="button"]';
const NAME_FIELD_SELECTOR = '[name="name"]';
@@ -118,26 +116,29 @@ module('Acceptance | scopes', function (hooks) {
assert.strictEqual(currentURL(), urls.orgScopes);
});
test('can navigate among org scopes via header navigation', async function (assert) {
test('can navigate among org scopes via side-nav', async function (assert) {
await visit(urls.globalScope);
await a11yAudit();
assert.strictEqual(currentURL(), urls.globalScope);
await click(ORG_SCOPES_DROPDOWN_SELECTOR(urls.orgScope));
await click(SCOPES_DROPDOWN_BTN);
await click(SCOPES_DROPDOWN_SELECTOR(urls.orgScope));
assert.strictEqual(currentURL(), urls.orgScopes);
await click(ORG_SCOPES_DROPDOWN_SELECTOR(urls.org2Scope));
await click(SCOPES_DROPDOWN_BTN);
await click(SCOPES_DROPDOWN_SELECTOR(urls.org2Scope));
assert.strictEqual(currentURL(), urls.org2Scopes);
});
test('can navigate among project scopes via header navigation', async function (assert) {
test('can navigate among project scopes via side-nav', async function (assert) {
await visit(urls.projectScope);
await a11yAudit();
assert.strictEqual(currentURL(), urls.projectScopeEdit);
await click(PROJECT_SCOPES_DROPDOWN_SELECTOR(urls.project2Scope));
await click(SCOPES_DROPDOWN_BTN);
await click(SCOPES_DROPDOWN_SELECTOR(urls.project2Scope));
assert.strictEqual(currentURL(), urls.project2ScopeEdit);
});
+13 -4
View File
@@ -4,7 +4,7 @@
*/
import { module, test } from 'qunit';
import { visit, fillIn, waitFor } from '@ember/test-helpers';
import { currentURL, visit, fillIn, waitFor } from '@ember/test-helpers';
import { setupApplicationTest } from 'admin/tests/helpers';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import { setupIndexedDb } from 'api/test-support/helpers/indexed-db';
@@ -28,6 +28,7 @@ module('Acceptance | scopes | list', function (hooks) {
};
const urls = {
scopes: null,
globalScope: null,
orgScope: null,
};
@@ -50,12 +51,20 @@ module('Acceptance | scopes | list', function (hooks) {
type: 'project',
scope: { id: instances.scopes.org1.id, type: 'org' },
});
urls.globalScope = `/scopes/global/scopes`;
urls.scopes = '/scopes';
urls.globalScope = '/scopes/global/scopes';
urls.orgScope = `/scopes/${instances.scopes.org1.id}/scopes`;
await authenticateSession({});
});
test('user can search for a specifc org scope by id', async function (assert) {
test('user gets redirected to scopes list view', async function (assert) {
await visit(urls.scopes);
assert.strictEqual(currentURL(), urls.globalScope);
});
test('user can search for a specific org scope by id', async function (assert) {
await visit(urls.globalScope);
assert
@@ -102,7 +111,7 @@ module('Acceptance | scopes | list', function (hooks) {
.doesNotExist();
});
test('user can search for a specifc project scope by id', async function (assert) {
test('user can search for a specific project scope by id', async function (assert) {
await visit(urls.orgScope);
assert
@@ -134,7 +134,9 @@ module('Acceptance | session recordings | list', function (hooks) {
].includes('list'),
);
assert.dom(`[href="${urls.sessionRecordings}"]`).exists();
assert.dom('[title="General"]').includesText(SESSION_RECORDING_TITLE);
assert
.dom(commonSelectors.SIDEBAR_NAV_CONTENT)
.includesText(SESSION_RECORDING_TITLE);
// Visit session recordings
await click(`[href="${urls.sessionRecordings}"]`);
@@ -153,7 +155,9 @@ module('Acceptance | session recordings | list', function (hooks) {
'session-recordings'
].includes('list'),
);
assert.dom('[title="General"]').doesNotIncludeText(SESSION_RECORDING_TITLE);
assert
.dom(commonSelectors.SIDEBAR_NAV_CONTENT)
.doesNotIncludeText(SESSION_RECORDING_TITLE);
assert.dom(`[href="${urls.sessionRecordings}"]`).doesNotExist();
});
@@ -127,7 +127,7 @@ module('Acceptance | sessions | list', function (hooks) {
);
assert
.dom(commonSelectors.RESOURCES_SIDEBAR_NAV_LINK(urls.sessions))
.dom(commonSelectors.SIDEBAR_NAV_LINK(urls.sessions))
.doesNotExist();
});
@@ -20,7 +20,7 @@ module('Acceptance | storage-buckets | delete', function (hooks) {
let features;
let getStorageBucketCount;
const DROPDOWN_BUTTON_SELECTOR = '.hds-dropdown-toggle-icon';
const DROPDOWN_BUTTON_SELECTOR = 'tbody tr td:last-child button';
const DELETE_DROPDOWN_SELECTOR = '.hds-dropdown-list-item [type="button"]';
const NOTIFICATION_MSG_TEXT = 'Deleted successfully.';
@@ -94,7 +94,7 @@ module('Acceptance | storage-buckets | list', function (hooks) {
].includes('create'),
);
assert
.dom(commonSelectors.GENERAL_SIDEBAR_NAV_LINK(urls.storageBuckets))
.dom(commonSelectors.SIDEBAR_NAV_LINK(urls.storageBuckets))
.doesNotExist();
// Tests that correct message is displayed when no buckets exist
@@ -177,7 +177,7 @@ module('Acceptance | storage-buckets | list', function (hooks) {
await visit(urls.globalScope);
assert.false(featuresService.isEnabled('ssh-session-recording'));
assert
.dom(commonSelectors.GENERAL_SIDEBAR_NAV_LINK(urls.storageBuckets))
.dom(commonSelectors.SIDEBAR_NAV_LINK(urls.storageBuckets))
.doesNotExist();
});
@@ -309,7 +309,7 @@ module('Acceptance | targets | brokered credential sources', function (hooks) {
const availableCredentialsCount = getCredentialCount();
await visit(urls.brokeredCredentialSources);
assert.strictEqual(findAll('tbody tr').length, credentialLibraryCount);
await click('.hds-dropdown-toggle-icon');
await click('tbody tr td:last-child .hds-dropdown-toggle-icon');
await click('tbody tr .hds-dropdown-list-item button');
assert.strictEqual(findAll('tbody tr').length, credentialLibraryCount - 1);
await visit(urls.addBrokeredCredentialSources);
@@ -327,7 +327,7 @@ module('Acceptance | targets | brokered credential sources', function (hooks) {
const availableCredentialsCount = getCredentialLibraryCount();
await visit(urls.brokeredCredentialSources);
assert.strictEqual(findAll('tbody tr').length, credentialCount);
await click('.hds-dropdown-toggle-icon');
await click('tbody tr td:last-child .hds-dropdown-toggle-icon');
await click('tbody tr .hds-dropdown-list-item button');
assert.strictEqual(findAll('tbody tr').length, credentialCount - 1);
await visit(urls.addBrokeredCredentialSources);
@@ -365,7 +365,7 @@ module('Acceptance | targets | brokered credential sources', function (hooks) {
const count = getCredentialLibraryCount();
await visit(urls.brokeredCredentialSources);
assert.strictEqual(findAll('tbody tr').length, count);
await click('.hds-dropdown-toggle-icon');
await click('tbody tr td:last-child .hds-dropdown-toggle-icon');
await click('tbody tr .hds-dropdown-list-item button');
assert.dom(commonSelectors.ALERT_TOAST_BODY).isVisible();
});
@@ -389,7 +389,7 @@ module('Acceptance | targets | brokered credential sources', function (hooks) {
const count = getCredentialCount();
await visit(urls.brokeredCredentialSources);
assert.strictEqual(findAll('tbody tr').length, count);
await click('.hds-dropdown-toggle-icon');
await click('tbody tr td:last-child .hds-dropdown-toggle-icon');
await click('tbody tr .hds-dropdown-list-item button');
assert.dom(commonSelectors.ALERT_TOAST_BODY).isVisible();
});
@@ -130,7 +130,7 @@ module('Acceptance | targets | host-sources', function (hooks) {
await visit(urls.target);
await click(`[href="${urls.targetHostSources}"]`);
await click('.hds-dropdown-toggle-icon');
await click('tbody tr td:last-child .hds-dropdown-toggle-icon');
await click('tbody tr .hds-dropdown-list-item button');
assert.strictEqual(getTargetHostSetCount(), targetHostSetCount - 1);
@@ -166,7 +166,7 @@ module('Acceptance | targets | host-sources', function (hooks) {
await visit(urls.target);
await click(`[href="${urls.targetHostSources}"]`);
await click('.hds-dropdown-toggle-icon');
await click('tbody tr td:last-child .hds-dropdown-toggle-icon');
await click('tbody tr .hds-dropdown-list-item button');
assert.dom('tbody tr').exists({ count: targetHostSetCount });
@@ -216,7 +216,7 @@ module('Acceptance | targets | host-sources', function (hooks) {
assert.dom('tbody tr').exists({ count: targetHostSetCount });
// first, remove a target host set (otherwise none would be available to add)
await click('.hds-dropdown-toggle-icon');
await click('tbody tr td:last-child .hds-dropdown-toggle-icon');
await click('tbody tr .hds-dropdown-list-item button');
assert.dom('tbody tr').exists({ count: targetHostSetCount - 1 });
@@ -331,7 +331,7 @@ module(
const credentialLibraryCount = getCredentialLibraryCount();
await visit(urls.injectedApplicationCredentialSources);
assert.strictEqual(findAll('tbody tr').length, credentialLibraryCount);
await click('.hds-dropdown-toggle-icon');
await click('tbody tr td:last-child .hds-dropdown-toggle-icon');
await click('tbody tr .hds-dropdown-list-item button');
assert.strictEqual(
findAll('tbody tr').length,
@@ -353,7 +353,7 @@ module(
const credentialCount = getCredentialCount();
await visit(urls.injectedApplicationCredentialSources);
assert.strictEqual(findAll('tbody tr').length, credentialCount);
await click('.hds-dropdown-toggle-icon');
await click('tbody tr td:last-child .hds-dropdown-toggle-icon');
await click('tbody tr .hds-dropdown-list-item button');
assert.strictEqual(findAll('tbody tr').length, credentialCount - 1);
await visit(urls.addInjectedApplicationCredentialSources);
@@ -390,7 +390,7 @@ module(
const count = getCredentialLibraryCount();
await visit(urls.injectedApplicationCredentialSources);
assert.strictEqual(findAll('tbody tr').length, count);
await click('.hds-dropdown-toggle-icon');
await click('tbody tr td:last-child .hds-dropdown-toggle-icon');
await click('tbody tr .hds-dropdown-list-item button');
assert.dom(commonSelectors.ALERT_TOAST_BODY).isVisible();
});
@@ -416,7 +416,7 @@ module(
const count = getCredentialCount();
await visit(urls.injectedApplicationCredentialSources);
assert.strictEqual(findAll('tbody tr').length, count);
await click('.hds-dropdown-toggle-icon');
await click('tbody tr td:last-child .hds-dropdown-toggle-icon');
await click('tbody tr .hds-dropdown-list-item button');
assert.dom(commonSelectors.ALERT_TOAST_BODY).isVisible();
});
@@ -17,6 +17,7 @@ import { setupIndexedDb } from 'api/test-support/helpers/indexed-db';
import { authenticateSession } from 'ember-simple-auth/test-support';
import { TYPE_TARGET_TCP, TYPE_TARGET_SSH } from 'api/models/target';
import { STATUS_SESSION_ACTIVE } from 'api/models/session';
import * as commonSelectors from 'admin/tests/helpers/selectors';
module('Acceptance | targets | list', function (hooks) {
setupApplicationTest(hooks);
@@ -122,9 +123,7 @@ module('Acceptance | targets | list', function (hooks) {
'create',
),
);
assert
.dom('[title="Resources"] a:nth-of-type(2)')
.doesNotIncludeText('Targets');
assert.dom(commonSelectors.SIDEBAR_NAV_CONTENT).doesNotIncludeText('Targets');
});
test('user can navigate to index with only create action', async function (assert) {
@@ -17,7 +17,7 @@ module('Acceptance | targets | manage-alias', function (hooks) {
let aliasResourceTwo, aliasResourceOne, getAliasCount;
const ALIASES_SIDEBAR = '.target-sidebar-aliases';
const ALIASES_SIDEBAR_LIST = '.link-list-item';
const DROPDOWN_ACTION = '.hds-dropdown-toggle-button__text';
const DROPDOWN_ACTION = '[data-test-manage-target-alias] button';
const CLEAR_BTN_SELECTOR = '.hds-dropdown-list-item--color-action button';
const DELETE_BTN_SELECTOR = '.hds-dropdown-list-item--color-critical button';
const LINK_TO_NEW_ALIAS = '.target-sidebar-aliases .hds-button';
+4 -5
View File
@@ -35,11 +35,10 @@ export const PAGE_MESSAGE_LINK =
export const HREF = (url) => `[href="${url}"]`;
export const GENERAL_SIDEBAR_NAV_LINK = (url) =>
`[title="General"] a[href="${url}"]`;
export const RESOURCES_SIDEBAR_NAV_LINK = (url) =>
`[title="Resources"] a[href="${url}"]`;
export const IAM_SIDEBAR_NAV_LINK = (url) => `[title="IAM"] a[href="${url}"]`;
export const SIDEBAR_NAV_LINK = (url) => `.hds-side-nav__content a[href="${url}"]`;
export const SIDEBAR_NAV_CONTENT = '.hds-side-nav__content';
export const SIDEBAR_USER_DROPDOWN =
'[data-test-side-nav-user-dropdown] button';
export const ALERT_TOAST = '[data-test-toast-notification]';
export const ALERT_TOAST_BODY =
@@ -0,0 +1,204 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupRenderingTest } from 'admin/tests/helpers';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { click } from '@ember/test-helpers';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import { setupIntl } from 'ember-intl/test-support';
module('Integration | Component | scope-picker/index', function (hooks) {
setupRenderingTest(hooks);
setupMirage(hooks);
setupIntl(hooks, 'en-us');
const SCOPE_PICKER_DROPDOWN = '.scope-picker button';
const SCOPE_PICKER_DROPDOWN_ICON = '.hds-dropdown-toggle-button__icon svg';
const ORG_ADD_MORE_TEXT = '[data-test-scope-picker-org-and-more-text]';
const ORG_LIST_ITEM = '[data-test-scope-picker-org-item]';
const ORG_LIST_COUNT = '[data-test-scope-picker-org-count]';
const PROJECT_LIST_ITEM = '[data-test-scope-picker-project-item]';
const PROJECT_ADD_MORE_TEXT =
'[data-test-scope-picker-project-and-more-text]';
const PROJECT_LIST_COUNT = '[data-test-scope-picker-project-count]';
const SELECTED_SCOPE_CHECK = (id) => `[href="/scopes/${id}"] .hds-icon-check`;
let global;
let scopeService;
let store;
hooks.beforeEach(async function () {
store = this.owner.lookup('service:store');
scopeService = this.owner.lookup('service:scope');
this.server.create('scope', { id: 'global' });
global = await store.findRecord('scope', 'global');
});
test('it renders correct content when there are more than five orgs', async function (assert) {
const orgs = this.server.createList('scope', 6, {
type: 'org',
scope: { id: 'global', type: 'global' },
});
this.server.createList('scope', 2, {
type: 'project',
scope: { id: orgs[0].id, type: 'org' },
});
scopeService.org = global;
scopeService.orgsList = await store.query('scope', { scope_id: 'global' });
scopeService.projectsList = await store.query('scope', {
scope_id: orgs[0].id,
});
await render(hbs`<ScopePicker />`);
await click(SCOPE_PICKER_DROPDOWN);
assert.dom(ORG_ADD_MORE_TEXT).isVisible();
// Scope picker should always render five or less orgs.
assert.dom(ORG_LIST_ITEM).isVisible({ count: 5 });
assert.dom(ORG_LIST_COUNT).includesText(`(${orgs.length})`);
// Project scopes do not render when a project hasn't been selected.
assert.dom(PROJECT_LIST_ITEM).doesNotExist();
});
test('it renders correct content when there are five or less orgs', async function (assert) {
const orgs = this.server.createList('scope', 4, {
type: 'org',
scope: { id: 'global', type: 'global' },
});
this.server.createList('scope', 2, {
type: 'project',
scope: { id: orgs[0].id, type: 'org' },
});
scopeService.org = global;
scopeService.orgsList = await store.query('scope', { scope_id: 'global' });
scopeService.projectsList = await store.query('scope', {
scope_id: orgs[0].id,
});
await render(hbs`<ScopePicker />`);
await click(SCOPE_PICKER_DROPDOWN);
assert.dom(ORG_ADD_MORE_TEXT).doesNotExist();
// Scope picker should always render five or less orgs.
assert.dom(ORG_LIST_ITEM).isVisible({ count: 4 });
assert.dom(ORG_LIST_COUNT).doesNotExist();
// Project scopes do not render when a project hasn't been selected.
assert.dom(PROJECT_LIST_ITEM).doesNotExist();
});
test('it renders correct content when there are more than five projects', async function (assert) {
const orgs = this.server.createList('scope', 2, {
type: 'org',
scope: { id: 'global', type: 'global' },
});
this.server.createList('scope', 6, {
type: 'project',
scope: { id: orgs[0].id, type: 'org' },
});
const projects = await store.query('scope', { scope_id: orgs[0].id });
scopeService.org = global;
scopeService.project = projects[0];
scopeService.orgsList = await store.query('scope', { scope_id: 'global' });
scopeService.projectsList = projects;
await render(hbs`<ScopePicker />`);
await click(SCOPE_PICKER_DROPDOWN);
assert.dom(SCOPE_PICKER_DROPDOWN).hasText(scopeService.project.displayName);
assert.dom(PROJECT_ADD_MORE_TEXT).isVisible();
// Scope picker should always render five or less projects.
assert.dom(PROJECT_LIST_ITEM).isVisible({ count: 5 });
assert.dom(PROJECT_LIST_ITEM).hasClass('indentation');
assert.dom(PROJECT_LIST_COUNT).includesText(`(${projects.length})`);
assert.dom(PROJECT_LIST_COUNT).hasClass('indentation');
assert.dom(ORG_LIST_ITEM).isVisible({ count: orgs.length });
});
test('it renders correct content when there are five or less projects', async function (assert) {
const orgs = this.server.createList('scope', 5, {
type: 'org',
scope: { id: 'global', type: 'global' },
});
this.server.createList('scope', 4, {
type: 'project',
scope: { id: orgs[0].id, type: 'org' },
});
const projects = await store.query('scope', { scope_id: orgs[0].id });
scopeService.org = global;
scopeService.project = projects[0];
scopeService.orgsList = await store.query('scope', { scope_id: 'global' });
scopeService.projectsList = projects;
await render(hbs`<ScopePicker />`);
await click(SCOPE_PICKER_DROPDOWN);
assert.dom(SCOPE_PICKER_DROPDOWN).hasText(scopeService.project.displayName);
assert.dom(PROJECT_ADD_MORE_TEXT).doesNotExist();
// Scope picker should always render five or less projects.
assert.dom(PROJECT_LIST_ITEM).isVisible({ count: projects.length });
assert.dom(PROJECT_LIST_ITEM).hasClass('indentation');
assert.dom(PROJECT_LIST_COUNT).doesNotExist();
assert.dom(ORG_LIST_ITEM).isVisible({ count: orgs.length });
});
test('it renders correct dropdown button text and icon when selected scope is global type', async function (assert) {
this.server.create('scope', {
type: 'org',
scope: { id: 'global', type: 'global' },
});
scopeService.org = global;
scopeService.orgsList = await store.query('scope', { scope_id: 'global' });
await render(hbs`<ScopePicker />`);
await click(SCOPE_PICKER_DROPDOWN);
assert.dom(SCOPE_PICKER_DROPDOWN).hasText('Global');
assert.dom(SCOPE_PICKER_DROPDOWN_ICON).hasClass('hds-icon-globe');
assert.dom(SELECTED_SCOPE_CHECK('global')).isVisible();
});
test('it renders correct dropdown button text and icon when selected scope is org type', async function (assert) {
this.server.create('scope', {
type: 'org',
scope: { id: 'global', type: 'global' },
});
scopeService.orgsList = await store.query('scope', { scope_id: 'global' });
scopeService.org = scopeService.orgsList[0];
await render(hbs`<ScopePicker />`);
await click(SCOPE_PICKER_DROPDOWN);
assert.dom(SCOPE_PICKER_DROPDOWN).hasText(scopeService.org.displayName);
assert.dom(SCOPE_PICKER_DROPDOWN_ICON).hasClass('hds-icon-org');
assert.dom(SELECTED_SCOPE_CHECK(scopeService.org.id)).isVisible();
});
test('it renders correct dropdown button text and icon when selected scope is project type', async function (assert) {
const org = this.server.create('scope', {
type: 'org',
scope: { id: 'global', type: 'global' },
});
this.server.createList('scope', 2, {
type: 'project',
scope: { id: org.id, type: 'org' },
});
const projects = await store.query('scope', { scope_id: org.id });
scopeService.org = global;
scopeService.project = projects[0];
scopeService.orgsList = await store.query('scope', { scope_id: 'global' });
scopeService.projectsList = projects;
await render(hbs`<ScopePicker />`);
await click(SCOPE_PICKER_DROPDOWN);
assert.dom(SCOPE_PICKER_DROPDOWN).hasText(scopeService.project.displayName);
assert.dom(SCOPE_PICKER_DROPDOWN_ICON).hasClass('hds-icon-grid');
assert.dom(SELECTED_SCOPE_CHECK(scopeService.project.id)).isVisible();
});
});
+696 -141
View File
File diff suppressed because it is too large. Load diff