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

upcoming: [M3-8378] - Add Bucket Management Properties Tab for Object Storage Gen2 #10795

Closed
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Add bucket management Properties Tab for object storage gen2 ([#10795](https://github.com/linode/manager/pull/10795))
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { styled } from '@mui/material/styles';

import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel';
import { Paper } from 'src/components/Paper';
import { Typography } from 'src/components/Typography';

export const StyledText = styled(Typography, {
label: 'StyledText',
})(({ theme }) => ({
lineHeight: 0.5,
paddingLeft: 8,
[theme.breakpoints.down('lg')]: {
marginLeft: 8,
},
[theme.breakpoints.down('sm')]: {
lineHeight: 1,
},
}));

export const StyledRootContainer = styled(Paper, {
label: 'StyledRootContainer',
})(({ theme }) => ({
marginTop: 25,
padding: theme.spacing(3),
}));

export const StyledHelperText = styled(Typography, {
label: 'StyledHelperText',
})(({ theme }) => ({
lineHeight: 1.5,
paddingBottom: theme.spacing(),
paddingTop: theme.spacing(),
}));

export const StyledActionsPanel = styled(ActionsPanel, {
label: 'StyledActionsPanel',
})(() => ({
display: 'flex',
justifyContent: 'right',
}));
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { ObjectStorageEndpointTypes } from '@linode/api-v4';
import * as React from 'react';

import { Link } from 'src/components/Link';
import { Notice } from 'src/components/Notice/Notice';
import { Typography } from 'src/components/Typography';
import { useFlags } from 'src/hooks/useFlags';
import { useAccount } from 'src/queries/account/account';
import { useObjectStorageBuckets } from 'src/queries/object-storage/queries';
import { isFeatureEnabledV2 } from 'src/utilities/accountCapabilities';
import { getQueryParamFromQueryString } from 'src/utilities/queryParams';

import { BucketRateLimitTable } from '../BucketLanding/BucketRateLimitTable';
import { BucketBreadcrumb } from './BucketBreadcrumb';
import {
StyledActionsPanel,
StyledHelperText,
StyledRootContainer,
StyledText,
} from './BucketProperties.styles';

interface Props {
bucketName: string;
clusterId: string;
endpointType?: ObjectStorageEndpointTypes;
}

export const BucketProperties = React.memo((props: Props) => {
const { bucketName, clusterId, endpointType } = props;
const [updateRateLimitLoading] = React.useState(false);
const [selectedRateLimit, setSelectedRateLimit] = React.useState<
null | string
>(null);
const [updateRateLimitSuccess] = React.useState(false);
const [rateLimitError] = React.useState('');
const [updateRateLimitError] = React.useState('');

const prefix = getQueryParamFromQueryString(location.search, 'prefix');
const flags = useFlags();
const { data: account } = useAccount();

const isObjMultiClusterEnabled = isFeatureEnabledV2(
'Object Storage Access Key Regions',
Boolean(flags.objMultiCluster),
account?.capabilities ?? []
);

const { data: buckets } = useObjectStorageBuckets();

const bucket = buckets?.buckets.find((bucket) => {
if (isObjMultiClusterEnabled) {
return bucket.label === bucketName && bucket.region === clusterId;
}
return bucket.label === bucketName && bucket.cluster === clusterId;
});

const handleSubmit = () => {
// TODO: OBJGen2 - Handle Bucket Rate Limit update logic once the endpoint for updating is available.
};

const errorText = rateLimitError || updateRateLimitError;

return (
<>
<BucketBreadcrumb
bucketName={bucketName}
history={history}
prefix={prefix}
/>
<StyledText>{bucket?.hostname}</StyledText>

<StyledRootContainer>
<Typography variant="h2">Bucket Rate Limits</Typography>
{updateRateLimitSuccess ? (
<Notice
text={`Bucket properties updated successfully.`}
variant="success"
/>
) : null}

{errorText ? <Notice text={errorText} variant="error" /> : null}
{/* TODO: OBJGen2 - We need to handle link in upcoming PR */}
<StyledHelperText>
Specifies the maximum Requests Per Second (RPS) for an Endpoint. To
increase it to High, open a <Link to="#">support ticket</Link>.
Understand <Link to="#">bucket rate limits</Link>.
</StyledHelperText>
<BucketRateLimitTable
onRateLimitChange={(selectedLimit: string) => {
setSelectedRateLimit(selectedLimit);
}}
endpointType={endpointType}
selectedRateLimit={selectedRateLimit}
/>
<StyledActionsPanel
primaryButtonProps={{
disabled: !selectedRateLimit,
label: 'Save',
loading: updateRateLimitLoading,
onClick: () => {
handleSubmit();
},
}}
style={{ padding: 0 }}
/>
</StyledRootContainer>
</>
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ const BucketSSL = React.lazy(() =>
default: module.BucketSSL,
}))
);
const BucketProperties = React.lazy(() =>
import('./BucketProperties').then((module) => ({
default: module.BucketProperties,
}))
);

interface MatchProps {
bucketName: string;
Expand Down Expand Up @@ -68,6 +73,10 @@ export const BucketDetailLanding = React.memo((props: Props) => {
routeName: `${props.match.url}/access`,
title: 'Access',
},
{
routeName: `${props.match.url}/properties`,
title: 'Properties',
},
...(!isSSLEnabled
? [
{
Expand Down Expand Up @@ -122,6 +131,13 @@ export const BucketDetailLanding = React.memo((props: Props) => {
/>
</SafeTabPanel>
<SafeTabPanel index={2}>
<BucketProperties
bucketName={bucketName}
clusterId={clusterId}
endpointType={endpointType}
/>
</SafeTabPanel>
<SafeTabPanel index={3}>
<BucketSSL bucketName={bucketName} clusterId={clusterId} />
</SafeTabPanel>
</TabPanels>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import React, { useState } from 'react';

import { Hidden } from 'src/components/Hidden';
import { Radio } from 'src/components/Radio/Radio';
import { Table } from 'src/components/Table';
import { TableBody } from 'src/components/TableBody';
Expand All @@ -17,16 +18,29 @@ import type { ObjectStorageEndpointTypes } from '@linode/api-v4';

interface BucketRateLimitTableProps {
endpointType: ObjectStorageEndpointTypes | undefined;
onRateLimitChange?: (selectedLimit: string) => void;
selectedRateLimit?: null | string;
}

interface RateLimit {
checked: boolean;
id: string;
label: string;
values: string[];
}

const tableHeaders = ['Limits', 'GET', 'PUT', 'LIST', 'DELETE', 'OTHER'];
const tableData = ({ endpointType }: BucketRateLimitTableProps) => [
const tableData = (endpointType: BucketRateLimitTableProps['endpointType']) => [
{
checked: true,
id: '1',
label: 'Basic',
values: ['1000', '000', '000', '000', '000'],
},
{
checked: false,
id: '2',
label: 'High',
values: [
endpointType === 'E3' ? '20000' : '5000',
'000',
Expand All @@ -37,60 +51,68 @@ const tableData = ({ endpointType }: BucketRateLimitTableProps) => [
},
];

export const BucketRateLimitTable = ({
endpointType,
}: BucketRateLimitTableProps) => (
<Table
sx={{
marginBottom: 3,
}}
>
<TableHead>
<TableRow>
{tableHeaders.map((header, index) => {
return (
<TableCell
sx={{
'&&:last-child': {
paddingRight: 2,
},
}}
key={`${index}-${header}`}
>
{header}
</TableCell>
);
})}
</TableRow>
</TableHead>
<TableBody>
{tableData({ endpointType }).map((row, rowIndex) => (
<TableRow key={rowIndex}>
<TableCell>
<Radio
checked={row.checked}
disabled
name="limit-selection"
onChange={() => {}}
value="2"
/>
</TableCell>
{row.values.map((value, index) => {
export const BucketRateLimitTable = (props: BucketRateLimitTableProps) => {
const { endpointType, onRateLimitChange, selectedRateLimit } = props;
const [rateLimits, setRateLimits] = useState<RateLimit[] | null>(null);

React.useEffect(() => {
const data = tableData(endpointType);
setRateLimits(data);

// Set default/initial value
const defaultRateLimit = data.find((rl: any) => rl.checked)?.id || '1';
onRateLimitChange?.(defaultRateLimit);
}, [endpointType]);

return (
<Table sx={{ marginBottom: 3 }}>
<TableHead>
<TableRow>
{tableHeaders.map((header, index) => {
return (
<TableCell
sx={{
'&&:last-child': {
paddingRight: 2,
},
}}
key={`${index}-${value}`}
key={`${index}-${header}`}
>
{value}
{header}
</TableCell>
);
})}
</TableRow>
))}
</TableBody>
</Table>
);
</TableHead>
<TableBody>
{rateLimits?.map((row, rowIndex) => (
<TableRow key={rowIndex}>
<TableCell>
<Radio
checked={selectedRateLimit === row.id}
name="limit-selection"
onChange={() => onRateLimitChange?.(row.id)}
value={row.id}
/>
<Hidden smDown>{row.label}</Hidden>
</TableCell>
{row.values.map((value, index) => {
return (
<TableCell
sx={{
'&&:last-child': {
paddingRight: 2,
},
}}
key={`${index}-${value}`}
>
{value}
</TableCell>
);
})}
</TableRow>
))}
</TableBody>
</Table>
);
};
Loading