-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
Allow Managed clusters to see Secrets Sync Overview and Sidebar nav #26649
Allow Managed clusters to see Secrets Sync Overview and Sidebar nav #26649
Conversation
CI Results: |
ui/app/services/version.js
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In another PR we can decide if we want to change this getter from hasSecretsSync
to licenseHasSecretsSync
to clarify between HVD vs managed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
love that idea!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I made the service's getters model params that I pass down to the component. In my mind it read clearer than calling a service each time these are used. However, I understand that one of the benefits of services are they can be called anywhere from within the app. Again, just a preference on my end, but I could be persuaded to not add them as model params and instead call from their services in the templates.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
glad you brought this up! i usually prefer to call service getters directly since the state they contain is (generally) global to the application. this way we get the most up-to-date state straight from its source, and have less opportunity for it to become out of date during the passing-down process. in this particular situation i know we don't need to worry about that happening though since we're only fetching flags once at a very high level and that data is unlikely (or impossible) to change.
the way you have it set up here is easier to test though, since we can easily use @licenseHasSecretsSync=true
without needing to stub the services that back that up. that's pretty nice. i also see that we're passing properties from the version
service down from the route to child components too, and that consistency is great. so, i think your approach works just fine as long as we don't anticipate flags or version getters changing frequently! 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To this point, it's probably a good idea for an acceptance test to cover activating the feature and ensuring the reliant getters update as expected
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removed wrapping conditional that would hide the nav item if it was a managed cluster.
@@ -70,7 +70,7 @@ export const PAGE = { | |||
action: (name) => `[data-test-overview-table-action="${name}"]`, | |||
}, | |||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
renamed this to match the other badgeText used on the sync-header component.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looking great so far! glad to see the implementation of this isn't as complicated as we were worried it might be. 🙌
const isManaged = this.flags.isManaged; | ||
const onLicense = this.version.hasSecretsSync; | ||
const isEnterprise = this.version.isEnterprise; | ||
|
||
get syncBadge() { | ||
if (this.version.isCommunity) return 'Enterprise'; | ||
if (!this.version.hasSecretsSync) return 'Premium'; | ||
if (isManaged) return 'Plus'; | ||
if (isEnterprise && !onLicense) return 'Premium'; | ||
if (!isEnterprise) 'Enterprise'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is great for readability. nice!
ui/app/services/flags.ts
Outdated
@@ -38,7 +38,10 @@ export default class flagsService extends Service { | |||
return null; | |||
} | |||
|
|||
// TODO getter will be used in the upcoming persona service | |||
get isManaged() { | |||
return this.managedNamespaceRoot !== null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
to futureproof this code i'd recommend swapping the logic here i.e.
get isManaged(): boolean {
return this.flags && this.flags.includes(FLAGS.vaultCloudNamespace);
}
get managedNamespaceRoot(): string | null {
return this.isManaged ? 'admin' : null;
}
^ this makes isManaged
more about checking the thing that actually determines if the cluster is managed or not, while managedNamespaceRoot
can stay focused on answering "what is the name of the root
namespace for this managed cluster?"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
smart!
ui/app/services/version.js
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
love that idea!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
glad you brought this up! i usually prefer to call service getters directly since the state they contain is (generally) global to the application. this way we get the most up-to-date state straight from its source, and have less opportunity for it to become out of date during the passing-down process. in this particular situation i know we don't need to worry about that happening though since we're only fetching flags once at a very high level and that data is unlikely (or impossible) to change.
the way you have it set up here is easier to test though, since we can easily use @licenseHasSecretsSync=true
without needing to stub the services that back that up. that's pretty nice. i also see that we're passing properties from the version
service down from the route to child components too, and that consistency is great. so, i think your approach works just fine as long as we don't anticipate flags or version getters changing frequently! 👍
const isManaged = this.flags.isManaged; | ||
const onLicense = this.version.hasSecretsSync; | ||
const isEnterprise = this.version.isEnterprise; | ||
|
||
if (isManaged) return 'Plus feature'; | ||
if (isEnterprise && !onLicense) return 'Premium feature'; | ||
if (!isEnterprise) 'Enterprise feature'; | ||
// no badge for Enterprise clusters with Secrets Sync on their license--the only remaining option. | ||
return undefined; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
totally optional, but if this logic is basically the same as that in components/sidebar/nav/cluster.js
it'd be nice to wrap that little badge in a component OR wrap the badge text in a helper used in both places. again, optional, since copying and pasting this in 2 places isn't so bad.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was on the fence about this, and decided to just copy paste the logic. If it was a unique function we could use elsewhere I'd for sure opt for a helper, but this is very sync only.
Build Results: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great work! These conditionals read clearly and I think this was the right decision on how to handle this logic flow! Just a couple minor comments/suggestions
ui/app/services/flags.ts
Outdated
} | ||
return null; | ||
get isManaged(): boolean { | ||
return this.flags && this.flags.includes(FLAGS.vaultCloudNamespace); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this.flags
will always be truthy because the default value is an empty array, right? So I think we can remove the first part of the conditional here. 🤔 If we're concerned that the value passed to setFeatureFlags
is undefined then I wonder if a more long-term solution is instead adding a conditional check above so that we can rely on this.flags
always being an array☝️
setFeatureFlags(flags: string[]) {
this.flags = flags ? flags : [];
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch. However, I did some additional moving to make the pattern more consistent and clear in the service how we were setting the tracked property flags (which I now call featureFlags
). You'll see I moved the one await fetch call from the application route to this service. Curious if this reads better.
return this.flags && this.flags.includes(FLAGS.vaultCloudNamespace); | ||
} | ||
|
||
get managedNamespaceRoot(): string | null { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
out of curiosity - is declaring the return here : string | null
necessary? It seems like typescript should be able to infer the return of this getter since it's fairly straight forward
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@noelledaley I'm going to punt this question to you. I probably have the least amount of typescript knowledge on the team. Do you know if this is just a best practice to keep here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good question! whether or not to explicitly include the return type is somewhat of a stylistic choice, and depends how defensive we want our code to be. 🙂
examples of why you might include the return type:
- makes the return type more obvious (i.e., no need to hover over the function in VSCode or skim through the code itself)
- enforces type safety. if we later refactor this function to return
undefined
instead ofnull
, for example, TS will cause an error and we'll have to decide whether the function should returnstring | undefined | null
or juststring | null
. it helps us make more intentional decisions. - removing the need to hover to find the return type is good for developer accessibility. check out this tremendously awesome article by @ashleemboyer for more.
why you might not include it:
- you're fine fine hovering/reading the code to figure out the return type
- you trust TypeScript's ability to infer the correct type
personally, thus far i haven't included return types in functions this small/simple, but as you can see there are benefits to doing so. so, it's up to you!🌟
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you so much Noelle for the context/conversation. I'll leave in this case per personal preference.
const isManaged = this.flags.isManaged; | ||
const onLicense = this.version.hasSecretsSync; | ||
const isEnterprise = this.version.isEnterprise; | ||
|
||
if (isManaged) return 'Plus feature'; | ||
if (isEnterprise && !onLicense) return 'Premium feature'; | ||
if (!isEnterprise) return 'Enterprise feature'; | ||
// no badge for Enterprise clusters with Secrets Sync on their license--the only remaining option. | ||
return undefined; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comments here as the get badgeText()
above
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To this point, it's probably a good idea for an acceptance test to cover activating the feature and ensuring the reliant getters update as expected
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
coupla test changes here, then all good!
|
||
test('it should render CTA copy and action when feature exists on enterprise license and is activated', async function (assert) { | ||
await render(hbs`<Secrets::LandingCta @isActivated={{true}} @hasSecretsSync={{true}} /> `, { | ||
test('it should render CTA copy and action feature is activated', async function (assert) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
test('it should render CTA copy and action feature is activated', async function (assert) { | |
test('it should render CTA copy and action if feature is activated', async function (assert) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the catch!
this.isActivated = true; | ||
this.licenseHasSecretsSync = true; | ||
this.isManged = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this.isManged = false; | |
this.isManaged = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it looks like we also need to add tests for when isManaged
is true
or licenseHasSecretsSync
is false
, right? just want to make sure we're accounting for all possible situations that this conditional checks for 👀
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, yes. Let me add those tests in.
const store = this.owner.lookup('service:store'); | ||
this.destinations = await store.query('sync/destination', {}); | ||
this.destinations = await this.store.query('sync/destination', {}); | ||
// component test so set values that would normally be calculated on the route model from the services. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i don't think this comment is necessary since stubbing properties like this is standard for component tests 👍
…um feature so cta message appplies to both managed and non-managed clusters
return this.isManaged ? 'admin' : null; | ||
} | ||
|
||
getFeatureFlags = keepLatestTask(async () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: I moved the fetch from the beforeModel
hook in the application route to this concurrency task. It follows the pattern of getActivatedFeatures
. However, there is a small difference in my pattern and I wanted to point that out. Compare the try/catch here to the previous call:
// previous call
const result = await fetch('/v1/sys/internal/ui/feature-flags', {
method: 'GET',
});
if (result.status === 200) {
const body = await result.json();
const flags = body.feature_flags || [];
this.flagsService.setFeatureFlags(flags);
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i considered suggesting this change earlier so i'm glad you brought this up. i think it keeps the service's scope nicely organized. i like that this ensures we don't have to manually call this.flags.setFeatureFlags
outside of the context of the service. 😀
setFeatureFlags(flags: string[]) { | ||
this.flags = flags; | ||
} | ||
@tracked featureFlags: string[] = []; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
another great change for readability, love it 👏
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is looking great! the only blocking item i see left is to fix the test here 🎉
return this.isManaged ? 'admin' : null; | ||
} | ||
|
||
getFeatureFlags = keepLatestTask(async () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i considered suggesting this change earlier so i'm glad you brought this up. i think it keeps the service's scope nicely organized. i like that this ensures we don't have to manually call this.flags.setFeatureFlags
outside of the context of the service. 😀
@@ -194,8 +193,8 @@ module('Acceptance | sync | overview', function (hooks) { | |||
this.server.post('/sys/activation-flags/secrets-sync/activate', (_, req) => { | |||
assert.strictEqual( | |||
req.requestHeaders['X-Vault-Namespace'], | |||
'admin', | |||
'Request is made to admin namespace' | |||
undefined, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i am fairly sure that this request is supposed to be made to the admin
namespace for HVD clusters since HVD users only have admin
access (and not root level).
to resolve that, you'll need to add the namespace to the request here:
.ajax('/v1/sys/activation-flags/secrets-sync/activate', 'POST', { namespace: null }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, good catch. Yes this is the POST on activate that we do need to add the namespace. Got myself mixed up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
well done Angel! this looks fantastic.
73439b9
into
ui/VAULT-25468/enable-secrets-sync-hvd
* Allow Managed clusters to see Secrets Sync Overview and Sidebar nav (#26649) * update badge text and allow hvd on secrets sync views * update logic in Secrets Sync overview and cta for hvd. * spacing * rearrange based on pr feedback * fix return on badgeText and cluster nav test * fix landing cta tests * update test to reflect new changes * moved call to feature-flags from application route to the service to match patterns * add managed test coverage on overview component test and remove premium feature so cta message appplies to both managed and non-managed clusters * missed service name and unskip admin test * clean up * fix tests * flags test fix * Rename isManaged and managedNamespaceRoot (#26697) * renames * lowercase HVD to match * missed some * test failure * [Secrets Sync] enable access to Sync clients page for HVD clusters (#26713) * feat: split client counts navbar into separate component * acceptance/clients/counts/overview-test: remove tests now covered by int tests * clients counts route: rename isSecretsSyncActivated to showSecretsSync * sync clients page: show unactivated state unless sync client history or feature is activated * client counts navbar: show sync tab only if client history or is /able to be/ activated * clients overview page: only show sync charts if activated * fix: rename isManaged to isHvd * acceptance/counts/overview-test: add HVD tests * acceptance/counts/overview-test: clean up unused cruft * aceptance/clients/counts/overview-test: ensure we dont get false negatives * chore: move Clients::Error to Clients::Counts::Error * chore: calculate showSecretSync in page component instead of route * chore: add copyright headers * acceptance/clients/counts/overview-test: stub activated flags to fix test * [Secrets sync] update sync test selectors (#26824) * acceptance/clients/counts/overview-test: use imported test selectors * general-selectors: add missing emptyStateSubtitle property * acceptance/clients/counts/sync: nest tests in top level module for easier test runs * Add permissions check to show/hide activate button (#26840) * add permissions check to flags service and consume in overview template * add back missing refresh * fix test failures * add test coverage * clean up * address flaky test * grr * address test failures * add changelog * try to fix test failure only on gh * fix fetch to match previous implementation of feature-flags * fix failing test * update comment --------- Co-authored-by: Noelle Daley <noelledaley@users.noreply.github.com> Co-authored-by: clairebontempo@gmail.com <clairebontempo@gmail.com>
Note: merging to sidebranch until backend changes are merged. Final screenshots of all views will be added to sidebranch PR.
Description