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

refactor: [M3-8064] - Clean up Database Feature Flagging Logic #10435

7 changes: 7 additions & 0 deletions packages/manager/src/GoTo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useIsACLBEnabled } from './features/LoadBalancers/utils';
import { useIsPlacementGroupsEnabled } from './features/PlacementGroups/utils';
import { useAccountManagement } from './hooks/useAccountManagement';
import { useGlobalKeyboardListener } from './hooks/useGlobalKeyboardListener';
import { useIsDatabasesEnabled } from './features/Databases/utilities';

const useStyles = makeStyles()((theme: Theme) => ({
input: {
Expand Down Expand Up @@ -62,6 +63,7 @@ export const GoTo = React.memo(() => {

const { isACLBEnabled } = useIsACLBEnabled();
const { isPlacementGroupsEnabled } = useIsPlacementGroupsEnabled();
const { isDatabasesEnabled } = useIsDatabasesEnabled();
const { goToOpen, setGoToOpen } = useGlobalKeyboardListener();

const onClose = () => {
Expand Down Expand Up @@ -120,6 +122,11 @@ export const GoTo = React.memo(() => {
hide: !isPlacementGroupsEnabled,
href: '/placement-groups',
},
{
display: 'Databases',
hide: !isDatabasesEnabled,
href: '/databases',
},
{
display: 'Domains',
href: '/domains',
Expand Down
34 changes: 9 additions & 25 deletions packages/manager/src/MainContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,20 @@ import {
useNotificationContext,
} from 'src/features/NotificationCenter/NotificationContext';
import { TopMenu } from 'src/features/TopMenu/TopMenu';
import { useAccountManagement } from 'src/hooks/useAccountManagement';
import { useFlags } from 'src/hooks/useFlags';
import { useDatabaseEnginesQuery } from 'src/queries/databases';
import { useMutatePreferences, usePreferences } from 'src/queries/preferences';
import { ManagerPreferences } from 'src/types/ManagerPreferences';
import { isFeatureEnabled } from 'src/utilities/accountCapabilities';

import { ENABLE_MAINTENANCE_MODE } from './constants';
import { complianceUpdateContext } from './context/complianceUpdateContext';
import { switchAccountSessionContext } from './context/switchAccountSessionContext';
import { FlagSet } from './featureFlags';
import { useIsDatabasesEnabled } from './features/Databases/utilities';
import { useIsACLBEnabled } from './features/LoadBalancers/utils';
import { useIsPlacementGroupsEnabled } from './features/PlacementGroups/utils';
import { useGlobalErrors } from './hooks/useGlobalErrors';
import { useAccountSettings } from './queries/account/settings';
import { useProfile } from './queries/profile';

const useStyles = makeStyles()((theme: Theme) => ({
activationWrapper: {
Expand Down Expand Up @@ -201,35 +201,19 @@ export const MainContent = () => {
});

const [menuIsOpen, toggleMenu] = React.useState<boolean>(false);
const {
_isManagedAccount,
account,
accountError,
profile,
} = useAccountManagement();

const { data: profile } = useProfile();
const username = profile?.username || '';

const [bannerDismissed, setBannerDismissed] = React.useState<boolean>(false);

const checkRestrictedUser = !Boolean(flags.databases) && !!accountError;
const {
error: enginesError,
isLoading: enginesLoading,
} = useDatabaseEnginesQuery(checkRestrictedUser);

const showDatabases =
isFeatureEnabled(
'Managed Databases',
Boolean(flags.databases),
account?.capabilities ?? []
) ||
(checkRestrictedUser && !enginesLoading && !enginesError);

const { isDatabasesEnabled } = useIsDatabasesEnabled();
const { isACLBEnabled } = useIsACLBEnabled();
const { isPlacementGroupsEnabled } = useIsPlacementGroupsEnabled();

const defaultRoot = _isManagedAccount ? '/managed' : '/linodes';
const { data: accountSettings } = useAccountSettings();

const defaultRoot = accountSettings?.managed ? '/managed' : '/linodes';

const shouldDisplayMainContentBanner =
!bannerDismissed &&
Expand Down Expand Up @@ -376,7 +360,7 @@ export const MainContent = () => {
<Route component={SearchLanding} path="/search" />
<Route component={EventsLanding} path="/events" />
<Route component={Firewalls} path="/firewalls" />
{showDatabases && (
{isDatabasesEnabled && (
<Route component={Databases} path="/databases" />
)}
{flags.selfServeBetas && (
Expand Down
24 changes: 12 additions & 12 deletions packages/manager/src/components/PrimaryNav/PrimaryNav.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,22 +54,22 @@ describe('PrimaryNav', () => {
expect(getByTestId(queryString).getAttribute('aria-current')).toBe('false');
});

it('should show Databases menu item when feature flag is off but user has Managed Databases', () => {
const { getByTestId } = renderWithTheme(<PrimaryNav {...props} />, {
flags: { databases: false },
queryClient,
it('should show Databases menu item if the user has the account capability', async () => {
const account = accountFactory.build({
capabilities: ["Managed Databases"],
});

expect(getByTestId('menu-item-Databases')).toBeInTheDocument();
});
server.use(
http.get('*/account', () => {
return HttpResponse.json(account);
})
);

it('should show databases menu when feature is on', () => {
const { getByTestId } = renderWithTheme(<PrimaryNav {...props} />, {
flags: { databases: true },
queryClient,
});
const { findByText } = renderWithTheme(<PrimaryNav {...props} />);

const databaseNavItem = await findByText('Databases');

expect(getByTestId('menu-item-Databases')).toBeInTheDocument();
expect(databaseNavItem).toBeVisible();
});

it('should show ACLB if the feature flag is on, but there is not an account capability', async () => {
Expand Down
23 changes: 5 additions & 18 deletions packages/manager/src/components/PrimaryNav/PrimaryNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ import AkamaiLogo from 'src/assets/logo/akamai-logo.svg';
import { BetaChip } from 'src/components/BetaChip/BetaChip';
import { Box } from 'src/components/Box';
import { Divider } from 'src/components/Divider';
import { useIsDatabasesEnabled } from 'src/features/Databases/utilities';
import { useIsACLBEnabled } from 'src/features/LoadBalancers/utils';
import { useIsPlacementGroupsEnabled } from 'src/features/PlacementGroups/utils';
import { useAccountManagement } from 'src/hooks/useAccountManagement';
import { useFlags } from 'src/hooks/useFlags';
import { usePrefetch } from 'src/hooks/usePreFetch';
import { useDatabaseEnginesQuery } from 'src/queries/databases';
import {
useObjectStorageBuckets,
useObjectStorageClusters,
Expand Down Expand Up @@ -98,7 +98,7 @@ export const PrimaryNav = (props: PrimaryNavProps) => {
setEnableMarketplacePrefetch,
] = React.useState(false);

const { _isManagedAccount, account, accountError } = useAccountManagement();
const { _isManagedAccount, account } = useAccountManagement();

const isObjMultiClusterEnabled = isFeatureEnabled(
'Object Storage Access Key Regions',
Expand Down Expand Up @@ -156,22 +156,9 @@ export const PrimaryNav = (props: PrimaryNavProps) => {
const allowMarketplacePrefetch =
!oneClickApps && !oneClickAppsLoading && !oneClickAppsError;

const checkRestrictedUser = !Boolean(flags.databases) && !!accountError;
const {
error: enginesError,
isLoading: enginesLoading,
} = useDatabaseEnginesQuery(checkRestrictedUser);

const showDatabases =
isFeatureEnabled(
'Managed Databases',
Boolean(flags.databases),
account?.capabilities ?? []
) ||
(checkRestrictedUser && !enginesLoading && !enginesError);

const { isACLBEnabled } = useIsACLBEnabled();
const { isPlacementGroupsEnabled } = useIsPlacementGroupsEnabled();
const { isDatabasesEnabled } = useIsDatabasesEnabled();

const prefetchObjectStorage = () => {
if (!enableObjectPrefetch) {
Expand Down Expand Up @@ -261,7 +248,7 @@ export const PrimaryNav = (props: PrimaryNavProps) => {
},
{
display: 'Databases',
hide: !showDatabases,
hide: !isDatabasesEnabled,
href: '/databases',
icon: <Database />,
isBeta: flags.databaseBeta,
Expand Down Expand Up @@ -318,7 +305,7 @@ export const PrimaryNav = (props: PrimaryNavProps) => {
],
// eslint-disable-next-line react-hooks/exhaustive-deps
[
showDatabases,
isDatabasesEnabled,
_isManagedAccount,
allowObjPrefetch,
allowMarketplacePrefetch,
Expand Down
80 changes: 80 additions & 0 deletions packages/manager/src/features/Databases/utilities.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { renderHook, waitFor } from '@testing-library/react';

import { accountFactory } from 'src/factories';
import { makeResourcePage } from 'src/mocks/serverHandlers';
import { HttpResponse, http, server } from 'src/mocks/testServer';
import { wrapWithTheme } from 'src/utilities/testHelpers';

import { useIsDatabasesEnabled } from './utilities';

describe('useIsDatabasesEnabled', () => {
it('should return true for an unrestricted user with the account capability', async () => {
const account = accountFactory.build({
capabilities: ['Managed Databases'],
});

server.use(
http.get('*/v4/account', () => {
return HttpResponse.json(account);
})
);

const { result } = renderHook(() => useIsDatabasesEnabled(), {
wrapper: wrapWithTheme,
});

await waitFor(() => expect(result.current.isDatabasesEnabled).toBe(true));
});

it('should return false for an unrestricted user without the account capability', async () => {
const account = accountFactory.build({
capabilities: [],
});

server.use(
http.get('*/v4/account', () => {
return HttpResponse.json(account);
})
);

const { result } = renderHook(() => useIsDatabasesEnabled(), {
wrapper: wrapWithTheme,
});

await waitFor(() => expect(result.current.isDatabasesEnabled).toBe(false));
});

it('should return true for a restricted user who can not load account but can load database engines', async () => {
server.use(
http.get('*/v4/account', () => {
return HttpResponse.json({}, { status: 403 });
}),
http.get('*/v4beta/databases/engines', () => {
return HttpResponse.json(makeResourcePage([]));
}),
);

const { result } = renderHook(() => useIsDatabasesEnabled(), {
wrapper: wrapWithTheme,
});

await waitFor(() => expect(result.current.isDatabasesEnabled).toBe(true));
});

it('should return false for a restricted user who can not load account and database engines', async () => {
server.use(
http.get('*/v4/account', () => {
return HttpResponse.json({}, { status: 403 });
}),
http.get('*/v4beta/databases/engines', () => {
return HttpResponse.json({}, { status: 404 });
})
);

const { result } = renderHook(() => useIsDatabasesEnabled(), {
wrapper: wrapWithTheme,
});

await waitFor(() => expect(result.current.isDatabasesEnabled).toBe(false));
});
});
39 changes: 39 additions & 0 deletions packages/manager/src/features/Databases/utilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { useAccount } from 'src/queries/account/account';
import { useDatabaseEnginesQuery } from 'src/queries/databases';

/**
* A hook to determine if Databases should be visible to the user.
*
* Because DBaaS is end of sale, we treat is differently than other products.
bnussman-akamai marked this conversation as resolved.
Show resolved Hide resolved
* It should only be visible to customers with the account capability.
*
* For unrestricted users, databases will show when
* The user has the `Managed Databases` account capability.
*
* For users who don't have permission to load /v4/account
* (which is restricted users without acount read access),
bnussman-akamai marked this conversation as resolved.
Show resolved Hide resolved
bnussman-akamai marked this conversation as resolved.
Show resolved Hide resolved
* we must check if they can load Database Engines as a workaround.
* If these users can successfully fetch database engines, we will
* show databases.
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you clarify why we'd want to show dbaas for restricted users with no billing access?

This PR just moves the fetching to this util (so I don't think we're fetching more than before) but it would be nice to clarify cause I don't think that's something we do for other entities?

Copy link
Member Author

Choose a reason for hiding this comment

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

I tried to improve the clarification. In summary, Databases are end of sale so they only should show for users who we know for sure have the account capability. The restricted user case is kind of wacky because a restricted user may or may not have access to GET /v4/account

Added Databases to the GoTo menu βœ…

*/
export const useIsDatabasesEnabled = () => {
const { data: account } = useAccount();

// If the flag is off AND we don't have permission to GET /v4/account,
Copy link
Contributor

Choose a reason for hiding this comment

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

Is specifying whether the flag is off relevant anymore? We could probably omit those words now.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good call, removed βœ…

// we need to try fetching Database engines to know if the user has databases enabled.
const checkRestrictedUser = !account;

const { data: engines } = useDatabaseEnginesQuery(checkRestrictedUser);

if (account) {
return {
isDatabasesEnabled: account.capabilities.includes('Managed Databases'),
};
}

const userCouldLoadDatabaseEngines = engines !== undefined;

return {
isDatabasesEnabled: userCouldLoadDatabaseEngines,
};
};
24 changes: 3 additions & 21 deletions packages/manager/src/features/TopMenu/AddNewMenu/AddNewMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,9 @@ import VolumeIcon from 'src/assets/icons/entityIcons/volume.svg';
import VPCIcon from 'src/assets/icons/entityIcons/vpc.svg';
import { Button } from 'src/components/Button/Button';
import { Divider } from 'src/components/Divider';
import { useIsDatabasesEnabled } from 'src/features/Databases/utilities';
import { useIsACLBEnabled } from 'src/features/LoadBalancers/utils';
import { useIsPlacementGroupsEnabled } from 'src/features/PlacementGroups/utils';
import { useFlags } from 'src/hooks/useFlags';
import { useAccount } from 'src/queries/account/account';
import { useDatabaseEnginesQuery } from 'src/queries/databases';
import { isFeatureEnabled } from 'src/utilities/accountCapabilities';

interface LinkProps {
attr?: { [key: string]: boolean };
Expand All @@ -44,25 +41,10 @@ interface LinkProps {

export const AddNewMenu = () => {
const theme = useTheme();
const { data: account, error: accountError } = useAccount();
const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null);
const flags = useFlags();
const open = Boolean(anchorEl);

const checkRestrictedUser = !Boolean(flags.databases) && !!accountError;
const {
error: enginesError,
isLoading: enginesLoading,
} = useDatabaseEnginesQuery(checkRestrictedUser);

const showDatabases =
isFeatureEnabled(
'Managed Databases',
Boolean(flags.databases),
account?.capabilities ?? []
) ||
(checkRestrictedUser && !enginesLoading && !enginesError);

const { isDatabasesEnabled } = useIsDatabasesEnabled();
const { isACLBEnabled } = useIsACLBEnabled();
const { isPlacementGroupsEnabled } = useIsPlacementGroupsEnabled();

Expand Down Expand Up @@ -129,7 +111,7 @@ export const AddNewMenu = () => {
{
description: 'High-performance managed database clusters',
entity: 'Database',
hide: !showDatabases,
hide: !isDatabasesEnabled,
icon: DatabaseIcon,
link: '/databases/create',
},
Expand Down