Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add nav bar item to show HCP link status and encourage folks to link #20370

Merged
merged 20 commits into from
Feb 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
2dd5ecf
Convert consul-hcp to a simpler component
chris-hut Jan 24, 2024
bcfd789
update existing test to use envStub helper
chris-hut Jan 25, 2024
f42152a
Merge branch 'main' into CC-7146/convert-consul-hcp-to-a-simpler-comp…
chris-hut Jan 25, 2024
e2e9af5
An hcp link item for the navbar
chris-hut Jan 25, 2024
6acfe1c
A method of linking to HCP
chris-hut Jan 26, 2024
247c18e
Merge branch 'main' of github.com:hashicorp/consul into CC-7146/hcp-l…
chris-hut Jan 26, 2024
026ee28
Hook up fetching linking status to the nav-item
chris-hut Jan 26, 2024
7d4e39f
Hooking up fetching link status to the hcp link friend
chris-hut Jan 26, 2024
5451806
Adding some tests
chris-hut Jan 26, 2024
eeb6a70
Merge branch 'main' of github.com:hashicorp/consul into CC-7146/hcp-l…
chris-hut Jan 27, 2024
e125f98
Merge branch 'main' of github.com:hashicorp/consul into CC-7146/hcp-l…
chris-hut Jan 29, 2024
4f8d369
remove a comment - but also fix padding justify-content
chris-hut Jan 29, 2024
21b019f
Fix the banner tests
chris-hut Jan 29, 2024
f40ad19
Adding permission tests as well
chris-hut Jan 29, 2024
cb4a35d
some more sane formatting
chris-hut Jan 29, 2024
db22d5d
Rename function with its now multipurpose use
chris-hut Jan 29, 2024
85555d3
Feature change: No more NEW Badge since it breaks padding - instead a…
chris-hut Jan 29, 2024
1436ce6
Merge branch 'main' of github.com:hashicorp/consul into CC-7146/hcp-l…
chris-hut Jan 30, 2024
ce347ef
Removing unused class
chris-hut Jan 31, 2024
9f99acd
Merge branch 'main' into CC-7146/hcp-link-item-in-the-nav-bar
chris-hut Jan 31, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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';
}
Comment on lines +8 to +10
Copy link
Contributor Author

@chris-hut chris-hut Jan 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would love some eyes on this, from Melissa, we need the operator write permission, is this enough?

By adding this I'm able to do this.abilities.can('write operators'), and it seems to be checking the right permission

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

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
Loading