Skip to content

Commit

Permalink
Merge branch 'develop' into M3-7772-vmpg-create-integration-tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Joe D'Amore committed May 8, 2024
2 parents e35c5af + af6d38a commit 084ac8c
Show file tree
Hide file tree
Showing 12 changed files with 185 additions and 82 deletions.
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-10423-fixed-1714504053358.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Fixed
---

Prevent Modification of Linode config 'interfaces' Array on No Changes ([#10423](https://github.com/linode/manager/pull/10423))
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Tech Stories
---

Clean up Database feature flagging logic ([#10435](https://github.com/linode/manager/pull/10435))
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-10447-fixed-1715190382947.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Fixed
---

Fix One-Click App test by using Ubuntu 22.04 image ([#10447](https://github.com/linode/manager/pull/10447))
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ describe('OneClick Apps (OCA)', () => {
description: 'Minecraft OCA',
ordinal: 10,
logo_url: 'assets/Minecraft.svg',
images: ['linode/debian11', 'linode/ubuntu20.04'],
images: ['linode/debian11', 'linode/ubuntu22.04'],
deployments_total: 18854,
deployments_active: 412,
is_public: true,
Expand Down Expand Up @@ -161,7 +161,7 @@ describe('OneClick Apps (OCA)', () => {

const firstName = randomLabel();
const password = randomString(16);
const image = 'linode/ubuntu20.04';
const image = 'linode/ubuntu22.04';
const rootPassword = randomString(16);
const region = chooseRegion();
const linodeLabel = randomLabel();
Expand Down
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 @@ -20,19 +20,19 @@ 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 { isFeatureEnabled } from 'src/utilities/accountCapabilities';

import { ENABLE_MAINTENANCE_MODE } from './constants';
import { complianceUpdateContext } from './context/complianceUpdateContext';
import { sessionExpirationContext } from './context/sessionExpirationContext';
import { switchAccountSessionContext } from './context/switchAccountSessionContext';
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 @@ -204,33 +204,17 @@ 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 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';

/**
* this is the case where the user has successfully completed signup
Expand Down Expand Up @@ -358,7 +342,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 it differently than other products.
* 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
* (who are restricted users without account read access),
* we must check if they can load Database Engines as a workaround.
* If these users can successfully fetch database engines, we will
* show databases.
*/
export const useIsDatabasesEnabled = () => {
const { data: account } = useAccount();

// If we don't have permission to GET /v4/account,
// 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,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,18 @@ const interfacesToPayload = (
return [];
}

if (equals(interfaces, defaultInterfaceList)) {
const filteredInterfaces = interfaces.filter(
(thisInterface) => thisInterface.purpose !== 'none'
);

if (
equals(
filteredInterfaces,
defaultInterfaceList.filter(
(thisInterface) => thisInterface.purpose !== 'none'
)
)
) {
// In this case, where eth0 is set to public interface
// and no other interfaces are specified, the API prefers
// to receive an empty array.
Expand All @@ -224,9 +235,7 @@ const interfacesToPayload = (
interfaces[primaryInterfaceIndex].primary = true;
}

return interfaces.filter(
(thisInterface) => thisInterface.purpose !== 'none'
) as Interface[];
return filteredInterfaces as Interface[];
};

const deviceSlots = ['sda', 'sdb', 'sdc', 'sdd', 'sde', 'sdf', 'sdg', 'sdh'];
Expand Down
Loading

0 comments on commit 084ac8c

Please sign in to comment.