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

feat: [M3-6468] Add resource links to Object Storage empty state #9098

Merged
merged 10 commits into from
May 11, 2023
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
## [Unreleased]

### Tech Stories:

- React Query - Linodes - Networking #9046
- React Query - Linodes - Details Header #9099

Expand All @@ -19,6 +20,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
- Resource links to empty state StackScripts landing page #9091
- Resource links to empty state Domains landing page #9092
- Resource links to empty state Images landing page #9095
- Resource links to empty state Object Storage landing page #9092
- Ability download DNS zone file #9075

### Changed:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
mockGetAccessKeys,
} from 'support/intercepts/object-storage';
import { paginateResponse } from 'support/util/paginate';
import { randomLabel, randomNumber } from 'support/util/random';
import { randomLabel, randomNumber, randomString } from 'support/util/random';
import { ui } from 'support/ui';

describe('object storage access keys smoke tests', () => {
Expand All @@ -21,8 +21,9 @@ describe('object storage access keys smoke tests', () => {
*/
it('can create access key - smoke', () => {
const keyLabel = randomLabel();
const accessKey = '1Yx6kbVF35t15k2CmNQJ';
const secretKey = 'bN12cDCBbb90meUgwvb0Tu9KWmNyFqMl2MGK1Ol';
// Mocked key values
const accessKey = randomString(20);
const secretKey = randomString(39);

mockGetAccessKeys(paginateResponse([])).as('getKeys');

Expand Down Expand Up @@ -100,8 +101,9 @@ describe('object storage access keys smoke tests', () => {
it('can revoke access key - smoke', () => {
const keyId = randomNumber(1, 99999);
const keyLabel = randomLabel();
const accessKey = '1Yx6kbVF35t15k2CmNQJ';
const secretKey = 'bN12cDCBbb90meUgwvb0Tu9KWmNyFqMl2MGK1Ol';
// Mocked key values
const accessKey = randomString(20);
const secretKey = randomString(39);

// Mock initial GET request to include an access key.
mockGetAccessKeys(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,7 @@ describe('object storage end-to-end tests', () => {
cy.visitWithLogin('/object-storage');
cy.wait('@getBuckets');

// Click "Create Bucket", fill out bucket creation form in drawer.
ui.entityHeader.find().within(() => {
cy.findByText('Create Bucket').should('be.visible').click();
});
ui.button.findByTitle('Create Bucket').should('be.visible').click();

ui.drawer
.findByTitle('Create Bucket')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ describe('object storage smoke tests', () => {
cy.visitWithLogin('/object-storage');
cy.wait('@getBuckets');

ui.entityHeader.find().within(() => {
ui.landingPageEmptyStateResources.find().within(() => {
cy.findByText('Getting Started Guides').should('be.visible');
cy.findByText('Video Playlist').should('be.visible');
cy.findByText('Create Bucket').should('be.visible').click();
});

Expand Down Expand Up @@ -183,6 +185,6 @@ describe('object storage smoke tests', () => {
});

cy.wait('@deleteBucket');
cy.findByText('Need help getting started?').should('be.visible');
cy.findByText('S3-compatible storage solution').should('be.visible');
});
});
2 changes: 2 additions & 0 deletions packages/manager/cypress/support/ui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as dialog from './dialog';
import * as drawer from './drawer';
import * as entityHeader from './entity-header';
import * as heading from './heading';
import * as landingPageEmptyStateResources from './landing-page-empty-state-resources';
import * as nav from './nav';
import * as select from './select';
import * as tabList from './tab-list';
Expand All @@ -21,6 +22,7 @@ export const ui = {
...drawer,
...entityHeader,
...heading,
...landingPageEmptyStateResources,
...nav,
...select,
...toast,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Landing page empty state resources UI element.
*
* Useful for checking the content of an empty state landing page. (e.g. /domains with no domains)
*/
export const landingPageEmptyStateResources = {
/**
* Finds the entity header and returns the Cypress chainable.
*
* @returns Cypress chainable.
*/
find: (): Cypress.Chainable => {
return cy.get('[data-qa-placeholder-container="resources-section"]');
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export const ResourcesSection = (props: ResourcesSectionProps) => {
return (
<Placeholder
buttonProps={buttonProps}
dataQAPlaceholder="resources-section"
descriptionMaxWidth={descriptionMaxWidth}
icon={icon}
isEntity
Expand Down
66 changes: 35 additions & 31 deletions packages/manager/src/components/LandingHeader/LandingHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
BreadcrumbProps,
} from 'src/components/Breadcrumb/Breadcrumb';
import DocsLink from '../DocsLink';
import Grid from '@mui/material/Grid';
import Grid from '@mui/material/Unstable_Grid2';
import { useTheme, styled } from '@mui/material/styles';

export interface Props {
Expand All @@ -24,6 +24,7 @@ export interface Props {
onButtonKeyPress?: (e: React.KeyboardEvent<HTMLButtonElement>) => void;
onDocsClick?: () => void;
removeCrumbX?: number;
shouldHideDocsAndCreateButtons?: boolean;
title?: string | JSX.Element;
}

Expand All @@ -47,6 +48,7 @@ export const LandingHeader = ({
onButtonKeyPress,
onDocsClick,
removeCrumbX,
shouldHideDocsAndCreateButtons,
title,
}: Props) => {
const theme = useTheme();
Expand All @@ -68,7 +70,7 @@ export const LandingHeader = ({
justifyContent="space-between"
alignItems="center"
>
<Grid item>
<Grid>
<Breadcrumb
data-qa-title
labelTitle={labelTitle}
Expand All @@ -79,36 +81,38 @@ export const LandingHeader = ({
{...breadcrumbProps}
/>
</Grid>
<Grid item>
<Grid alignItems="center" container item justifyContent="flex-end">
{docsLink ? (
<DocsLink
href={docsLink}
label={docsLabel}
analyticsLabel={docsAnalyticsLabel}
onClick={onDocsClick}
/>
) : null}
{renderActions && (
<Actions>
{extraActions}
{onButtonClick ? (
<Button
buttonType="primary"
disabled={disabledCreateButton}
loading={loading}
onClick={onButtonClick}
sx={sxButton}
onKeyPress={onButtonKeyPress}
{...buttonDataAttrs}
>
{createButtonText ?? `Create ${entity}`}
</Button>
) : null}
</Actions>
)}
{!shouldHideDocsAndCreateButtons && (
<Grid>
<Grid alignItems="center" container justifyContent="flex-end">
{docsLink ? (
<DocsLink
href={docsLink}
label={docsLabel}
analyticsLabel={docsAnalyticsLabel}
onClick={onDocsClick}
/>
) : null}
{renderActions && (
<Actions>
{extraActions}
{onButtonClick ? (
<Button
buttonType="primary"
disabled={disabledCreateButton}
loading={loading}
onClick={onButtonClick}
sx={sxButton}
onKeyPress={onButtonKeyPress}
{...buttonDataAttrs}
>
{createButtonText ?? `Create ${entity}`}
</Button>
) : null}
</Actions>
)}
</Grid>
</Grid>
</Grid>
)}
</Grid>
);
};
Expand Down
23 changes: 13 additions & 10 deletions packages/manager/src/components/Placeholder/Placeholder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,30 +156,32 @@ export interface ExtendedButtonProps extends ButtonProps {
}

export interface Props {
icon?: React.ComponentType<any>;
children?: string | React.ReactNode;
title: string;
buttonProps?: ExtendedButtonProps[];
children?: string | React.ReactNode;
className?: string;
dataQAPlaceholder?: string | boolean;
descriptionMaxWidth?: number;
icon?: React.ComponentType<any>;
isEntity?: boolean;
renderAsSecondary?: boolean;
subtitle?: string;
linksSection?: JSX.Element;
renderAsSecondary?: boolean;
showTransferDisplay?: boolean;
subtitle?: string;
title: string;
}

const Placeholder: React.FC<Props> = (props) => {
const {
isEntity,
title,
icon: Icon,
buttonProps,
dataQAPlaceholder,
descriptionMaxWidth,
renderAsSecondary,
subtitle,
icon: Icon,
isEntity,
linksSection,
renderAsSecondary,
showTransferDisplay,
subtitle,
title,
} = props;

const classes = useStyles();
Expand All @@ -196,6 +198,7 @@ const Placeholder: React.FC<Props> = (props) => {
showTransferDisplay && linksSection === undefined,
[classes.rootWithShowTransferDisplay]: showTransferDisplay,
})}
data-qa-placeholder-container={dataQAPlaceholder || true}
>
<div
className={`${classes.iconWrapper} ${isEntity ? classes.entity : ''}`}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,38 @@ import {
ObjectStorageBucket,
ObjectStorageCluster,
} from '@linode/api-v4/lib/object-storage';
import { APIError } from '@linode/api-v4/lib/types';
import classNames from 'classnames';
import * as React from 'react';
import BucketIcon from 'src/assets/icons/entityIcons/bucket.svg';
import { CircleProgress } from 'src/components/CircleProgress';
import { makeStyles } from '@mui/styles';
import { Theme } from '@mui/material/styles';
import Typography from 'src/components/core/Typography';
import { DocumentTitleSegment } from 'src/components/DocumentTitle';
import BucketDetailsDrawer from './BucketDetailsDrawer';
import BucketTable from './BucketTable';
import CancelNotice from '../CancelNotice';
import ErrorState from 'src/components/ErrorState';
import Grid from '@mui/material/Unstable_Grid2';
import { Notice } from 'src/components/Notice/Notice';
import OrderBy from 'src/components/OrderBy';
import Placeholder from 'src/components/Placeholder';
import TransferDisplay from 'src/components/TransferDisplay';
import TypeToConfirmDialog from 'src/components/TypeToConfirmDialog';
import Typography from 'src/components/core/Typography';
import useOpenClose from 'src/hooks/useOpenClose';
import { APIError } from '@linode/api-v4/lib/types';
import { BucketLandingEmptyState } from './BucketLandingEmptyState';
import { CircleProgress } from 'src/components/CircleProgress';
import { DocumentTitleSegment } from 'src/components/DocumentTitle';
import { makeStyles } from '@mui/styles';
import { readableBytes } from 'src/utilities/unitConversions';
import { Theme } from '@mui/material/styles';
import { useProfile } from 'src/queries/profile';
import { useRegionsQuery } from 'src/queries/regions';

import {
sendDeleteBucketEvent,
sendDeleteBucketFailedEvent,
} from 'src/utilities/ga';
import {
BucketError,
useDeleteBucketMutation,
useObjectStorageBuckets,
useObjectStorageClusters,
} from 'src/queries/objectStorage';
import { useRegionsQuery } from 'src/queries/regions';
import {
sendDeleteBucketEvent,
sendDeleteBucketFailedEvent,
sendObjectStorageDocsEvent,
} from 'src/utilities/ga';
import { readableBytes } from 'src/utilities/unitConversions';
import CancelNotice from '../CancelNotice';
import BucketDetailsDrawer from './BucketDetailsDrawer';
import BucketTable from './BucketTable';
import { useProfile } from 'src/queries/profile';
import { useHistory } from 'react-router-dom';

const useStyles = makeStyles((theme: Theme) => ({
copy: {
Expand Down Expand Up @@ -279,46 +276,7 @@ export const BucketLanding = () => {
};

const RenderEmpty = () => {
const classes = useStyles();
const history = useHistory();

return (
<React.Fragment>
<DocumentTitleSegment segment="Buckets" />
<Placeholder
title="Object Storage"
className={classNames({
[classes.empty]: true,
[classes.placeholderAdjustment]: true,
})}
isEntity
icon={BucketIcon}
renderAsSecondary
buttonProps={[
{
onClick: () => history.replace('/object-storage/buckets/create'),
children: 'Create Bucket',
},
]}
showTransferDisplay
>
<Typography variant="subtitle1">Need help getting started?</Typography>
<Typography variant="subtitle1">
<a
onClick={() => sendObjectStorageDocsEvent('Empty state')}
href="https://linode.com/docs/platform/object-storage"
target="_blank"
aria-describedby="external-site"
rel="noopener noreferrer"
className="h-u"
>
Learn more about storage options for your multimedia, archives, and
data backups here.
</a>
</Typography>
</Placeholder>
</React.Fragment>
);
return <BucketLandingEmptyState />;
};

export default BucketLanding;
Expand Down
Loading