mirror of https://github.com/hashicorp/consul
Cc 7147 link to hcp modal (#20474)
* add link hcp modal component * integrate modal with SideNav and link to hcp banner --------- Co-authored-by: Chris Hut <tophernuts@gmail.com>pull/20564/head
parent
6c4b83c119
commit
8c05e57ac1
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:breaking-change
|
||||||
|
ui: Adds a "Link to HCP Consul Central" modal with integration to side-nav and link to HCP banner. There will be an option to disable the Link to HCP banner from the UI in a follow-up release.
|
||||||
|
```
|
|
@ -14,6 +14,7 @@ import { action } from '@ember/object';
|
||||||
export default class HcpLinkItemComponent extends Component {
|
export default class HcpLinkItemComponent extends Component {
|
||||||
@service env;
|
@service env;
|
||||||
@service('hcp-link-status') hcpLinkStatus;
|
@service('hcp-link-status') hcpLinkStatus;
|
||||||
|
@service('hcp-link-modal') hcpLinkModal;
|
||||||
|
|
||||||
get alreadyLinked() {
|
get alreadyLinked() {
|
||||||
return this.args.linkData?.isLinked;
|
return this.args.linkData?.isLinked;
|
||||||
|
@ -51,6 +52,7 @@ export default class HcpLinkItemComponent extends Component {
|
||||||
|
|
||||||
@action
|
@action
|
||||||
onLinkToConsulCentral() {
|
onLinkToConsulCentral() {
|
||||||
// TODO: https://hashicorp.atlassian.net/browse/CC-7147 open the modal
|
this.hcpLinkModal.setResourceId(this.args.linkData?.resourceId);
|
||||||
|
this.hcpLinkModal.show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { inject as service } from '@ember/service';
|
||||||
|
|
||||||
export default class LinkToHcpBannerComponent extends Component {
|
export default class LinkToHcpBannerComponent extends Component {
|
||||||
@service('hcp-link-status') hcpLinkStatus;
|
@service('hcp-link-status') hcpLinkStatus;
|
||||||
|
@service('hcp-link-modal') hcpLinkModal;
|
||||||
@service('env') env;
|
@service('env') env;
|
||||||
|
|
||||||
get notLinked() {
|
get notLinked() {
|
||||||
|
@ -21,6 +22,7 @@ export default class LinkToHcpBannerComponent extends Component {
|
||||||
}
|
}
|
||||||
@action
|
@action
|
||||||
onClusterLink() {
|
onClusterLink() {
|
||||||
// TODO: CC-7147: Open simplified modal
|
this.hcpLinkModal.setResourceId(this.args.linkData?.resourceId);
|
||||||
|
this.hcpLinkModal.show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,100 @@
|
||||||
|
{{!
|
||||||
|
Copyright (c) HashiCorp, Inc.
|
||||||
|
SPDX-License-Identifier: BUSL-1.1
|
||||||
|
}}
|
||||||
|
<DataSource @src={{uri '/${partition}/${nspace}/${dc}/policy/00000000-0000-0000-0000-000000000002'
|
||||||
|
(hash dc=@dc partition=@partition nspace=@nspace) }} as |globalReadonlyPolicy|>
|
||||||
|
<Hds::Modal id="link-to-hcp-modal" class="link-to-hcp-modal" data-test-link-to-hcp-modal
|
||||||
|
@onClose={{fn this.deactivateModal}} as |M|>
|
||||||
|
<M.Header>
|
||||||
|
Link to HCP Consul Central
|
||||||
|
</M.Header>
|
||||||
|
<M.Body>
|
||||||
|
<Hds::Form::Radio::Group data-test-link-to-hcp-modal-access-level-options @layout="vertical" @name="accessMode" as
|
||||||
|
|G|>
|
||||||
|
<G.Legend>Select cluster access mode before linking</G.Legend>
|
||||||
|
<G.HelperText>Control the level of access that HCP Consul Central has to your linked cluster.
|
||||||
|
<Hds::Link::Inline @href="https://developer.hashicorp.com/consul/docs/security/acl" @isHrefExternal={{true}}
|
||||||
|
@color="secondary">Learn more
|
||||||
|
</Hds::Link::Inline>
|
||||||
|
</G.HelperText>
|
||||||
|
<G.Radio::Field @id="accessMode-management" checked @value={{this.AccessLevel.GLOBALREADWRITE}} {{on "change"
|
||||||
|
this.onAccessModeChanged}}
|
||||||
|
as |F|>
|
||||||
|
<F.Label>Read/write</F.Label>
|
||||||
|
<F.HelperText>HCP Consul Central can perform write operations on your cluster (i.e. cluster peering).
|
||||||
|
</F.HelperText>
|
||||||
|
</G.Radio::Field>
|
||||||
|
<G.Radio::Field @id="accessMode-readonly" @value={{this.AccessLevel.GLOBALREADONLY}} {{on "change"
|
||||||
|
this.onAccessModeChanged}}
|
||||||
|
as |F|>
|
||||||
|
<F.Label>Read-only</F.Label>
|
||||||
|
<F.HelperText>HCP Consul Central can only read information from your cluster. Read-only requires an ACL token
|
||||||
|
with the “builtin/global-read-only” policy in the next step.
|
||||||
|
</F.HelperText>
|
||||||
|
</G.Radio::Field>
|
||||||
|
</Hds::Form::Radio::Group>
|
||||||
|
{{#if (and this.isReadOnlyAccessLevelSelected (can "create tokens"))}}
|
||||||
|
<div class="link-to-hcp-modal__generate-token">
|
||||||
|
{{#if globalReadonlyPolicy.data}}
|
||||||
|
<p class="hds-typography-display-100 hds-font-weight-medium font-family-sans-display">
|
||||||
|
Generate a read-only ACL token now (preferred) or copy an existing token’s secret ID
|
||||||
|
</p>
|
||||||
|
{{#if this.isTokenGenerated}}
|
||||||
|
<Hds::Card::Container data-test-link-to-hcp-modal-generate-token-card
|
||||||
|
class="link-to-hcp-modal__generate-token__copy-card"
|
||||||
|
@level="mid"
|
||||||
|
@hasBorder={{true}}>
|
||||||
|
<div>
|
||||||
|
<p class="hds-font-weight-semibold">Token secret ID</p>
|
||||||
|
<p class="hds-typography-code-200 link-to-hcp-modal__generate-token__copy-card__token"
|
||||||
|
data-test-link-to-hcp-modal-generate-token-card-value
|
||||||
|
id="tokenSecretId">
|
||||||
|
{{this.token}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Hds::Copy::Button
|
||||||
|
@text="Copy"
|
||||||
|
data-test-link-to-hcp-modal-generate-token-card-copy-button
|
||||||
|
@isIconOnly={{true}}
|
||||||
|
@targetToCopy="#tokenSecretId" />
|
||||||
|
|
||||||
|
</Hds::Card::Container>
|
||||||
|
{{else}}
|
||||||
|
<div>
|
||||||
|
<Hds::Button
|
||||||
|
data-test-link-to-hcp-modal-generate-token-button
|
||||||
|
@color="tertiary"
|
||||||
|
@text={{if this.isGeneratingToken "Generating token" "Generate a read-only ACL token"}}
|
||||||
|
@icon={{if this.isGeneratingToken "loading" "token"}}
|
||||||
|
@disabled={{this.isGeneratingToken}}
|
||||||
|
{{on "click" (fn this.onGenerateTokenClicked globalReadonlyPolicy)}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{else}}
|
||||||
|
<Hds::Alert @type="compact" data-test-link-to-hcp-modal-missed-policy-alert as |A|>
|
||||||
|
<A.Description>Could not generate token.</A.Description>
|
||||||
|
</Hds::Alert>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</M.Body>
|
||||||
|
<M.Footer as |F|>
|
||||||
|
<Hds::ButtonSet>
|
||||||
|
<Hds::Button type="button"
|
||||||
|
@text="Next: Authenticate into HCP"
|
||||||
|
@icon="external-link"
|
||||||
|
@iconPosition="trailing"
|
||||||
|
data-test-link-to-hcp-modal-next-button
|
||||||
|
@href={{hcp-authentication-link this.hcpLinkModal.resourceId this.accessLevel}}
|
||||||
|
/>
|
||||||
|
<Hds::Button type="button" @text="Cancel" @color="secondary"
|
||||||
|
data-test-link-to-hcp-modal-cancel-button
|
||||||
|
{{on "click" F.close}}
|
||||||
|
/>
|
||||||
|
</Hds::ButtonSet>
|
||||||
|
</M.Footer>
|
||||||
|
</Hds::Modal>
|
||||||
|
</DataSource>
|
|
@ -0,0 +1,64 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Component from '@glimmer/component';
|
||||||
|
import { tracked } from '@glimmer/tracking';
|
||||||
|
import { action } from '@ember/object';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
|
||||||
|
export const ACCESS_LEVEL = {
|
||||||
|
GLOBALREADONLY: 'CONSUL_ACCESS_LEVEL_GLOBAL_READ_ONLY',
|
||||||
|
GLOBALREADWRITE: 'CONSUL_ACCESS_LEVEL_GLOBAL_READ_WRITE',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class LinkToHcpModalComponent extends Component {
|
||||||
|
@service('repository/token') tokenRepo;
|
||||||
|
@service('repository/policy') policyRepo;
|
||||||
|
@service('hcp-link-modal') hcpLinkModal;
|
||||||
|
@service('router') router;
|
||||||
|
|
||||||
|
@tracked
|
||||||
|
token = '';
|
||||||
|
@tracked
|
||||||
|
accessLevel = ACCESS_LEVEL.GLOBALREADWRITE;
|
||||||
|
@tracked
|
||||||
|
isGeneratingToken = false;
|
||||||
|
AccessLevel = ACCESS_LEVEL;
|
||||||
|
|
||||||
|
get isReadOnlyAccessLevelSelected() {
|
||||||
|
return this.accessLevel === this.AccessLevel.GLOBALREADONLY;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isTokenGenerated() {
|
||||||
|
return this.token && this.token.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
deactivateModal = () => {
|
||||||
|
this.hcpLinkModal.hide();
|
||||||
|
};
|
||||||
|
|
||||||
|
onGenerateTokenClicked = (policy) => {
|
||||||
|
this.isGeneratingToken = true;
|
||||||
|
let token = this.tokenRepo.create({
|
||||||
|
Datacenter: this.args.dc,
|
||||||
|
Partition: this.args.partition,
|
||||||
|
Namespace: this.args.nspace,
|
||||||
|
Policies: [policy.data],
|
||||||
|
});
|
||||||
|
this.tokenRepo.persist(token, event).then((token) => {
|
||||||
|
this.token = token.SecretID;
|
||||||
|
this.isGeneratingToken = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
@action
|
||||||
|
onCancel() {
|
||||||
|
this.deactivateModal();
|
||||||
|
}
|
||||||
|
@action
|
||||||
|
onAccessModeChanged({ target }) {
|
||||||
|
this.accessLevel = target.value;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
.link-to-hcp-modal {
|
||||||
|
&__generate-token {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
margin-top: 8px;
|
||||||
|
|
||||||
|
&__copy-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: start;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 16px 24px;
|
||||||
|
|
||||||
|
&__token {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,6 +13,7 @@ export default class ApplicationController extends Controller {
|
||||||
@service('router') router;
|
@service('router') router;
|
||||||
@service('store') store;
|
@service('store') store;
|
||||||
@service('feedback') feedback;
|
@service('feedback') feedback;
|
||||||
|
@service('hcp-link-modal') hcpLinkModal;
|
||||||
|
|
||||||
// TODO: We currently do this in the controller instead of the router
|
// TODO: We currently do this in the controller instead of the router
|
||||||
// as the nspace and dc variables aren't available directly on the Route
|
// as the nspace and dc variables aren't available directly on the Route
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Helper from '@ember/component/helper';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A resourceId Looks like:
|
||||||
|
* organization/b4432207-bb9c-438e-a160-b98923efa979/project/4b09958c-fa91-43ab-8029-eb28d8cee9d4/hashicorp.consul.global-network-manager.cluster/test-from-api
|
||||||
|
* organization/${organizationId}/project/${projectId}/hashicorp.consul.global-network-manager.cluster/${clusterName}
|
||||||
|
*
|
||||||
|
* A HCP URL looks like:
|
||||||
|
* https://portal.cloud.hashicorp.com/services/consul/clusters/self-managed/link-existing?cluster_name=test-from-api&cluster_version=1.18.0&cluster_access_mode=CONSUL_ACCESS_LEVEL_GLOBAL_READ_WRITE&redirect_url=localhost:8500/services
|
||||||
|
*/
|
||||||
|
export const HCP_PREFIX =
|
||||||
|
'https://portal.cloud.hashicorp.com/services/consul/clusters/self-managed/link-existing';
|
||||||
|
export default class hcpAuthenticationLink extends Helper {
|
||||||
|
@service('env') env;
|
||||||
|
compute([resourceId, accessMode], hash) {
|
||||||
|
let url = new URL(HCP_PREFIX);
|
||||||
|
const clusterVersion = this.env.var('CONSUL_VERSION');
|
||||||
|
|
||||||
|
// if resourceId is empty, we still might want the user to get to the HCP sign-in page
|
||||||
|
if (resourceId) {
|
||||||
|
// Array looks like: ["organization", organizationId, "project", projectId, "hashicorp.consul.global-network-manager.cluster", "Cluster Id"]
|
||||||
|
const [, , , , , clusterName] = resourceId.split('/');
|
||||||
|
if (clusterName) {
|
||||||
|
url.searchParams.append('cluster_name', clusterName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clusterVersion) {
|
||||||
|
url.searchParams.append('cluster_version', clusterVersion);
|
||||||
|
}
|
||||||
|
if (accessMode) {
|
||||||
|
url.searchParams.append('cluster_access_mode', accessMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Service from '@ember/service';
|
||||||
|
import { tracked } from '@glimmer/tracking';
|
||||||
|
|
||||||
|
export default class HcpLinkModalService extends Service {
|
||||||
|
@tracked isModalVisible = false;
|
||||||
|
@tracked resourceId = null;
|
||||||
|
|
||||||
|
show(hcpLinkData) {
|
||||||
|
this.isModalVisible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
hide() {
|
||||||
|
this.isModalVisible = false;
|
||||||
|
}
|
||||||
|
setResourceId(resourceId) {
|
||||||
|
this.resourceId = resourceId;
|
||||||
|
}
|
||||||
|
}
|
|
@ -74,6 +74,7 @@
|
||||||
@import 'consul-ui/components/tab-nav';
|
@import 'consul-ui/components/tab-nav';
|
||||||
@import 'consul-ui/components/search-bar';
|
@import 'consul-ui/components/search-bar';
|
||||||
@import 'consul-ui/components/copyable-code';
|
@import 'consul-ui/components/copyable-code';
|
||||||
|
@import 'consul-ui/components/link-to-hcp-modal';
|
||||||
|
|
||||||
@import 'consul-ui/components/consul/loader';
|
@import 'consul-ui/components/consul/loader';
|
||||||
@import 'consul-ui/components/consul/tomography/graph';
|
@import 'consul-ui/components/consul/tomography/graph';
|
||||||
|
|
|
@ -93,47 +93,50 @@
|
||||||
as |dc dcs|
|
as |dc dcs|
|
||||||
}}
|
}}
|
||||||
{{#if (and (gt dc.Name.length 0) dcs)}}
|
{{#if (and (gt dc.Name.length 0) dcs)}}
|
||||||
|
{{#if this.hcpLinkModal.isModalVisible}}
|
||||||
{{! figure out our current DC and convert it to a model }}
|
<LinkToHcpModal @dc={{dc.Name}} @nspace={{nspace}} @partition={{partition}}/>
|
||||||
<DataSource
|
|
||||||
@src={{uri
|
|
||||||
'/${partition}/*/${dc}/datacenter-cache/${name}'
|
|
||||||
(hash dc=dc.Name partition=partition name=dc.Name)
|
|
||||||
}}
|
|
||||||
as |dc|
|
|
||||||
>
|
|
||||||
{{#if dc.data}}
|
|
||||||
<HashicorpConsul
|
|
||||||
id='wrapper'
|
|
||||||
@dcs={{dcs}}
|
|
||||||
@dc={{dc.data}}
|
|
||||||
@partition={{partition}}
|
|
||||||
@nspace={{nspace}}
|
|
||||||
@user={{hash token=token}}
|
|
||||||
@onchange={{action 'reauthorize'}}
|
|
||||||
as |consul|
|
|
||||||
>
|
|
||||||
|
|
||||||
{{#if error}}
|
|
||||||
{{! If we got an error from anything, show an error page }}
|
|
||||||
<AppError @error={{error}} @login={{consul.login.open}} />
|
|
||||||
{{else}}
|
|
||||||
{{! Otherwise show the rest of the app}}
|
|
||||||
<Outlet
|
|
||||||
@name='application'
|
|
||||||
@model={{hash app=consul user=(hash token=token) dc=dc.data dcs=dcs}}
|
|
||||||
as |o|
|
|
||||||
>
|
|
||||||
{{outlet}}
|
|
||||||
</Outlet>
|
|
||||||
|
|
||||||
{{! loading component for when we need it}}
|
|
||||||
<Consul::Loader class='view-loader' />
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
</HashicorpConsul>
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</DataSource>
|
|
||||||
|
{{! figure out our current DC and convert it to a model }}
|
||||||
|
<DataSource
|
||||||
|
@src={{uri
|
||||||
|
'/${partition}/*/${dc}/datacenter-cache/${name}'
|
||||||
|
(hash dc=dc.Name partition=partition name=dc.Name)
|
||||||
|
}}
|
||||||
|
as |dc|
|
||||||
|
>
|
||||||
|
{{#if dc.data}}
|
||||||
|
<HashicorpConsul
|
||||||
|
id='wrapper'
|
||||||
|
@dcs={{dcs}}
|
||||||
|
@dc={{dc.data}}
|
||||||
|
@partition={{partition}}
|
||||||
|
@nspace={{nspace}}
|
||||||
|
@user={{hash token=token}}
|
||||||
|
@onchange={{action 'reauthorize'}}
|
||||||
|
as |consul|
|
||||||
|
>
|
||||||
|
|
||||||
|
{{#if error}}
|
||||||
|
{{! If we got an error from anything, show an error page }}
|
||||||
|
<AppError @error={{error}} @login={{consul.login.open}} />
|
||||||
|
{{else}}
|
||||||
|
{{! Otherwise show the rest of the app}}
|
||||||
|
<Outlet
|
||||||
|
@name='application'
|
||||||
|
@model={{hash app=consul user=(hash token=token) dc=dc.data dcs=dcs}}
|
||||||
|
as |o|
|
||||||
|
>
|
||||||
|
{{outlet}}
|
||||||
|
</Outlet>
|
||||||
|
|
||||||
|
{{! loading component for when we need it}}
|
||||||
|
<Consul::Loader class='view-loader' />
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
</HashicorpConsul>
|
||||||
|
{{/if}}
|
||||||
|
</DataSource>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/let}}
|
{{/let}}
|
||||||
</DataSource>
|
</DataSource>
|
||||||
|
|
|
@ -6,15 +6,29 @@
|
||||||
import { module, test } from 'qunit';
|
import { module, test } from 'qunit';
|
||||||
import { click, visit } from '@ember/test-helpers';
|
import { click, visit } from '@ember/test-helpers';
|
||||||
import { setupApplicationTest } from 'ember-qunit';
|
import { setupApplicationTest } from 'ember-qunit';
|
||||||
|
import HcpLinkModalService from 'consul-ui/services/hcp-link-modal';
|
||||||
|
|
||||||
const bannerSelector = '[data-test-link-to-hcp-banner]';
|
const bannerSelector = '[data-test-link-to-hcp-banner]';
|
||||||
const linkToHcpSelector = '[data-test-link-to-hcp]';
|
const linkToHcpSelector = '[data-test-link-to-hcp]';
|
||||||
|
const linkToHcpBannerButtonSelector = '[data-test-link-to-hcp-banner-button]';
|
||||||
|
const linkToHcpModalSelector = '[data-test-link-to-hcp-modal]';
|
||||||
|
const linkToHcpModalCancelButtonSelector = '[data-test-link-to-hcp-modal-cancel-button]';
|
||||||
module('Acceptance | link to hcp', function (hooks) {
|
module('Acceptance | link to hcp', function (hooks) {
|
||||||
setupApplicationTest(hooks);
|
setupApplicationTest(hooks);
|
||||||
|
const correctResourceId =
|
||||||
|
'organization/b4432207-bb9c-438e-a160-b98923efa979/project/4b09958c-fa91-43ab-8029-eb28d8cee9d4/hashicorp.consul.global-network-manager.cluster/test-from-api';
|
||||||
|
|
||||||
hooks.beforeEach(function () {
|
hooks.beforeEach(function () {
|
||||||
// clear local storage so we don't have any settings
|
// clear local storage so we don't have any settings
|
||||||
window.localStorage.clear();
|
window.localStorage.clear();
|
||||||
|
this.owner.register(
|
||||||
|
'service:hcp-link-modal',
|
||||||
|
class extends HcpLinkModalService {
|
||||||
|
setResourceId(resourceId) {
|
||||||
|
super.setResourceId(correctResourceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the banner and nav item are initially displayed on services page', async function (assert) {
|
test('the banner and nav item are initially displayed on services page', async function (assert) {
|
||||||
|
@ -35,4 +49,27 @@ module('Acceptance | link to hcp', function (hooks) {
|
||||||
// link to HCP nav item still there
|
// link to HCP nav item still there
|
||||||
assert.dom(linkToHcpSelector).isVisible('Link to HCP nav item is visible by default');
|
assert.dom(linkToHcpSelector).isVisible('Link to HCP nav item is visible by default');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('the link to hcp modal window appears when trigger from side-nav item and from banner', async function (assert) {
|
||||||
|
// default route is services page so we're good here
|
||||||
|
await visit('/');
|
||||||
|
// Expect the banner to be visible by default
|
||||||
|
assert.dom(bannerSelector).isVisible('Banner is visible by default');
|
||||||
|
// expect linkToHCP nav item to be visible as well
|
||||||
|
assert.dom(linkToHcpSelector).isVisible('Link to HCP nav item is visible by default');
|
||||||
|
// Click on the link to HCP banner button
|
||||||
|
await click(`${bannerSelector} ${linkToHcpBannerButtonSelector}`);
|
||||||
|
|
||||||
|
// link to HCP modal appears
|
||||||
|
assert.dom(linkToHcpModalSelector).isVisible('Link to HCP modal is visible');
|
||||||
|
// Click on the cancel button
|
||||||
|
await click(`${linkToHcpModalSelector} ${linkToHcpModalCancelButtonSelector}`);
|
||||||
|
assert.dom(linkToHcpModalSelector).doesNotExist('Link to HCP modal is gone after cancel');
|
||||||
|
|
||||||
|
// Click on the link to HCP nav item
|
||||||
|
await click(`${linkToHcpSelector} button`);
|
||||||
|
|
||||||
|
// link to HCP modal appears
|
||||||
|
assert.dom(linkToHcpModalSelector).isVisible('Link to HCP modal is visible');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,192 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { module, test } from 'qunit';
|
||||||
|
import { setupRenderingTest } from 'ember-qunit';
|
||||||
|
import { click, render } from '@ember/test-helpers';
|
||||||
|
import hbs from 'htmlbars-inline-precompile';
|
||||||
|
import Service, { inject as service } from '@ember/service';
|
||||||
|
import DataSourceComponent from 'consul-ui/components/data-source/index';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
import { BlockingEventSource as RealEventSource } from 'consul-ui/utils/dom/event-source';
|
||||||
|
import { ACCESS_LEVEL } from 'consul-ui/components/link-to-hcp-modal';
|
||||||
|
|
||||||
|
const modalSelector = '[data-test-link-to-hcp-modal]';
|
||||||
|
const modalOptionReadOnlySelector = '#accessMode-readonly';
|
||||||
|
const modalGenerateTokenCardSelector = '[data-test-link-to-hcp-modal-generate-token-card]';
|
||||||
|
const modalGenerateTokenCardValueSelector =
|
||||||
|
'[data-test-link-to-hcp-modal-generate-token-card-value]';
|
||||||
|
const modalGenerateTokenCardCopyButtonSelector =
|
||||||
|
'[data-test-link-to-hcp-modal-generate-token-card-copy-button]';
|
||||||
|
const modalGenerateTokenButtonSelector = '[data-test-link-to-hcp-modal-generate-token-button]';
|
||||||
|
const modalGenerateTokenMissedPolicyAlertSelector =
|
||||||
|
'[data-test-link-to-hcp-modal-missed-policy-alert]';
|
||||||
|
const modalNextButtonSelector = '[data-test-link-to-hcp-modal-next-button]';
|
||||||
|
const modalCancelButtonSelector = '[data-test-link-to-hcp-modal-cancel-button]';
|
||||||
|
const resourceId =
|
||||||
|
'organization/b4432207-bb9c-438e-a160-b98923efa979/project/4b09958c-fa91-43ab-8029-eb28d8cee9d4/hashicorp.consul.global-network-manager.cluster/test-from-api';
|
||||||
|
|
||||||
|
module('Integration | Component | link-to-hcp-modal', function (hooks) {
|
||||||
|
let originalClipboardWriteText;
|
||||||
|
let hideModal = sinon.stub();
|
||||||
|
const close = sinon.stub();
|
||||||
|
const source = new RealEventSource();
|
||||||
|
|
||||||
|
setupRenderingTest(hooks);
|
||||||
|
|
||||||
|
hooks.beforeEach(function () {
|
||||||
|
const fakeService = class extends Service {
|
||||||
|
close = close;
|
||||||
|
open() {
|
||||||
|
source.getCurrentEvent = function () {
|
||||||
|
return { data: { Name: 'global-read-only', ID: '00000000-0000-0000-0000-000000000002' } };
|
||||||
|
};
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.owner.register('service:data-source/fake-service', fakeService);
|
||||||
|
this.owner.register(
|
||||||
|
'component:data-source',
|
||||||
|
class extends DataSourceComponent {
|
||||||
|
@service('data-source/fake-service') dataSource;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this.owner.register(
|
||||||
|
'service:abilities',
|
||||||
|
class Stub extends Service {
|
||||||
|
can(permission) {
|
||||||
|
if (permission === 'create tokens') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this.owner.register(
|
||||||
|
'service:hcp-link-modal',
|
||||||
|
class Stub extends Service {
|
||||||
|
resourceId = resourceId;
|
||||||
|
hide = hideModal;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
originalClipboardWriteText = navigator.clipboard.writeText;
|
||||||
|
navigator.clipboard.writeText = sinon.stub();
|
||||||
|
});
|
||||||
|
|
||||||
|
hooks.afterEach(function () {
|
||||||
|
navigator.clipboard.writeText = originalClipboardWriteText;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it renders modal', async function (assert) {
|
||||||
|
await render(hbs`<LinkToHcpModal @dc="dc-1"
|
||||||
|
@nspace="default"
|
||||||
|
@partition="-" />`);
|
||||||
|
|
||||||
|
assert.dom(modalSelector).exists({ count: 1 });
|
||||||
|
// select read-only
|
||||||
|
await click(`${modalSelector} ${modalOptionReadOnlySelector}`);
|
||||||
|
|
||||||
|
// when read-only selected, it shows the generate token button
|
||||||
|
assert.dom(`${modalSelector} ${modalGenerateTokenButtonSelector}`).isVisible();
|
||||||
|
|
||||||
|
// with the correct policy, it doesn't show the missed policy alert
|
||||||
|
assert.dom(`${modalSelector} ${modalGenerateTokenMissedPolicyAlertSelector}`).doesNotExist();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it updates next link on option selected', async function (assert) {
|
||||||
|
await render(hbs`<LinkToHcpModal @dc="dc-1"
|
||||||
|
@nspace="default"
|
||||||
|
@partition="-" />`);
|
||||||
|
|
||||||
|
let hrefValue = this.element
|
||||||
|
.querySelector(`${modalSelector} ${modalNextButtonSelector}`)
|
||||||
|
.getAttribute('href');
|
||||||
|
assert.ok(
|
||||||
|
hrefValue.includes(ACCESS_LEVEL.GLOBALREADWRITE),
|
||||||
|
'next link includes read/write access level'
|
||||||
|
);
|
||||||
|
|
||||||
|
// select read-only
|
||||||
|
await click(`${modalSelector} ${modalOptionReadOnlySelector}`);
|
||||||
|
|
||||||
|
hrefValue = this.element
|
||||||
|
.querySelector(`${modalSelector} ${modalNextButtonSelector}`)
|
||||||
|
.getAttribute('href');
|
||||||
|
assert.ok(
|
||||||
|
hrefValue.includes(ACCESS_LEVEL.GLOBALREADONLY),
|
||||||
|
'next link includes read-only access level'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it creates token and copy it to clipboard', async function (assert) {
|
||||||
|
await render(hbs`<LinkToHcpModal @dc="dc-1"
|
||||||
|
@nspace="default"
|
||||||
|
@partition="-" />`);
|
||||||
|
// select read-only
|
||||||
|
await click(`${modalSelector} ${modalOptionReadOnlySelector}`);
|
||||||
|
assert
|
||||||
|
.dom(`${modalSelector} ${modalGenerateTokenButtonSelector}`)
|
||||||
|
.hasText('Generate a read-only ACL token');
|
||||||
|
|
||||||
|
// with the correct policy, it doesn't show the missed policy alert
|
||||||
|
assert.dom(`${modalSelector} ${modalGenerateTokenMissedPolicyAlertSelector}`).doesNotExist();
|
||||||
|
|
||||||
|
// trigger generate token
|
||||||
|
await click(`${modalSelector} ${modalGenerateTokenButtonSelector}`);
|
||||||
|
|
||||||
|
assert.dom(`${modalSelector} ${modalGenerateTokenCardSelector}`).isVisible();
|
||||||
|
assert.dom(`${modalSelector} ${modalGenerateTokenCardValueSelector}`).exists();
|
||||||
|
const tokenValue = this.element.querySelector(
|
||||||
|
`${modalSelector} ${modalGenerateTokenCardValueSelector}`
|
||||||
|
).textContent;
|
||||||
|
// click on copy button
|
||||||
|
await click(`${modalSelector} ${modalGenerateTokenCardCopyButtonSelector}`);
|
||||||
|
assert.ok(
|
||||||
|
navigator.clipboard.writeText.called,
|
||||||
|
'clipboard write function is called when copy button is clicked'
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
navigator.clipboard.writeText.calledWith(tokenValue.trim()),
|
||||||
|
'clipboard contains expected value'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it calls hcpLinkModal.hide when closing modal', async function (assert) {
|
||||||
|
await render(hbs`<LinkToHcpModal @dc="dc-1"
|
||||||
|
@nspace="default"
|
||||||
|
@partition="-" />`);
|
||||||
|
|
||||||
|
await click(`${modalSelector} ${modalCancelButtonSelector}`);
|
||||||
|
|
||||||
|
assert.ok(hideModal.called, 'hide method is called when cancel button is clicked');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it shows an alert when policy was not loaded and it is not possible to generate a token', async function (assert) {
|
||||||
|
// creating a fake service that will return an empty policy
|
||||||
|
const fakeService = class extends Service {
|
||||||
|
close = close;
|
||||||
|
open() {
|
||||||
|
source.getCurrentEvent = function () {
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.owner.register('service:data-source/fake-service', fakeService);
|
||||||
|
|
||||||
|
await render(hbs`<LinkToHcpModal @dc="dc-1"
|
||||||
|
@nspace="default"
|
||||||
|
@partition="-" />`);
|
||||||
|
|
||||||
|
assert.dom(modalSelector).exists({ count: 1 });
|
||||||
|
// select read-only
|
||||||
|
await click(`${modalSelector} ${modalOptionReadOnlySelector}`);
|
||||||
|
|
||||||
|
// when read-only selected and no policy, it doesn't show the generate token button
|
||||||
|
assert.dom(`${modalSelector} ${modalGenerateTokenButtonSelector}`).doesNotExist();
|
||||||
|
// Missed policy alert is visible
|
||||||
|
assert.dom(`${modalSelector} ${modalGenerateTokenMissedPolicyAlertSelector}`).isVisible();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,80 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: BUSL-1.1
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { module, test } from 'qunit';
|
||||||
|
import { render } from '@ember/test-helpers';
|
||||||
|
import hbs from 'htmlbars-inline-precompile';
|
||||||
|
import { setupRenderingTest } from 'ember-qunit';
|
||||||
|
import { HCP_PREFIX } from 'consul-ui/helpers/hcp-authentication-link';
|
||||||
|
import { EnvStub } from 'consul-ui/services/env';
|
||||||
|
|
||||||
|
// organization/b4432207-bb9c-438e-a160-b98923efa979/project/4b09958c-fa91-43ab-8029-eb28d8cee9d4/hashicorp.consul.global-network-manager.cluster/test-from-api
|
||||||
|
const clusterName = 'hello';
|
||||||
|
const clusterVersion = '1.18.0';
|
||||||
|
const accessMode = 'CONSUL_ACCESS_LEVEL_GLOBAL_READ_WRITE';
|
||||||
|
const projectId = '4b09958c-fa91-43ab-8029-eb28d8cee9d4';
|
||||||
|
const realResourceId = `organization/b4432207-bb9c-438e-a160-b98923efa979/project/${projectId}/hashicorp.consul.global-network-manager.cluster/${clusterName}`;
|
||||||
|
module('Integration | Helper | hcp-authentication-link', function (hooks) {
|
||||||
|
setupRenderingTest(hooks);
|
||||||
|
hooks.beforeEach(function () {
|
||||||
|
this.owner.register(
|
||||||
|
'service:env',
|
||||||
|
class Stub extends EnvStub {
|
||||||
|
stubEnv = {
|
||||||
|
CONSUL_VERSION: clusterVersion,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
test('it makes a URL out of a real resourceId', async function (assert) {
|
||||||
|
this.resourceId = realResourceId;
|
||||||
|
|
||||||
|
await render(hbs`{{hcp-authentication-link resourceId}}`);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
this.element.textContent.trim(),
|
||||||
|
`${HCP_PREFIX}?cluster_name=${clusterName}&cluster_version=${clusterVersion}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it returns correct link with invalid resourceId', async function (assert) {
|
||||||
|
this.resourceId = 'invalid';
|
||||||
|
|
||||||
|
await render(hbs`{{hcp-authentication-link resourceId}}`);
|
||||||
|
assert.equal(
|
||||||
|
this.element.textContent.trim(),
|
||||||
|
`${HCP_PREFIX}?cluster_version=${clusterVersion}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// not enough items in id
|
||||||
|
this.resourceId =
|
||||||
|
'`organization/b4432207-bb9c-438e-a160-b98923efa979/project/${projectId}/hashicorp.consul.global-network-manager.cluster`';
|
||||||
|
await render(hbs`{{hcp-authentication-link resourceId}}`);
|
||||||
|
assert.equal(
|
||||||
|
this.element.textContent.trim(),
|
||||||
|
`${HCP_PREFIX}?cluster_version=${clusterVersion}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// value is null
|
||||||
|
this.resourceId = null;
|
||||||
|
await render(hbs`{{hcp-authentication-link resourceId}}`);
|
||||||
|
assert.equal(
|
||||||
|
this.element.textContent.trim(),
|
||||||
|
`${HCP_PREFIX}?cluster_version=${clusterVersion}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it makes a URL out of a real resourceId and accessLevel, if passed', async function (assert) {
|
||||||
|
this.resourceId = realResourceId;
|
||||||
|
this.accessMode = accessMode;
|
||||||
|
|
||||||
|
await render(hbs`{{hcp-authentication-link resourceId accessMode}}`);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
this.element.textContent.trim(),
|
||||||
|
`${HCP_PREFIX}?cluster_name=${clusterName}&cluster_version=${clusterVersion}&cluster_access_mode=${accessMode}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue