Skip to content

Commit

Permalink
RHSTOR-6195: Bucket - list page
Browse files Browse the repository at this point in the history
Signed-off-by: Gowtham Shanmugasundaram <[email protected]>
  • Loading branch information
GowthamShanmugam committed Oct 3, 2024
1 parent eb8baf6 commit 1201bc3
Show file tree
Hide file tree
Showing 24 changed files with 785 additions and 139 deletions.
20 changes: 15 additions & 5 deletions locales/en/plugin__odf-console.json
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,6 @@
"Clusters": "Clusters",
"Connected applications": "Connected applications",
"Cannot delete while connected to an application.": "Cannot delete while connected to an application.",
"Loading Empty Page": "Loading Empty Page",
"You are not authorized to complete this action. See your cluster administrator for role-based access control information.": "You are not authorized to complete this action. See your cluster administrator for role-based access control information.",
"Not Authorized": "Not Authorized",
"Empty Page": "Empty Page",
"Clean up application resources on current primary cluster {{ failoverCluster }} to start the relocation.": "Clean up application resources on current primary cluster {{ failoverCluster }} to start the relocation.",
"Cleanup Pending": "Cleanup Pending",
"Relocating to cluster {{ preferredCluster }}": "Relocating to cluster {{ preferredCluster }}",
Expand Down Expand Up @@ -1110,6 +1106,17 @@
"MCG": "MCG",
"Object path: ": "Object path: ",
"Copy to share": "Copy to share",
"Erase the contents of your bucket": "Erase the contents of your bucket",
"Storage endpoint": "Storage endpoint",
"Create on": "Create on",
"Owner": "Owner",
"Create and manage your buckets": "Create and manage your buckets",
"Navigate through your buckets effortlessly. View the contents of your S3-managed and Openshift-managed buckets, making it easy to locate and inspect objects.": "Navigate through your buckets effortlessly. View the contents of your S3-managed and Openshift-managed buckets, making it easy to locate and inspect objects.",
"No buckets found": "No buckets found",
"Search a bucket by name": "Search a bucket by name",
"Create bucket": "Create bucket",
"Browse, upload, and manage objects in buckets.<1></1>": "Browse, upload, and manage objects in buckets.<1></1>",
"Help materials": "Help materials",
"Create Bucket": "Create Bucket",
"An object bucket is a cloud storage container that organizes and manages files (objects), allowing users to store, retrieve and control access to data efficiently.": "An object bucket is a cloud storage container that organizes and manages files (objects), allowing users to store, retrieve and control access to data efficiently.",
"Select bucket creation method": "Select bucket creation method",
Expand Down Expand Up @@ -1347,7 +1354,6 @@
"{{count}} annotation_one": "{{count}} annotation",
"{{count}} annotation_other": "{{count}} annotation",
"Created at": "Created at",
"Owner": "Owner",
"No labels": "No labels",
"No owner": "No owner",
"Select input": "Select input",
Expand All @@ -1356,6 +1362,10 @@
"No resources available": "No resources available",
"Select {{resourceLabel}}": "Select {{resourceLabel}}",
"Error Loading": "Error Loading",
"Loading empty page": "Loading empty page",
"You are not authorized to complete this action. See your cluster administrator for role-based access control information.": "You are not authorized to complete this action. See your cluster administrator for role-based access control information.",
"Not Authorized": "Not Authorized",
"Empty Page": "Empty Page",
"Reset": "Reset",
"An error occurred. Please try again.": "An error occurred. Please try again.",
"Error Loading {{label}}: {{message}}": "Error Loading {{label}}: {{message}}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,6 @@ describe('Test drpolicy list page', () => {
test('Empty page loading test', async () => {
testCase = 3;
render(<DRPolicyListPage />);
expect(screen.getByLabelText('Loading Empty Page')).toBeInTheDocument();
expect(screen.getByLabelText('Loading empty page')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as React from 'react';
import { pluralize } from '@odf/core/components/utils';
import EmptyPage from '@odf/shared/empty-state-page/empty-page';
import { useAccessReview } from '@odf/shared/hooks/rbac-hook';
import { Kebab } from '@odf/shared/kebab/kebab';
import { getName } from '@odf/shared/selectors';
Expand Down Expand Up @@ -28,7 +29,6 @@ import {
import { DRPolicyModel } from '../../models';
import { DRPolicyKind } from '../../types';
import { getReplicationType, isDRPolicyValidated } from '../../utils';
import EmptyPage from '../empty-state-page/empty-page';
import { Header, kebabActionItems, tableColumnInfo } from './helper';
import './drpolicy-list-page.scss';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from 'react';
import { DRPolicyKind, DRPlacementControlKind } from '@odf/mco/types';
import { getDRPolicyStatus, parseSyncInterval } from '@odf/mco/utils';
import { formatTime, getLatestDate } from '@odf/shared/details-page/datetime';
import EmptyPage from '@odf/shared/empty-state-page/empty-page';
import { StatusBox } from '@odf/shared/generic/status-box';
import { Labels } from '@odf/shared/labels';
import { ModalBody, ModalFooter } from '@odf/shared/modals/Modal';
Expand Down Expand Up @@ -31,7 +32,6 @@ import {
SYNC_SCHEDULE_DISPLAY_TEXT,
} from '../../../constants';
import { getDRPlacementControlResourceObj } from '../../../hooks';
import EmptyPage from '../../empty-state-page/empty-page';
import {
doNotDeletePVCAnnotationPromises,
unAssignPromises,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
ActionDropdown,
ToggleVariant,
} from '@odf/shared/dropdown/action-dropdown';
import EmptyPage from '@odf/shared/empty-state-page/empty-page';
import { DataUnavailableError } from '@odf/shared/generic/Error';
import { NamespaceModel } from '@odf/shared/models';
import { ResourceNameWIcon } from '@odf/shared/resource-link/resource-link';
Expand Down Expand Up @@ -36,7 +37,6 @@ import {
} from '../../constants';
import { DRPlacementControlModel } from '../../models';
import { DRPlacementControlKind } from '../../types';
import EmptyPage from '../empty-state-page/empty-page';
import { getCurrentActivity } from '../mco-dashboard/disaster-recovery/cluster-app-card/application';
import {
getAlertMessages,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import * as React from 'react';
import {
BUCKET_BOOKMARKS_USER_SETTINGS_KEY,
BUCKET_DETAILS_PAGE_PATH,
} from '@odf/core/constants';
import { BucketCrFormat } from '@odf/core/types';
import { EmptyPage } from '@odf/shared/empty-state-page';
import { useUserSettingsLocalStorage } from '@odf/shared/hooks/useUserSettingsLocalStorage';
import {
ComposableTable,
RowComponentType,
} from '@odf/shared/table/composable-table';
import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook';
import { sortRows } from '@odf/shared/utils';
import { Timestamp } from '@openshift-console/dynamic-plugin-sdk';
import { TFunction, Trans } from 'react-i18next';
import { Link } from 'react-router-dom-v5-compat';
import { Bullseye, Label } from '@patternfly/react-core';
import { UserIcon } from '@patternfly/react-icons';
import { ActionsColumn, IAction, Td, Tr } from '@patternfly/react-table';

const getRowActions = (t: TFunction<string>): IAction[] => [
{
title: (
<>
{t('Empty bucket')}
<p className="text-muted pf-v5-u-font-size-xs">
{t('Erase the contents of your bucket')}
</p>
</>
),
onClick: () => undefined,
},
{
title: t('Delete bucket'),
onClick: () => undefined,
},
];

const getColumnNames = (t: TFunction<string>) => [
'', // favoritable,
t('Name'),
t('Storage endpoint'),
t('Create on'),
t('Owner'),
'', // action kebab
];

const getHeaderColumns = (t: TFunction<string>, favorites: string[]) => {
const columnNames = getColumnNames(t);
return [
{
columnName: columnNames[0],
sortFunction: (a, b, c) => sortRows(a, b, c, 'metadata.name', favorites),
},
{
columnName: columnNames[1],
sortFunction: (a, b, c) => sortRows(a, b, c, 'metadata.name'),
},
{
columnName: columnNames[2],
thProps: {
className: 'pf-v5-u-w-16-on-lg',
},
},
{
columnName: columnNames[3],
sortFunction: (a, b, c) =>
sortRows(a, b, c, 'metadata.creationTimestamp'),
thProps: {
className: 'pf-v5-u-w-16-on-lg',
},
},
{
columnName: columnNames[4],
thProps: {
className: 'pf-v5-u-w-16-on-lg',
},
},
{
columnName: columnNames[5],
},
];
};

const NoBucketMessage: React.FC = () => {
const { t } = useCustomTranslation();
return (
<EmptyPage
ButtonComponent={() => <></>}
title={t('Create and manage your buckets')}
isLoaded
canAccess
>
<Trans t={t}>
Navigate through your buckets effortlessly. View the contents of your
S3-managed and Openshift-managed buckets, making it easy to locate and
inspect objects.
</Trans>
</EmptyPage>
);
};

const EmptyRowMessage: React.FC = () => {
const { t } = useCustomTranslation();
return <Bullseye className="pf-v5-u-mt-xl">{t('No buckets found')}</Bullseye>;
};

const BucketsTableRow: React.FC<RowComponentType<BucketCrFormat>> = ({
row: bucket,
rowIndex,
extraProps,
}) => {
const { t } = useCustomTranslation();
const columnNames = getColumnNames(t);
const {
apiResponse: { owner },
metadata: { name, creationTimestamp },
} = bucket;
const { favorites, setFavorites }: RowExtraPropsType = extraProps;

const onSetFavorite = (key, active) => {
setFavorites((oldFavorites) => [
...oldFavorites.filter((oldFavorite) => oldFavorite !== key),
...(active ? [key] : []),
]);
};

return (
<Tr translate={null} key={rowIndex}>
<Td
translate={null}
favorites={{
isFavorited: favorites.includes(name),
onFavorite: (_event, isFavoriting) =>
onSetFavorite(name, isFavoriting),
rowIndex,
}}
/>
<Td translate={null} dataLabel={columnNames[1]}>
<Link to={`${BUCKET_DETAILS_PAGE_PATH}/${name}`}>{name}</Link>
</Td>
<Td translate={null} dataLabel={columnNames[2]}>
{/* ToDo: Currently we only support MCG, make is configurable once RGW is supported as well */}
<Label color="gold">{t('MCG')}</Label>
</Td>
<Td translate={null} dataLabel={columnNames[3]}>
{<Timestamp timestamp={creationTimestamp} />}
</Td>
<Td translate={null} dataLabel={columnNames[4]}>
<UserIcon /> <span data-test="owner">{owner}</span>
</Td>
<Td translate={null} isActionCell>
<ActionsColumn items={getRowActions(t)} translate={null} />
</Td>
</Tr>
);
};

export const BucketsListTable: React.FC<BucketsListTableProps> = ({
allBuckets,
filteredBuckets,
loaded,
error,
}) => {
const { t } = useCustomTranslation();
const [favorites, setFavorites] = useUserSettingsLocalStorage<string[]>(
BUCKET_BOOKMARKS_USER_SETTINGS_KEY,
true,
[]
);
return (
<ComposableTable
rows={filteredBuckets}
columns={getHeaderColumns(t, favorites)}
RowComponent={BucketsTableRow}
noDataMsg={NoBucketMessage}
emptyRowMessage={EmptyRowMessage}
unfilteredData={allBuckets as []}
loaded={loaded}
loadError={error}
isFavorites={true}
isCompact={true}
extraProps={{ favorites, setFavorites }}
/>
);
};

type BucketsListTableProps = {
allBuckets: BucketCrFormat[];
filteredBuckets: BucketCrFormat[];
loaded: boolean;
error: any;
};

type RowExtraPropsType = {
favorites: string[];
setFavorites: React.Dispatch<React.SetStateAction<string[]>>;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import * as React from 'react';
import { LIST_BUCKET, MAX_BUCKETS } from '@odf/core/constants';
import { BucketCrFormat } from '@odf/core/types';
import { convertBucketDataToCrFormat } from '@odf/core/utils';
import useSWRMutation from 'swr/mutation';
import { NoobaaS3Context } from '../noobaa-context';
import {
ContinuationTokens,
fetchObjects,
Pagination,
} from '../pagination-helper';

export const BucketPagination: React.FC<BucketPaginationProps> = ({
setBucketInfo,
}) => {
const { noobaaS3 } = React.useContext(NoobaaS3Context);
const { data, error, isMutating, trigger } = useSWRMutation(
LIST_BUCKET,
(_url, { arg }: { arg: string }) =>
noobaaS3.listBuckets({
MaxBuckets: MAX_BUCKETS,
...(!!arg && { ContinuationToken: arg }),
})
);

const loadedWOError = !isMutating && !error;
const [continuationTokens, setContinuationTokens] =
React.useState<ContinuationTokens>({
previous: [],
current: '',
next: '',
});

React.useEffect(() => {
setBucketInfo([
convertBucketDataToCrFormat(data),
!isMutating && !error,
error,
]);
}, [data, isMutating, error, setBucketInfo]);

// initial fetch on first mount
React.useEffect(() => {
fetchObjects(setContinuationTokens, trigger, true, undefined);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const onNextClick = async () => {
if (!!continuationTokens.next && loadedWOError)
fetchObjects(
setContinuationTokens,
trigger,
true,
undefined,
continuationTokens.next
);
};

const onPreviousClick = async () => {
if (!!continuationTokens.current && loadedWOError) {
const paginationToken =
continuationTokens.previous[continuationTokens.previous.length - 1];
fetchObjects(
setContinuationTokens,
trigger,
false,
undefined,
paginationToken
);
}
};

return (
<Pagination
disableNext={!continuationTokens.next || !loadedWOError}
disablePrevious={!continuationTokens.current || !loadedWOError}
onNext={onNextClick}
onPrevious={onPreviousClick}
/>
);
};

type BucketPaginationProps = {
setBucketInfo: React.Dispatch<
React.SetStateAction<[BucketCrFormat[], boolean, any]>
>;
};
Loading

0 comments on commit 1201bc3

Please sign in to comment.