Skip to content

Commit

Permalink
Add nav bar item to show HCP link status and encourage folks to link (#…
Browse files Browse the repository at this point in the history
…20370)

* Convert consul-hcp to a simpler component

* update existing test to use envStub helper

* An hcp link item for the navbar

* A method of linking to HCP

* Hook up fetching linking status to the nav-item

* Hooking up fetching link status to the hcp link friend

* Adding some tests

* remove a comment - but also fix padding justify-content

* Fix the banner tests

* Adding permission tests as well

* some more sane formatting

* Rename function with its now multipurpose use

* Feature change: No more NEW Badge since it breaks padding - instead a linked badge

* Removing unused class
  • Loading branch information
chris-hut authored Feb 1, 2024
1 parent 4902510 commit 22e6ce0
Show file tree
Hide file tree
Showing 12 changed files with 423 additions and 68 deletions.
10 changes: 10 additions & 0 deletions ui/packages/consul-ui/app/abilities/operator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/

import BaseAbility from './base';

export default class OperatorAbility extends BaseAbility {
resource = 'operator';
}
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,9 @@
class='hds-side-nav-hide-when-minimized consul-side-nav__selector-group'
as |SNL|
>
<HcpNavItem @list={{SNL}}/>
<DataSource @src={{uri '/${partition}/*/${dc}/hcp-link' (hash dc=@dc partition=@partition name=@dc)}} as |hcpLink|>
<HcpNavItem @list={{SNL}} @linkData={{hcpLink.data}} />
</DataSource>
<Consul::Datacenter::Selector
@list={{SNL}}
@dc={{@dc}}
Expand Down
19 changes: 19 additions & 0 deletions ui/packages/consul-ui/app/components/hcp-nav-item/index.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,24 @@
@isHrefExternal={{true}}
data-test-back-to-hcp
/>
{{else}}
{{#if this.shouldDisplayNavLinkItem}}
{{#if this.alreadyLinked}}
<SNL.Link
@text="HCP Consul Central"
@href={{hcp-resource-id-to-link @linkData.resourceId}}
@isHrefExternal={{true}}
@badge="Linked"
data-test-linked-cluster-hcp-link
/>
{{else}}
<SNL.Item data-test-link-to-hcp>
<button type="button" class="hds-side-nav__list-item-link hcp-nav-item" {{on "click" this.onLinkToConsulCentral}}>
<Hds::Text::Body @size='200'>Link to HCP Consul Central</Hds::Text::Body>
<FlightIcon class='w-4 h-4' @size='24' @name='arrow-right'/>
</button>
</SNL.Item>
{{/if}}
{{/if}}
{{/if}}
{{/let}}
35 changes: 35 additions & 0 deletions ui/packages/consul-ui/app/components/hcp-nav-item/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,52 @@

import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';

/**
* If the user has accessed consul from HCP managed consul, we do NOT want to display the
* "HCP Consul Central↗️" link in the nav bar. As we're already displaying a BackLink to HCP.
*/
export default class HcpLinkItemComponent extends Component {
@service env;
@service('hcp-link-status') hcpLinkStatus;

get alreadyLinked() {
return this.args.linkData?.isLinked;
}

get shouldDisplayNavLinkItem() {
const alreadyLinked = this.alreadyLinked;
const undefinedResourceId = !this.args.linkData?.resourceId;
const unauthorizedToLink = !this.hcpLinkStatus.hasPermissionToLink;
const undefinedLinkStatus = this.args.linkData?.isLinked === undefined;

// We need permission to link to display the link nav item
if (unauthorizedToLink) {
return false;
}

// If the link status is undefined, we don't want to display the link nav item
if (undefinedLinkStatus) {
return false;
}

// If the user has already linked, but we don't have the resourceId to link them to HCP, we don't want to display the link nav item
if (alreadyLinked && undefinedResourceId) {
return false;
}

return true;
}

get shouldShowBackToHcpItem() {
const isConsulHcpUrlDefined = !!this.env.var('CONSUL_HCP_URL');
const isConsulHcpEnabled = !!this.env.var('CONSUL_HCP_ENABLED');
return isConsulHcpEnabled && isConsulHcpUrlDefined;
}

@action
onLinkToConsulCentral() {
// TODO: https://hashicorp.atlassian.net/browse/CC-7147 open the modal
}
}
32 changes: 32 additions & 0 deletions ui/packages/consul-ui/app/helpers/hcp-resource-id-to-link.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/

import Helper from '@ember/component/helper';

/**
* 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.hcp.dev/services/consul/clusters/self-managed/test-from-api?project_id=4b09958c-fa91-43ab-8029-eb28d8cee9d4
* ${HCP_PREFIX}/${clusterName}?project_id=${projectId}
*/
export const HCP_PREFIX =
'https://portal.cloud.hashicorp.com/services/consul/clusters/self-managed';
export default class hcpResourceIdToLink extends Helper {
// TODO: How can we figure out different HCP environments?
compute([resourceId], hash) {
let url = HCP_PREFIX;
// Array looks like: ["organization", organizationId, "project", projectId, "hashicorp.consul.global-network-manager.cluster", "Cluster Id"]
const [, , , projectId, , clusterName] = resourceId.split('/');
if (!projectId || !clusterName) {
return '';
}

url += `/${clusterName}?project_id=${projectId}`;
return url;
}
}
7 changes: 6 additions & 1 deletion ui/packages/consul-ui/app/services/hcp-link-status.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,17 @@ const LOCAL_STORAGE_KEY = 'consul:hideHcpLinkBanner';

export default class HcpLinkStatus extends Service {
@service('env') env;
@service abilities;
@tracked
userDismissedBanner = false;

get shouldDisplayBanner() {
const hcpLinkEnabled = this.env.var('CONSUL_HCP_LINK_ENABLED');
return !this.userDismissedBanner && hcpLinkEnabled;
return !this.userDismissedBanner && this.hasPermissionToLink && hcpLinkEnabled;
}

get hasPermissionToLink() {
return this.abilities.can('write operators') && this.abilities.can('write acls');
}

constructor() {
Expand Down
48 changes: 45 additions & 3 deletions ui/packages/consul-ui/app/services/repository/hcp-link.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,44 @@ import RepositoryService from 'consul-ui/services/repository';
import dataSource from 'consul-ui/decorators/data-source';

export default class HcpLinkService extends RepositoryService {
/**
* Data looks like
* {
* "data": {
* "clientId": "5wZyAPvDFbgDdO3439m8tufwO9hElphu",
* "clientSecret": "SWX0XShcp3doc7RF8YCjJ-WATyeMAjFaf1eA0mnzlNHLF4IXbFz6xyjSZvHzAR_i",
* "resourceId": "organization/b4432207-bb9c-438e-a160-b98923efa979/project/4b09958c-fa91-43ab-8029-eb28d8cee9d4/hashicorp.consul.global-network-manager.cluster/test-from-api"
* },
* "generation": "01HMSDHXQTCQGD3Z68B3H58YFE",
* "id": {
* "name": "global",
* "tenancy": {
* "peerName": "local"
* },
* "type": {
* "group": "hcp",
* "groupVersion": "v2",
* "kind": "Link"
* },
* "uid": "01HMSDHXQTCQGD3Z68B10WBWHX"
* },
* "status": {
* "consul.io/hcp/link": {
* "conditions": [
* {
* "message": "Failed to link to HCP",
* "reason": "FAILED",
* "state": "STATE_FALSE",
* "type": "linked"
* }
* ],
* "observedGeneration": "01HMSDHXQTCQGD3Z68B3H58YFE",
* "updatedAt": "2024-01-22T20:24:57.141144170Z"
* }
* },
* "version": "57"
* }
*/
@dataSource('/:partition/:ns/:dc/hcp-link')
async fetch({ partition, ns, dc }, { uri }, request) {
let result;
Expand All @@ -16,15 +54,19 @@ export default class HcpLinkService extends RepositoryService {
GET /api/hcp/v2/link/global
`
)((headers, body) => {
const isLinked = (body.status['consul.io/hcp/link']['conditions'] || []).some(
(condition) => condition.type === 'linked' && condition.state === 'STATE_TRUE'
);
const resourceId = body.data?.resourceId;

return {
meta: {
version: 2,
uri: uri,
},
body: {
isLinked: (body.status['consul.io/hcp/link']['conditions'] || []).some(
(condition) => condition.type === 'linked' && condition.state === 'STATE_TRUE'
),
isLinked,
resourceId,
},
headers,
};
Expand Down
31 changes: 18 additions & 13 deletions ui/packages/consul-ui/mock-api/api/hcp/v2/link/global
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
{
"status": {
"consul.io/hcp/link": {
"conditions": [
{
"message": "Successfully linked to cluster 'organization/f53e5646-6529-4698-ae29-d74f8bd22a01/project/6994bb7a-5561-4d5c-8bb0-cf40177e5b77/hashicorp.consul.global-network-manager.cluster/mkam-vm'",
"reason": "SUCCESS",
"state": "STATE_FALSE",
"type": "linked"
}
],
"observedGeneration":"01HMA2VPHVKNF6QR8TD07KDN5K",
"updatedAt":"2024-01-16T21:29:25.923140Z"
}
"data": {
"clientId": "5wZyAPvDFbgDdO3439m8tufwO9hElphu",
"clientSecret": "SWX0XShcp3doc7RF8YCjJ-WATyeMAjFaf1eA0mnzlNHLF4IXbFz6xyjSZvHzAR_i",
"resourceId": "organization/b4432207-bb9c-438e-a160-b98923efa979/project/4b09958c-fa91-43ab-8029-eb28d8cee9d4/hashicorp.consul.global-network-manager.cluster/test-from-api"
},
"status": {
"consul.io/hcp/link": {
"conditions": [
{
"message": "Successfully linked to cluster 'organization/f53e5646-6529-4698-ae29-d74f8bd22a01/project/6994bb7a-5561-4d5c-8bb0-cf40177e5b77/hashicorp.consul.global-network-manager.cluster/mkam-vm'",
"reason": "SUCCESS",
"state": "STATE_FALSE",
"type": "linked"
}
],
"observedGeneration":"01HMA2VPHVKNF6QR8TD07KDN5K",
"updatedAt":"2024-01-16T21:29:25.923140Z"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import { setupApplicationTest } from 'ember-qunit';
import { EnvStub } from 'consul-ui/services/env';

const bannerSelector = '[data-test-link-to-hcp-banner]';
module('Acceptance | link to hcp banner', function (hooks) {
const linkToHcpSelector = '[data-test-link-to-hcp]';
module('Acceptance | link to hcp', function (hooks) {
setupApplicationTest(hooks);

hooks.beforeEach(function () {
Expand All @@ -25,18 +26,23 @@ module('Acceptance | link to hcp banner', function (hooks) {
);
});

test('the banner is initially displayed on services page', async function (assert) {
assert.expect(3);
test('the banner and nav item are initially displayed on services page', 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).exists({ count: 1 });
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 dismiss button
await click(`${bannerSelector} button[aria-label="Dismiss"]`);
assert.dom(bannerSelector).doesNotExist('Banner is gone after dismissing');
// link to HCP nav item still there
assert.dom(linkToHcpSelector).isVisible('Link to HCP nav item is visible by default');
// Refresh the page
await visit('/');
assert.dom(bannerSelector).doesNotExist('Banner is still gone after refresh');
// link to HCP nav item still there
assert.dom(linkToHcpSelector).isVisible('Link to HCP nav item is visible by default');
});

test('the banner is not displayed if the env var is not set', async function (assert) {
Expand Down
Loading

0 comments on commit 22e6ce0

Please sign in to comment.