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

[Serverless nav] Update footer + project settings cloud links #161971

Merged
merged 29 commits into from
Jul 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
23d765b
Allow cloudLink to be passed in nav node
sebelga Jul 11, 2023
e6d2b30
Add cloud config for new URLs
sebelga Jul 11, 2023
21cf3ca
Update observability nav to include links
sebelga Jul 11, 2023
e47a002
Add urls to cloud start contract
sebelga Jul 11, 2023
9997866
Add missing i18n
sebelga Jul 13, 2023
ecdbf63
Add project settings group to serverless es
sebelga Jul 13, 2023
7c5c20f
Don't force collapsed state by default
sebelga Jul 13, 2023
9e3208c
Improve comment
sebelga Jul 13, 2023
47dc8f5
Update GetApi type to retrieve active state
sebelga Jul 13, 2023
80c564d
Update navs to set active state in details views
sebelga Jul 13, 2023
4f9097c
Fix TS issue
sebelga Jul 14, 2023
fe0bda0
Custom render group with no children with a link
sebelga Jul 14, 2023
e817610
Update default navigation to include Developer tools link
sebelga Jul 14, 2023
6f98ab2
Fix issue when active node is at root level of the tree
sebelga Jul 14, 2023
e394800
Add dev tools link to search and observability
sebelga Jul 14, 2023
113aa67
[CI] Auto-commit changed files from 'node scripts/lint_ts_projects --…
kibanamachine Jul 14, 2023
fda487c
Update storybook to render cloud links
sebelga Jul 14, 2023
e31f70c
Add jest test for cloud links
sebelga Jul 14, 2023
a3df9fb
Merge branch 'project-settings-cloud-links' of github.com:sebelga/kib…
sebelga Jul 14, 2023
d91fbea
Remove old dev tools menu
sebelga Jul 15, 2023
b859014
Merge remote-tracking branch 'upstream/main' into project-settings-cl…
sebelga Jul 17, 2023
b900e98
Add config to test
sebelga Jul 17, 2023
98253a4
Add links in serverless.yml
sebelga Jul 17, 2023
b1c9d44
Merge remote-tracking branch 'upstream/main' into project-settings-cl…
sebelga Jul 17, 2023
247d057
Remove trailing slash
sebelga Jul 17, 2023
5cbed96
Merge remote-tracking branch 'upstream/main' into project-settings-cl…
sebelga Jul 18, 2023
f8a1af9
Revert change to serverless search nav
sebelga Jul 18, 2023
40b9c04
Revert change to obs service type
sebelga Jul 18, 2023
de41cd2
Remove static cloud link
sebelga Jul 18, 2023
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
3 changes: 0 additions & 3 deletions config/serverless.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@ xpack.fleet.internal.activeAgentsSoftLimit: 25000

# Cloud links
xpack.cloud.base_url: "https://cloud.elastic.co"
xpack.cloud.profile_url: "/user/settings"
xpack.cloud.billing_url: "/billing"
xpack.cloud.organization_url: "/account"

# Enable ZDT migration algorithm
migrations.algorithm: zdt
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,8 @@ export class ProjectNavigationService {
const activeNodes = findActiveNodes(
currentPathname,
this.projectNavigationNavTreeFlattened,
location
location,
this.http?.basePath.prepend
);

// Each time we call findActiveNodes() we create a new array of activeNodes. As this array is used
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,29 @@ describe('findActiveNodes', () => {
]);
});

test('should find active node at the root', () => {
const flattendNavTree: Record<string, ChromeProjectNavigationNode> = {
'[0]': {
id: 'root',
title: 'Root',
deepLink: getDeepLink('root', `root`),
path: ['root'],
},
};

expect(findActiveNodes(`/foo/root`, flattendNavTree)).toEqual([
[
{
id: 'root',
title: 'Root',
isActive: true,
deepLink: getDeepLink('root', `root`),
path: ['root'],
},
],
]);
});

