Skip to content
This repository has been archived by the owner on Jan 16, 2025. It is now read-only.

Fix manage tags when selecting all on serverside pagination #77

Merged
merged 1 commit into from
Apr 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 36 additions & 14 deletions src/AutoUI/Actions/Tags.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import React from 'react';
import {
ResourceTagSubmitInfo,
Spinner,
SubmitInfo,
TagManagementModal,
useRequest,
} from 'rendition';
import { useTranslation } from '../../hooks/useTranslation';
import { AutoUIContext, AutoUIBaseResource } from '../schemaOps';
import { enqueueSnackbar, closeSnackbar } from '@balena/ui-shared-components';

interface TagsProps<T> {
selected: T[];
selected: T[] | undefined;
autouiContext: AutoUIContext<T>;
setIsBusyMessage: (message: string | undefined) => void;
onDone: () => void;
Expand All @@ -25,7 +27,25 @@ export const Tags = <T extends AutoUIBaseResource<T>>({
}: TagsProps<T>) => {
const { t } = useTranslation();

const { sdk } = autouiContext;
const { sdk, internalPineFilter, checkedState } = autouiContext;

const getAllTags = sdk?.tags && 'getAll' in sdk.tags ? sdk.tags.getAll : null;

const { data: items, isLoading } = useRequest(
async () => {
if (
// we are in server side pagination
selected == null &&
checkedState === 'all' &&
getAllTags
) {
return await getAllTags(internalPineFilter);
}
return selected;
},
[internalPineFilter, checkedState, getAllTags, selected == null],
{ polling: false },
);

const changeTags = React.useCallback(
async (tags: SubmitInfo<ResourceTagSubmitInfo, ResourceTagSubmitInfo>) => {
Expand Down Expand Up @@ -64,21 +84,23 @@ export const Tags = <T extends AutoUIBaseResource<T>>({
[sdk?.tags, refresh, selected],
);

if (!autouiContext.tagField || !autouiContext.nameField) {
if (!autouiContext.tagField || !autouiContext.nameField || !items) {
return null;
}

return (
<TagManagementModal<T>
items={selected}
itemType={autouiContext.resource}
titleField={autouiContext.nameField as keyof T}
tagField={autouiContext.tagField as keyof T}
done={(tagSubmitInfo) => {
changeTags(tagSubmitInfo);
onDone();
}}
cancel={() => onDone()}
/>
<Spinner show={isLoading} width="100%" height="100%">
<TagManagementModal<T>
items={items}
itemType={autouiContext.resource}
titleField={autouiContext.nameField as keyof T}
tagField={autouiContext.tagField as keyof T}
done={(tagSubmitInfo) => {
changeTags(tagSubmitInfo);
onDone();
}}
cancel={() => onDone()}
/>
</Spinner>
);
};
60 changes: 43 additions & 17 deletions src/AutoUI/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
autoUIJsonSchemaPick,
ActionData,
Priorities,
AutoUITagsSdk,
} from './schemaOps';
import { LensSelection } from './Lenses/LensSelection';
import type { JSONSchema7 as JSONSchema } from 'json-schema';
Expand Down Expand Up @@ -40,7 +41,6 @@ import {
Dictionary,
FiltersView,
Format,
ResourceTagModelService,
Spinner,
TableColumn,
SchemaSieve as sieve,
Expand All @@ -55,6 +55,7 @@ import { useHistory } from '../hooks/useHistory';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCircleQuestion } from '@fortawesome/free-regular-svg-icons';
import {
type PineFilterObject,
convertToPineClientFilter,
orderbyBuilder,
} from '../oData/jsonToOData';
Expand Down Expand Up @@ -102,9 +103,7 @@ export interface AutoUIProps<T> extends Omit<Material.BoxProps, 'onChange'> {
/** Actions is an array of actions applicable on the selected items */
actions?: Array<AutoUIAction<T>>;
/** The sdk is used to pass the method to handle tags when added removed or updated */
sdk?: {
tags?: ResourceTagModelService;
};
sdk?: { tags: AutoUITagsSdk<T> };
/** Dictionary of {[column_property]: customFunction} where the customFunction is the function to sort data on column header click */
customSort?: Dictionary<(a: T, b: T) => number> | Dictionary<string>;
// TODO: Ideally the base URL is autogenerated, but there are some issues with that currently (eg. instead of application we have apps in the URL)
Expand Down Expand Up @@ -188,6 +187,9 @@ export const AutoUI = <T extends AutoUIBaseResource<T>>({
const [views, setViews] = React.useState<FiltersView[]>([]);
const [selected, setSelected] = React.useState<T[] | undefined>([]);
const [checkedState, setCheckedState] = React.useState<CheckedState>('none');
const [internalPineFilter, setInternalPineFilter] = React.useState<
PineFilterObject | null | undefined
>();
const [isBusyMessage, setIsBusyMessage] = React.useState<
string | undefined
>();
Expand Down Expand Up @@ -298,18 +300,27 @@ export const AutoUI = <T extends AutoUIBaseResource<T>>({
title: t('actions.manage_tags'),
type: 'update',
section: 'settings',
renderer: ({ affectedEntries, onDone }) =>
!!affectedEntries && (
<Tags
selected={affectedEntries}
autouiContext={autouiContext}
onDone={onDone}
setIsBusyMessage={setIsBusyMessage}
refresh={refresh}
/>
renderer: ({ affectedEntries, onDone }) => {
return (
(!!affectedEntries || (sdk.tags && 'getAll' in sdk.tags)) && (
<Tags
selected={affectedEntries}
autouiContext={autouiContext}
onDone={onDone}
setIsBusyMessage={setIsBusyMessage}
refresh={refresh}
/>
)
);
},
isDisabled: async ({ affectedEntries, checkedState }) =>
getTagsDisabledReason(
affectedEntries,
tagField as keyof T,
checkedState,
sdk.tags,
t,
),
isDisabled: ({ affectedEntries }) =>
getTagsDisabledReason(affectedEntries, tagField as keyof T, t),
}
: null;

Expand All @@ -323,8 +334,19 @@ export const AutoUI = <T extends AutoUIBaseResource<T>>({
actions: !!tagsAction ? (actions || []).concat(tagsAction) : actions,
customSort,
sdk,
internalPineFilter,
checkedState,
};
}, [model, getBaseUrl, onEntityClick, actions, customSort, sdk]);
}, [
model,
getBaseUrl,
onEntityClick,
actions,
customSort,
sdk,
internalPineFilter,
checkedState,
]);

const properties = React.useMemo(
() =>
Expand Down Expand Up @@ -363,17 +385,21 @@ export const AutoUI = <T extends AutoUIBaseResource<T>>({
if (!onChange) {
return;
}
const pineFilter = pagination?.serverSide
? convertToPineClientFilter([], updatedFilters)
: null;
thgreasi marked this conversation as resolved.
Show resolved Hide resolved
const oData = pagination?.serverSide
? pickBy(
{
$filter: convertToPineClientFilter([], updatedFilters),
$filter: pineFilter,
$orderby: orderbyBuilder(sortInfo, customSort),
$top: itemsPerPage,
$skip: page * itemsPerPage,
},
(v) => v != null,
)
: null;
setInternalPineFilter(pineFilter);
onChange?.({
filters: updatedFilters,
page,
Expand Down
17 changes: 16 additions & 1 deletion src/AutoUI/schemaOps.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { JSONSchema7 as JSONSchema } from 'json-schema';
import pick from 'lodash/pick';
import { CheckedState, Dictionary, ResourceTagModelService } from 'rendition';
import { PineFilterObject } from '~/oData/jsonToOData';

type MaybePromise<T> = T | Promise<T>;

Expand Down Expand Up @@ -36,6 +37,18 @@ export interface AutoUIModel<T> {
priorities?: Priorities<T>;
}

export type AutoUITagsSdk<T> = ResourceTagModelService &
(
| {}
| {
getAll: (itemsOrFilters: any) => T[] | Promise<T[]>;
canAccess: (param: {
checkedState?: CheckedState;
selected?: T[];
}) => Promise<boolean>;
}
);

export interface AutoUIAction<T> {
title: string;
type: 'create' | 'update' | 'delete';
Expand Down Expand Up @@ -87,8 +100,10 @@ export interface AutoUIContext<T> {
actions?: Array<AutoUIAction<T>>;
customSort?: Dictionary<(a: T, b: T) => number> | Dictionary<string>;
sdk?: {
tags?: ResourceTagModelService;
tags?: AutoUITagsSdk<T>;
};
internalPineFilter?: PineFilterObject | null;
checkedState: CheckedState;
}

export interface ActionData<T> {
Expand Down
30 changes: 20 additions & 10 deletions src/AutoUI/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { AutoUIBaseResource, Permissions, Priorities } from './schemaOps';
import {
AutoUIBaseResource,
Permissions,
Priorities,
AutoUITagsSdk,
} from './schemaOps';
import castArray from 'lodash/castArray';
import get from 'lodash/get';
import { TFunction } from '../hooks/useTranslation';
Expand Down Expand Up @@ -72,22 +77,27 @@ export const ObjectFromEntries = (entries: any[]) => {
return obj;
};

export const getTagsDisabledReason = <T extends AutoUIBaseResource<T>>(
export const getTagsDisabledReason = async <T extends AutoUIBaseResource<T>>(
selected: T[] | undefined,
tagField: keyof T,
checkedState: CheckedState | undefined,
tagsSdk: AutoUITagsSdk<T>,
t: TFunction,
) => {
if (!selected || selected.length === 0) {
if (checkedState !== 'all' && (!selected || selected.length === 0)) {
return t('info.no_selected');
}

const lacksPermissionsOnSelected = selected.some((entry) => {
return (
!entry.__permissions.delete &&
!entry.__permissions.create.includes(tagField) &&
!entry.__permissions.update.includes(tagField as keyof T)
);
});
const lacksPermissionsOnSelected =
tagsSdk && 'canAccess' in tagsSdk
? !(await tagsSdk.canAccess({ checkedState, selected }))
: selected?.some((entry) => {
return (
!entry.__permissions.delete &&
!entry.__permissions.create.includes(tagField) &&
!entry.__permissions.update.includes(tagField as keyof T)
);
});

if (lacksPermissionsOnSelected) {
// TODO: Pass the resource name instead.
Expand Down
2 changes: 1 addition & 1 deletion src/oData/jsonToOData.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { JSONSchema, TableSortOptions } from 'rendition';
import { AutoUIContext } from '~/AutoUI/schemaOps';

type PineFilterObject = Record<string, any>;
export type PineFilterObject = Record<string, any>;

interface FilterMutation extends JSONSchema {
$and?: any[];
Expand Down
Loading