Skip to content

Commit

Permalink
RBAC fixes (#1030)
Browse files Browse the repository at this point in the history
* rbac-fixes

* [UI] EVEREST-1803 Monitoring unavailable in db details widget when specific rule is set #1018

Co-authored-by: Percona Platform Robot <[email protected]>

* [UI] EVEREST-1781 Update message for empty state when user has no create permissions (#1035)

* fix: update message for empty state when user has no create permissions

* fix: add specific prop for create permission

* [UI] EVEREST-1800 Fix display of Add DB button, redirect from /new if state if empty (#1036)

* fix: show add button if user has permission for any db engine, redirect /new url if user if not being directed to it by a component

* fix: change check for available engines

* fix: missing namespaces polling (#1044)

---------

Co-authored-by: Percona Platform Robot <[email protected]>
Co-authored-by: Fábio Silva <[email protected]>
  • Loading branch information
3 people authored Jan 31, 2025
1 parent 7f097f8 commit f703832
Show file tree
Hide file tree
Showing 12 changed files with 95 additions and 64 deletions.
41 changes: 12 additions & 29 deletions ui/apps/everest/src/hooks/api/backup-storages/useBackupStorages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ import {
UseMutationOptions,
useQueries,
useQuery,
UseQueryOptions,
UseQueryResult,
} from '@tanstack/react-query';
import {
createBackupStorageFn,
Expand All @@ -32,46 +30,31 @@ import {
GetBackupStoragesPayload,
} from 'shared-types/backupStorages.types';
import { PerconaQueryOptions } from 'shared-types/query.types';
import { useNamespaces } from '../namespaces';

export const BACKUP_STORAGES_QUERY_KEY = 'backupStorages';

export interface BackupStoragesForNamespaceResult {
namespace: string;
queryResult: UseQueryResult<BackupStorage[], unknown>;
}
export type BackupStoragesForNamespaceResult =
PerconaQueryOptions<GetBackupStoragesPayload>;

export const useBackupStorages = (
queriesParams: Array<{
namespace: string;
options?: PerconaQueryOptions<
GetBackupStoragesPayload,
unknown,
BackupStorage[]
>;
}>
) => {
const queries = queriesParams.map<
UseQueryOptions<GetBackupStoragesPayload, unknown, BackupStorage[]>
>(({ namespace, options }) => {
export const useBackupStorages = () => {
const { data: namespaces = [] } = useNamespaces({
refetchInterval: 5 * 1000,
});
const queries = namespaces.map((namespace) => {
return {
queryKey: [BACKUP_STORAGES_QUERY_KEY, namespace],
retry: false,
queryFn: () => getBackupStoragesFn(namespace),
refetchInterval: 5 * 1000,
...options,
};
});

const queryResults = useQueries({ queries });

const results: BackupStoragesForNamespaceResult[] = queryResults.map(
(item, i) => ({
namespace: queriesParams[i].namespace,
queryResult: item,
})
);
const queryResults = useQueries({
queries,
});

return results;
return queryResults;
};

export const useBackupStoragesByNamespace = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
UseQueryResult,
useMutation,
useQueries,
useQuery,
} from '@tanstack/react-query';
import {
getMonitoringInstancesFn,
Expand Down Expand Up @@ -78,6 +79,15 @@ export const useMonitoringInstancesList = (
return results;
};

export const useMonitoringInstancesForNamespace = (namespace: string) => {
return useQuery({
queryKey: [MONITORING_INSTANCES_QUERY_KEY, namespace],
retry: false,
queryFn: () => getMonitoringInstancesFn(namespace),
refetchInterval: 5 * 1000,
});
};

export const useCreateMonitoringInstance = (
options?: UseMutationOptions<
MonitoringInstance,
Expand Down
23 changes: 21 additions & 2 deletions ui/apps/everest/src/pages/common/empty-state/databases/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Box, Divider, Typography } from '@mui/material';
import { Box, Divider, Link, Typography } from '@mui/material';
import { EmptyStateIcon } from '@percona/ui-lib';
import { Messages } from './messages';
import CreateDbButton from 'pages/databases/create-db-button/create-db-button';
Expand All @@ -7,8 +7,10 @@ import { ContactSupportLink } from '../ContactSupportLink';

export const EmptyStateDatabases = ({
showCreationButton,
hasCreatePermission,
}: {
showCreationButton: boolean;
hasCreatePermission: boolean;
}) => {
return (
<>
Expand All @@ -22,7 +24,24 @@ export const EmptyStateDatabases = ({
<EmptyStateIcon w="60px" h="60px" />
<Box sx={centeredContainerStyle}>
<Typography>{Messages.noDbClusters}</Typography>
<Typography> {Messages.createToStart}</Typography>
{hasCreatePermission ? (
<Typography> {Messages.createToStart} </Typography>
) : (
<>
<Typography>{Messages.noPermissions}</Typography>
<Typography>
Click{' '}
<Link
target="_blank"
rel="noopener"
href="https://docs.percona.com/everest/administer/rbac.html"
>
here
</Link>{' '}
to learn how to get permissions.
</Typography>
</>
)}
</Box>
{showCreationButton && <CreateDbButton />}
<Divider sx={{ width: '30%', marginTop: '10px' }} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export const Messages = {
createToStart: 'Create one to get started.',
create: 'Create Database',
contactSupport: 'Contact Percona Support',
noPermissions: 'You lack permission to create a database cluster.',
};
6 changes: 6 additions & 0 deletions ui/apps/everest/src/pages/database-form/database-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,12 @@ export const DatabasePage = () => {
}
}, [defaultValues, isDirty, reset, mode]);

useEffect(() => {
if (!location.state) {
navigate('/');
}
}, []);

return formSubmitted ? (
<ConfirmationScreen />
) : (
Expand Down
17 changes: 13 additions & 4 deletions ui/apps/everest/src/pages/databases/DbClusterView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@
import { Box, Stack } from '@mui/material';
import { Table } from '@percona/ui-lib';
import StatusField from 'components/status-field';
import { useNamespaces } from 'hooks/api/namespaces/useNamespaces';
import {
useDBEnginesForNamespaces,
useNamespaces,
} from 'hooks/api/namespaces/useNamespaces';
import { type MRT_ColumnDef } from 'material-react-table';
import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
Expand Down Expand Up @@ -46,11 +49,14 @@ export const DbClusterView = () => {
);

const navigate = useNavigate();
const { results: dbEngines } = useDBEnginesForNamespaces();
const hasAvailableDbEngines = dbEngines.some(
(obj) => (obj?.data || []).length > 0
);

const { canCreate } = useNamespacePermissionsForResource('database-clusters');
const { canRead } = useNamespacePermissionsForResource('database-engines');

const canAddCluster = canCreate.length > 0 && canRead.length > 0;
const canAddCluster = canCreate.length > 0 && hasAvailableDbEngines;
const dbClustersResults = useDBClustersForNamespaces(
namespaces.map((ns) => ({
namespace: ns,
Expand Down Expand Up @@ -155,7 +161,10 @@ export const DbClusterView = () => {
tableName="dbClusterView"
emptyState={
namespaces.length > 0 ? (
<EmptyStateDatabases showCreationButton={canAddCluster} />
<EmptyStateDatabases
showCreationButton={canAddCluster}
hasCreatePermission={canAddCluster}
/>
) : (
<EmptyStateNamespaces />
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ import { BasicInformationSection } from './basic-information/basic';
import { ConnectionDetails } from './connection-details';
import { MonitoringDetails } from './monitoring/monitoring';
import { AdvancedConfiguration } from './advanced-configuration';
import { DbClusterContext } from 'pages/db-cluster-details/dbCluster.context';
import { useContext } from 'react';
import { useMemo } from 'react';
import { useMonitoringInstancesForNamespace } from 'hooks';
import { useRBACPermissions } from 'hooks/rbac';

export const DbDetails = ({
loading,
Expand All @@ -40,7 +41,22 @@ export const DbDetails = ({
externalAccess,
parameters,
}: DatabaseDetailsOverviewCardProps) => {
const { canReadMonitoring } = useContext(DbClusterContext);
const { data: monitoringInstances } =
useMonitoringInstancesForNamespace(namespace);

const monitoringInstancesToCheck = useMemo(
() =>
(monitoringInstances || []).map(
(monitoringInstance) =>
`${monitoringInstance.namespace}/${monitoringInstance.name}`
),
[monitoringInstances]
);

const { canRead: canReadMonitoring } = useRBACPermissions(
'monitoring-instances',
monitoringInstancesToCheck.length > 0 ? monitoringInstancesToCheck : '*'
);

return (
<OverviewCard
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,12 @@ export const MonitoringDetails = ({
monitoring,
}: MonitoringConfigurationOverviewCardProps) => {
const [openEditModal, setOpenEditModal] = useState(false);
const { dbCluster, canUpdateDb, canUpdateMonitoring } =
useContext(DbClusterContext);
const { dbCluster, canUpdateDb } = useContext(DbClusterContext);
const restoringOrDeleting = [
DbClusterStatus.restoring,
DbClusterStatus.deleting,
].includes(dbCluster?.status?.status!);
const editable = canUpdateDb && !restoringOrDeleting && canUpdateMonitoring;
const editable = canUpdateDb && !restoringOrDeleting;

const { mutate: updateDbClusterMonitoring } = useUpdateDbClusterMonitoring();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ export const DbClusterContext = createContext<DbClusterContextProps>({
dbCluster: {} as DbCluster,
isLoading: false,
canReadBackups: false,
canReadMonitoring: false,
canUpdateMonitoring: false,
canReadCredentials: false,
canUpdateDb: false,
clusterDeleted: false,
Expand Down Expand Up @@ -59,8 +57,6 @@ export const DbClusterContextProvider = ({
'database-cluster-backups',
`${namespace}/${dbClusterName}`
);
const { canRead: canReadMonitoring, canUpdate: canUpdateMonitoring } =
useRBACPermissions('monitoring-instances', `${namespace}/*`);
const { canRead: canReadCredentials } = useRBACPermissions(
'database-cluster-credentials',
`${namespace}/${dbClusterName}`
Expand Down Expand Up @@ -97,8 +93,6 @@ export const DbClusterContextProvider = ({
dbCluster,
isLoading,
canReadBackups,
canReadMonitoring,
canUpdateMonitoring,
canUpdateDb,
canReadCredentials,
clusterDeleted,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ export interface DbClusterContextProps {
dbCluster?: DbCluster;
isLoading: boolean;
canReadBackups: boolean;
canReadMonitoring: boolean;
canUpdateMonitoring: boolean;
canUpdateDb: boolean;
canReadCredentials: boolean;
queryResult: QueryObserverResult<DbCluster, unknown>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,23 +29,17 @@ import {
convertBackupStoragesPayloadToTableFormat,
convertStoragesType,
} from './storage-locations.utils';
import { useNamespaces } from 'hooks/api/namespaces';
import { useNamespacePermissionsForResource } from 'hooks/rbac';
import TableActionsMenu from '../../../components/table-actions-menu';
import { StorageLocationsActionButtons } from './storage-locations-menu-actions';

export const StorageLocations = () => {
const queryClient = useQueryClient();
const { canCreate } = useNamespacePermissionsForResource('backup-storages');
const { data: namespaces = [] } = useNamespaces();
const backupStorages = useBackupStorages(
namespaces.map((namespace) => ({
namespace: namespace,
}))
);
const backupStorages = useBackupStorages();

const backupStoragesLoading = backupStorages.some(
(result) => result.queryResult.isLoading
(result) => result.isLoading
);

const tableData = useMemo(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { StorageType } from 'shared-types/backupStorages.types';
import { UseQueryResult } from '@tanstack/react-query';
import {
GetBackupStoragesPayload,
StorageType,
} from 'shared-types/backupStorages.types';
import { Messages } from './storage-locations.messages';
import { BackupStoragesForNamespaceResult } from 'hooks/api/backup-storages/useBackupStorages';
import { BackupStorageTableElement } from './storage-locations.types';

export const convertStoragesType = (value: StorageType) =>
Expand All @@ -11,14 +14,13 @@ export const convertStoragesType = (value: StorageType) =>
})[value];

export const convertBackupStoragesPayloadToTableFormat = (
data: BackupStoragesForNamespaceResult[]
data: UseQueryResult<GetBackupStoragesPayload, Error>[]
): BackupStorageTableElement[] => {
const result: BackupStorageTableElement[] = [];
data.forEach((item) => {
const tableDataForNamespace: BackupStorageTableElement[] = item?.queryResult
?.isSuccess
? item.queryResult?.data.map((storage) => ({
namespace: item.namespace,
const tableDataForNamespace: BackupStorageTableElement[] = item.isSuccess
? item.data.map((storage) => ({
namespace: storage.namespace,
name: storage.name,
type: storage.type,
bucketName: storage.bucketName,
Expand Down

0 comments on commit f703832

Please sign in to comment.