test('should match the longest matching node', () => {
const flattendNavTree: Record<string, ChromeProjectNavigationNode> = {
'[0]': {
Expand Down Expand Up @@ -345,7 +368,7 @@ describe('findActiveNodes', () => {
id: 'item1',
title: 'Item 1',
path: ['root', 'item1'],
getIsActive: (loc) => loc.pathname.startsWith('/foo'), // Should match
getIsActive: ({ location }) => location.pathname.startsWith('/foo'), // Should match
},
'[0][2]': {
id: 'item2',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,20 +73,25 @@ function serializeDeeplinkUrl(url?: string) {
* @param key The key to extract parent paths from
* @returns An array of parent paths
*/
function extractParentPaths(key: string) {
function extractParentPaths(key: string, navTree: Record<string, ChromeProjectNavigationNode>) {
// Split the string on every '][' to get an array of values without the brackets.
const arr = key.split('][');
if (arr.length === 1) {
return arr;
}
// Add the brackets back in for the first and last elements, and all elements in between.
arr[0] = `${arr[0]}]`;
arr[arr.length - 1] = `[${arr[arr.length - 1]}`;
for (let i = 1; i < arr.length - 1; i++) {
arr[i] = `[${arr[i]}]`;
}

return arr.reduce<string[]>((acc, currentValue, currentIndex) => {
acc.push(arr.slice(0, currentIndex + 1).join(''));
return acc;
}, []);
return arr
.reduce<string[]>((acc, currentValue, currentIndex) => {
acc.push(arr.slice(0, currentIndex + 1).join(''));
return acc;
}, [])
.filter((k) => Boolean(navTree[k]));
}

/**
Expand All @@ -101,7 +106,8 @@ function extractParentPaths(key: string) {
export const findActiveNodes = (
currentPathname: string,
navTree: Record<string, ChromeProjectNavigationNode>,
location?: Location
location?: Location,
prepend: (path: string) => string = (path) => path
): ChromeProjectNavigationNode[][] => {
const activeNodes: ChromeProjectNavigationNode[][] = [];
const matches: string[][] = [];
Expand All @@ -113,9 +119,9 @@ export const findActiveNodes = (

Object.entries(navTree).forEach(([key, node]) => {
if (node.getIsActive && location) {
const isActive = node.getIsActive(location);
const isActive = node.getIsActive({ pathNameSerialized: currentPathname, location, prepend });
if (isActive) {
const keysWithParents = extractParentPaths(key);
const keysWithParents = extractParentPaths(key, navTree);
activeNodes.push(keysWithParents.map(activeNodeFromKey));
}
return;
Expand All @@ -139,7 +145,7 @@ export const findActiveNodes = (
if (matches.length > 0) {
const longestMatch = matches[matches.length - 1];
longestMatch.forEach((key) => {
const keysWithParents = extractParentPaths(key);
const keysWithParents = extractParentPaths(key, navTree);
activeNodes.push(keysWithParents.map(activeNodeFromKey));
});
}
Expand Down
1 change: 1 addition & 0 deletions packages/core/chrome/core-chrome-browser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export type {
ChromeUserBanner,
ChromeProjectNavigation,
ChromeProjectNavigationNode,
CloudLinkId,
SideNavCompProps,
SideNavComponent,
ChromeProjectBreadcrumb,
Expand Down
1 change: 1 addition & 0 deletions packages/core/chrome/core-chrome-browser/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export type {
ChromeProjectNavigationNode,
AppDeepLinkId,
AppId,
CloudLinkId,
SideNavCompProps,
SideNavComponent,
ChromeSetProjectBreadcrumbsParams,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,18 @@ export type AppDeepLinkId =
| SearchLink
| ObservabilityLink;

/** @public */
export type CloudLinkId = 'userAndRoles' | 'performance' | 'billingAndSub';

export type GetIsActiveFn = (params: {
/** The current path name including the basePath + hash value but **without** any query params */
pathNameSerialized: string;
/** The history Location */
location: Location;
/** Utiliy function to prepend a path with the basePath */
prepend: (path: string) => string;
}) => boolean;

/** @public */
export interface ChromeProjectNavigationNode {
/** Optional id, if not passed a "link" must be provided. */
Expand All @@ -69,7 +81,7 @@ export interface ChromeProjectNavigationNode {
/**
* Optional function to get the active state. This function is called whenever the location changes.
*/
getIsActive?: (location: Location) => boolean;
getIsActive?: GetIsActiveFn;

/**
* Optional flag to indicate if the breadcrumb should be hidden when this node is active.
Expand Down Expand Up @@ -123,6 +135,8 @@ export interface NodeDefinition<
title?: string;
/** App id or deeplink id */
link?: LinkId;
/** Cloud link id */
cloudLink?: CloudLinkId;
/** Optional icon for the navigation node. Note: not all navigation depth will render the icon */
icon?: string;
/** Optional children of the navigation node */
Expand All @@ -134,7 +148,7 @@ export interface NodeDefinition<
/**
* Optional function to get the active state. This function is called whenever the location changes.
*/
getIsActive?: (location: Location) => boolean;
getIsActive?: GetIsActiveFn;

/**
* Optional flag to indicate if the breadcrumb should be hidden when this node is active.
Expand Down
14 changes: 14 additions & 0 deletions packages/shared-ux/chrome/navigation/mocks/src/jest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,19 @@ export const getServicesMock = ({
navigateToUrl,
onProjectNavigationChange: jest.fn(),
activeNodes$: of(activeNodes),
cloudLinks: {
billingAndSub: {
title: 'Mock Billing & Subscriptions',
href: 'https://cloud.elastic.co/account/billing',
},
performance: {
title: 'Mock Performance',
href: 'https://cloud.elastic.co/deployments/123456789/performance',
},
userAndRoles: {
title: 'Mock Users & Roles',
href: 'https://cloud.elastic.co/deployments/123456789/security/users',
},
},
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const allNavLinks: AppDeepLinkId[] = [
'discover',
'fleet',
'integrations',
'management',
'management:api_keys',
'management:cases',
'management:cross_cluster_replication',
Expand Down
14 changes: 14 additions & 0 deletions packages/shared-ux/chrome/navigation/mocks/src/storybook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,20 @@ export class StorybookMock extends AbstractStorybookMock<{}, NavigationServices>
navLinks$: params.navLinks$ ?? new BehaviorSubject([]),
onProjectNavigationChange: params.onProjectNavigationChange ?? (() => undefined),
activeNodes$: new BehaviorSubject([]),
cloudLinks: {
billingAndSub: {
title: 'Billing & Subscriptions',
href: 'https://cloud.elastic.co/account/billing',
},
performance: {
title: 'Performance',
href: 'https://cloud.elastic.co/deployments/123456789/performance',
},
userAndRoles: {
title: 'Users & Roles',
href: 'https://cloud.elastic.co/deployments/123456789/security/users',
},
},
};
}

Expand Down
58 changes: 58 additions & 0 deletions packages/shared-ux/chrome/navigation/src/cloud_links.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
import type { CloudLinkId } from '@kbn/core-chrome-browser';
import type { CloudStart } from '@kbn/cloud-plugin/public';

export type CloudLinks = {
[id in CloudLinkId]?: {
title: string;
href: string;
};
};

export const getCloudLinks = (cloud: CloudStart): CloudLinks => {
const { billingUrl, performanceUrl, usersAndRolesUrl } = cloud;

const links: CloudLinks = {};

if (usersAndRolesUrl) {
links.userAndRoles = {
title: i18n.translate(
'sharedUXPackages.chrome.sideNavigation.cloudLinks.usersAndRolesLinkText',
{
defaultMessage: 'Users and roles',
}
),
href: usersAndRolesUrl,
};
}

if (performanceUrl) {
links.performance = {
title: i18n.translate(
'sharedUXPackages.chrome.sideNavigation.cloudLinks.performanceLinkText',
{
defaultMessage: 'Performance',
}
),
href: performanceUrl,
};
}

if (billingUrl) {
links.billingAndSub = {
title: i18n.translate('sharedUXPackages.chrome.sideNavigation.cloudLinks.billingLinkText', {
defaultMessage: 'Billing and subscription',
}),
href: billingUrl,
};
}

return links;
};
8 changes: 6 additions & 2 deletions packages/shared-ux/chrome/navigation/src/services.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
* Side Public License, v 1.
*/

import React, { FC, useContext } from 'react';
import React, { FC, useContext, useMemo } from 'react';
import { NavigationKibanaDependencies, NavigationServices } from '../types';
import { CloudLinks, getCloudLinks } from './cloud_links';

const Context = React.createContext<NavigationServices | null>(null);

Expand All @@ -25,11 +26,13 @@ export const NavigationKibanaProvider: FC<NavigationKibanaDependencies> = ({
children,
...dependencies
}) => {
const { core, serverless } = dependencies;
const { core, serverless, cloud } = dependencies;
const { chrome, http } = core;
const { basePath } = http;
const { navigateToUrl } = core.application;

const cloudLinks: CloudLinks = useMemo(() => (cloud ? getCloudLinks(cloud) : {}), [cloud]);

const value: NavigationServices = {
basePath,
recentlyAccessed$: chrome.recentlyAccessed.get$(),
Expand All @@ -38,6 +41,7 @@ export const NavigationKibanaProvider: FC<NavigationKibanaDependencies> = ({
navIsOpen: true,
onProjectNavigationChange: serverless.setNavigation,
activeNodes$: serverless.getActiveNavigationNodes$(),
cloudLinks,
};

return <Context.Provider value={value}>{children}</Context.Provider>;
Expand Down
Loading