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 <gshanmug@redhat.com>
  • Loading branch information
GowthamShanmugam committed Oct 1, 2024
1 parent 59dec8a commit 835cc2f
Show file tree
Hide file tree
Showing 22 changed files with 646 additions and 63 deletions.
18 changes: 14 additions & 4 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 @@ -1097,6 +1093,16 @@
"NamespaceStore details": "NamespaceStore details",
"Target Blob Container": "Target Blob Container",
"Num Volumes": "Num Volumes",
"Storage endpoint": "Storage endpoint",
"Create on": "Create on",
"No buckets found": "No buckets found",
"Create bucket": "Create bucket",
"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.",
"Browse, upload, and manage objects in buckets.<1></1>": "Browse, upload, and manage objects in buckets.<1></1>",
"Help materials": "Help materials",
"MCG": "MCG",
"Search a bucket by name": "Search a bucket by name",
"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 @@ -1331,6 +1337,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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@
"sass-loader": "^13.0.2",
"semver": "6.x",
"style-loader": "^0.23.1",
"swr": "2.2.5",
"thread-loader": "3.0.4",
"ts-loader": "^9.4.1",
"ts-node": "5.0.1",
Expand Down
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,241 @@
import * as React from 'react';
import { odfDocLinks } from '@odf/shared/constants';
import EmptyPage from '@odf/shared/empty-state-page/empty-page';
import {
DOC_VERSION as odfDocVersion,
useUserSettingsLocalStorage,
} from '@odf/shared/hooks';
import { PaginatedListPage } from '@odf/shared/list-page';
import { RowComponentType } from '@odf/shared/table';
import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook';
import { sortByFavorite, sortRows, ViewDocumentation } from '@odf/shared/utils';
import {
Timestamp,
useListPageFilter,
} from '@openshift-console/dynamic-plugin-sdk';
import { TFunction, Trans } from 'react-i18next';
import { Link, useNavigate } from 'react-router-dom-v5-compat';
import useSWR from 'swr';
import { Bullseye, Button, ButtonVariant, Label } from '@patternfly/react-core';
import { SyncAltIcon } from '@patternfly/react-icons';
import { ActionsColumn, Td, Tr } from '@patternfly/react-table';
import {
BUCKET_BOOKMARKS_USER_SETTINGS_KEY,
LIST_BUCKET,
} from '../../../constants';
import { S3BucketKind } from '../../../types';
import { convertBucketDataToCrFormat } from '../../utils/s3-browser';
import { NoobaaS3Context, NoobaaS3Provider } from '../noobaa-context';

const CREATE_BUCKET_PAGE_PATH = '/odf/object-storage/create-bucket';
const BUCKET_DETAILS_PAGE_BASE_PATH = '/odf/object-storage/buckets';

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

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

// ToDo(Gowtham): Update the sync logic after: https://github.com/red-hat-storage/odf-console/pull/1591
const BucketSyncButton: React.FC = () => {
return (
<div className="pf-v5-u-ml-md pf-v5-u-mt-md">
<Button
data-test="bucket-list-sync-button"
variant={ButtonVariant.plain}
onClick={() => null}
>
<SyncAltIcon />
</Button>
</div>
);
};

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

const NoDataMessage: React.FC = () => {
const { t } = useCustomTranslation();
const navigate = useNavigate();
return (
<EmptyPage
onClick={() => navigate(CREATE_BUCKET_PAGE_PATH)}
buttonText={t('Create bucket')}
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 ListPageHeaderHelper: React.FC = () => {
const { t } = useCustomTranslation();
return (
<Trans t={t}>
Browse, upload, and manage objects in buckets.
<ViewDocumentation
doclink={odfDocLinks(odfDocVersion).S3_BUCKET}
text={t('Help materials')}
/>
</Trans>
);
};

const BucketsTableRow: React.FC<RowComponentType<S3BucketKind>> = ({
row: bucket,
rowIndex,
extraProps,
}) => {
const { t } = useCustomTranslation();
const columnNames = getColumnNames(t);
const { Name: name, CreationDate: creationDate } = bucket.apiResponse;
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_BASE_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={creationDate} />}
</Td>
<Td translate={null} isActionCell>
<ActionsColumn items={[]} translate={null} />
</Td>
</Tr>
);
};

const BucketsListPage: React.FC = () => {
const { t } = useCustomTranslation();
const { noobaaS3 } = React.useContext(NoobaaS3Context);
const { data, error, isLoading } = useSWR(LIST_BUCKET, () =>
noobaaS3.listBuckets()
);
const [buckets, filteredData, onFilterChange] = useListPageFilter(
convertBucketDataToCrFormat(data)
);
const [favorites, setFavorites] = useUserSettingsLocalStorage<string[]>(
BUCKET_BOOKMARKS_USER_SETTINGS_KEY,
true,
[]
);

const isLoadedWOAnyError = !isLoading && !error;
const noData = !isLoadedWOAnyError || !buckets.length;

return (
<>
<PaginatedListPage
filteredData={filteredData}
CreateButton={BucketSyncButton}
noData={noData}
listPageHeaderProps={{
title: t('Buckets'),
}}
ListPageHeaderHelper={ListPageHeaderHelper}
listPageCreateLinkProps={{
to: CREATE_BUCKET_PAGE_PATH,
}}
createButtonText={t('Create bucket')}
listPageFilterProps={{
data: data,
loaded: !isLoading,
hideLabelFilter: true,
nameFilterPlaceholder: t('Search a bucket by name'),
onFilterChange: onFilterChange,
}}
composableTableProps={{
columns: getHeaderColumns(t, favorites),
RowComponent: BucketsTableRow,
emptyRowMessage: EmptyRowMessage,
unfilteredData: buckets as [],
noDataMsg: NoDataMessage,
loaded: !isLoading,
loadError: error,
isFavorites: true,
isCompact: true,
extraProps: { favorites, setFavorites },
}}
/>
</>
);
};

const BucketsListPage_: React.FC = () => {
return (
<NoobaaS3Provider loading={false}>
<BucketsListPage />
</NoobaaS3Provider>
);
};

type RowExtraPropsType = {
favorites: string[];
setFavorites: React.Dispatch<React.SetStateAction<string[]>>;
};

export default BucketsListPage_;
10 changes: 10 additions & 0 deletions packages/odf/components/utils/s3-browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ListBucketsCommandOutput } from '@aws-sdk/client-s3';
import { S3BucketKind } from '../../types';

export const convertBucketDataToCrFormat = (
listBucketsCommandOutput: ListBucketsCommandOutput
): S3BucketKind[] =>
listBucketsCommandOutput?.Buckets.map((bucket) => ({
metadata: { name: bucket.Name },
apiResponse: bucket,
})) || [];
6 changes: 6 additions & 0 deletions packages/odf/constants/s3-browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,9 @@ export const NOOBAA_ADMIN_SECRET = 'noobaa-admin';
export const NOOBAA_S3_ROUTE = 's3';
export const NOOBAA_ACCESS_KEY_ID = 'AWS_ACCESS_KEY_ID';
export const NOOBAA_SECRET_ACCESS_KEY = 'AWS_SECRET_ACCESS_KEY';

// key to be used by SWR for caching particular API call
export const LIST_BUCKET = 'LIST_BUCKET_CACHE_KEY';

// Bookmarking / favorites
export const BUCKET_BOOKMARKS_USER_SETTINGS_KEY = 'bucket-bookmarks';
6 changes: 6 additions & 0 deletions packages/odf/types/mcg.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Bucket } from '@aws-sdk/client-s3';
import { K8sResourceCondition } from '@odf/shared/types';
import { K8sResourceCommon } from '@openshift-console/dynamic-plugin-sdk';
import {
Expand Down Expand Up @@ -98,3 +99,8 @@ export type BucketClassKind = K8sResourceCommon & {
conditions: K8sResourceCondition[];
};
};

// Custom k8s compatible type to reuse shared components
export type S3BucketKind = K8sResourceCommon & {
apiResponse: Bucket;
};
5 changes: 5 additions & 0 deletions packages/shared/src/constants/doc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,8 @@ export const odfDRDocApplyPolicy = (odfDocVersion) =>
`${odfDRDocHome(
odfDocVersion
)}#apply-drpolicy-to-sample-application_manage-dr`;

// ToDo(Gowtham): Update doc link
export const odfDocLinks = (odfDocVersion) => ({
S3_BUCKET: `${odfDRDocHome(odfDocVersion)}#s3-bucket`,
});
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const EmptyPage: React.FC<EmptyPageProps> = (props) => {
return !isLoaded ? (
<div
className="loading-skeleton--table mco-empty-page__skeleton"
aria-label={t('Loading Empty Page')}
aria-label={t('Loading empty page')}
/>
) : (
<EmptyState variant={EmptyStateVariant.lg}>
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/empty-state-page/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as EmptyPage } from './empty-page';
1 change: 1 addition & 0 deletions packages/shared/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export * from './use-fetch-csv';
export * from './useK8sList';
export * from './scheduler';
export * from './use-doc-version';
export * from './useUserSettingsLocalStorage';
Loading

0 comments on commit 835cc2f

Please sign in to comment.