1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-08 23:35:31 +02:00

feat(edge): associate edge env to meta fields [EE-3209] (#8551)

* refactor(edge/groups): load edge groups in selector

fix(edge/stacks): remove double groups title

* feat(edge): supply meta fields to edge script [EE-5043]

* feat(edge): auto assign aeec envs to groups and tags [EE-5043]

fix [EE-5043]

fix(envs): fix global key test

* fix(edge/groups): save group type

* refactor(edge/devices): move loading of devices to table

* refactor(tags): select paramter for query

* feat(edge/devices): show meta fields

* refactor(home): simplify filter

* feat(edge/devices): filter by meta fields

* refactor(edge/devices): break filter and loading hook
This commit is contained in:
Chaim Lev-Ari 2023-03-06 22:25:04 +02:00 committed by GitHub
parent 03712966e4
commit 70710cfeb7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 554 additions and 263 deletions

View file

@ -130,17 +130,21 @@ export function EnvironmentItem({
}
function useEnvironmentTagNames(tagIds?: TagId[]) {
const { tags, isLoading } = useTags((tags) => {
if (!tagIds) {
return [];
}
return _.compact(
tagIds
.map((id) => tags.find((tag) => tag.ID === id))
.map((tag) => tag?.Name)
);
const tagsQuery = useTags({
select: (tags) => {
if (!tagIds) {
return [];
}
return _.compact(
tagIds
.map((id) => tags.find((tag) => tag.ID === id))
.map((tag) => tag?.Name)
);
},
});
const { data: tags, isLoading } = tagsQuery;
if (tags && tags.length > 0) {
return tags.join(', ');
}

View file

@ -29,7 +29,7 @@ import { PaginationControls } from '@@/PaginationControls';
import { SearchBar, useSearchBarState } from '@@/datatables/SearchBar';
import { useHomePageFilter } from './HomepageFilter';
import { ConnectionType, Filter } from './types';
import { ConnectionType } from './types';
import { EnvironmentItem } from './EnvironmentItem';
import { KubeconfigButton } from './KubeconfigButton';
import { NoEnvironmentsInfoPanel } from './NoEnvironmentsInfoPanel';
@ -48,15 +48,16 @@ export function EnvironmentList({ onClickBrowse, onRefresh }: Props) {
const { isAdmin } = useUser();
const currentEnvStore = useStore(environmentStore);
const [platformTypes, setPlatformTypes] = useHomePageFilter<
Filter<PlatformType>[]
>('platformType', []);
const [platformTypes, setPlatformTypes] = useHomePageFilter<PlatformType[]>(
'platformType',
[]
);
const [searchBarValue, setSearchBarValue] = useSearchBarState(storageKey);
const [pageLimit, setPageLimit] = usePaginationLimitState(storageKey);
const [page, setPage] = useState(1);
const [connectionTypes, setConnectionTypes] = useHomePageFilter<
Filter<ConnectionType>[]
ConnectionType[]
>('connectionTypes', []);
const [statusFilter, setStatusFilter] = useHomePageFilter<
@ -77,20 +78,17 @@ export function EnvironmentList({ onClickBrowse, onRefresh }: Props) {
false
);
const [statusState, setStatusState] = useHomePageFilter<Filter[]>(
const [statusState, setStatusState] = useHomePageFilter<number[]>(
'status_state',
[]
);
const [tagState, setTagState] = useHomePageFilter<Filter[]>('tag_state', []);
const [groupState, setGroupState] = useHomePageFilter<Filter[]>(
const [tagState, setTagState] = useHomePageFilter<number[]>('tag_state', []);
const [groupState, setGroupState] = useHomePageFilter<number[]>(
'group_state',
[]
);
const [sortByState, setSortByState] = useHomePageFilter<Filter | undefined>(
'sort_by_state',
undefined
);
const [agentVersions, setAgentVersions] = useHomePageFilter<Filter<string>[]>(
const [agentVersions, setAgentVersions] = useHomePageFilter<string[]>(
'agentVersions',
[]
);
@ -98,17 +96,14 @@ export function EnvironmentList({ onClickBrowse, onRefresh }: Props) {
const groupsQuery = useGroups();
const environmentsQueryParams: EnvironmentsQueryParams = {
types: getTypes(
platformTypes.map((p) => p.value),
connectionTypes.map((p) => p.value)
),
types: getTypes(platformTypes, connectionTypes),
search: searchBarValue,
status: statusFilter,
tagIds: tagFilter?.length ? tagFilter : undefined,
groupIds: groupFilter,
provisioned: true,
tagsPartialMatch: true,
agentVersions: agentVersions.map((a) => a.value),
agentVersions,
updateInformation: isBE,
edgeAsync: getEdgeAsyncValue(connectionTypes),
};
@ -202,11 +197,11 @@ export function EnvironmentList({ onClickBrowse, onRefresh }: Props) {
setAgentVersions={setAgentVersions}
agentVersions={agentVersions}
clearFilter={clearFilter}
sortOnchange={sortOnchange}
sortOnChange={sortOnchange}
sortOnDescending={sortOnDescending}
sortByDescending={sortByDescending}
sortByButton={sortByButton}
sortByState={sortByState}
sortByState={sortByFilter}
/>
</div>
<div
@ -305,50 +300,32 @@ export function EnvironmentList({ onClickBrowse, onRefresh }: Props) {
return _.intersection(selectedTypesByConnection, selectedTypesByPlatform);
}
function statusOnChange(filterOptions: Filter[]) {
setStatusState(filterOptions);
if (filterOptions.length === 0) {
function statusOnChange(value: number[]) {
setStatusState(value);
if (value.length === 0) {
setStatusFilter([]);
} else {
const filteredStatus = [
...new Set(
filterOptions.map(
(filterOptions: { value: number }) => filterOptions.value
)
),
];
const filteredStatus = [...new Set(value)];
setStatusFilter(filteredStatus);
}
}
function groupOnChange(filterOptions: Filter[]) {
setGroupState(filterOptions);
if (filterOptions.length === 0) {
function groupOnChange(value: number[]) {
setGroupState(value);
if (value.length === 0) {
setGroupFilter([]);
} else {
const filteredGroups = [
...new Set(
filterOptions.map(
(filterOptions: { value: number }) => filterOptions.value
)
),
];
const filteredGroups = [...new Set(value)];
setGroupFilter(filteredGroups);
}
}
function tagOnChange(filterOptions: Filter[]) {
setTagState(filterOptions);
if (filterOptions.length === 0) {
function tagOnChange(value: number[]) {
setTagState(value);
if (value.length === 0) {
setTagFilter([]);
} else {
const filteredTags = [
...new Set(
filterOptions.map(
(filterOptions: { value: number }) => filterOptions.value
)
),
];
const filteredTags = [...new Set(value)];
setTagFilter(filteredTags);
}
}
@ -365,16 +342,9 @@ export function EnvironmentList({ onClickBrowse, onRefresh }: Props) {
setConnectionTypes([]);
}
function sortOnchange(filterOptions: Filter) {
if (filterOptions !== null) {
setSortByFilter(filterOptions.label);
setSortByButton(true);
setSortByState(filterOptions);
} else {
setSortByFilter('');
setSortByButton(true);
setSortByState(undefined);
}
function sortOnchange(value: string) {
setSortByFilter(value);
setSortByButton(!!value);
}
function sortOnDescending() {
@ -407,14 +377,13 @@ function renderItems(
return items;
}
function getEdgeAsyncValue(connectionTypes: Filter<ConnectionType>[]) {
function getEdgeAsyncValue(connectionTypes: ConnectionType[]) {
const hasEdgeAsync = connectionTypes.some(
(connectionType) => connectionType.value === ConnectionType.EdgeAgentAsync
(connectionType) => connectionType === ConnectionType.EdgeAgentAsync
);
const hasEdgeStandard = connectionTypes.some(
(connectionType) =>
connectionType.value === ConnectionType.EdgeAgentStandard
(connectionType) => connectionType === ConnectionType.EdgeAgentStandard
);
// If both are selected, we don't want to filter on either, and same for if both are not selected

View file

@ -9,7 +9,7 @@ import { useGroups } from '../../environments/environment-groups/queries';
import { HomepageFilter } from './HomepageFilter';
import { SortbySelector } from './SortbySelector';
import { ConnectionType, Filter } from './types';
import { ConnectionType } from './types';
import styles from './EnvironmentList.module.css';
const status = [
@ -17,11 +17,10 @@ const status = [
{ value: EnvironmentStatus.Down, label: 'Down' },
];
const sortByOptions = [
{ value: 1, label: 'Name' },
{ value: 2, label: 'Group' },
{ value: 3, label: 'Status' },
];
const sortByOptions = ['Name', 'Group', 'Status'].map((v) => ({
value: v,
label: v,
}));
export function EnvironmentListFilters({
agentVersions,
@ -37,32 +36,32 @@ export function EnvironmentListFilters({
sortByDescending,
sortByState,
sortOnDescending,
sortOnchange,
sortOnChange,
statusOnChange,
statusState,
tagOnChange,
tagState,
}: {
platformTypes: Filter<PlatformType>[];
setPlatformTypes: (value: Filter<PlatformType>[]) => void;
platformTypes: PlatformType[];
setPlatformTypes: (value: PlatformType[]) => void;
connectionTypes: Filter<ConnectionType>[];
setConnectionTypes: (value: Filter<ConnectionType>[]) => void;
connectionTypes: ConnectionType[];
setConnectionTypes: (value: ConnectionType[]) => void;
statusState: Filter<number>[];
statusOnChange: (filterOptions: Filter[]) => void;
statusState: number[];
statusOnChange: (value: number[]) => void;
tagOnChange: (filterOptions: Filter[]) => void;
tagState: Filter<number>[];
tagOnChange: (value: number[]) => void;
tagState: number[];
groupOnChange: (filterOptions: Filter[]) => void;
groupState: Filter<number>[];
groupOnChange: (value: number[]) => void;
groupState: number[];
setAgentVersions: (value: Filter<string>[]) => void;
agentVersions: Filter<string>[];
setAgentVersions: (value: string[]) => void;
agentVersions: string[];
sortByState: Filter<number> | undefined;
sortOnchange: (filterOptions: Filter) => void;
sortByState: string;
sortOnChange: (value: string) => void;
sortOnDescending: () => void;
sortByDescending: boolean;
@ -85,7 +84,7 @@ export function EnvironmentListFilters({
}));
const tagsQuery = useTags();
const tagOptions = [...(tagsQuery.tags || [])];
const tagOptions = [...(tagsQuery.data || [])];
const uniqueTag = [
...new Map(tagOptions.map((item) => [item.ID, item])).values(),
].map(({ ID: value, Name: label }) => ({
@ -136,7 +135,7 @@ export function EnvironmentListFilters({
/>
</div>
<div className={styles.filterLeft}>
<HomepageFilter<string>
<HomepageFilter
filterOptions={
agentVersionsQuery.data?.map((v) => ({
label: v,
@ -159,7 +158,7 @@ export function EnvironmentListFilters({
<div className={styles.filterRight}>
<SortbySelector
filterOptions={sortByOptions}
onChange={sortOnchange}
onChange={sortOnChange}
onDescending={sortOnDescending}
placeHolder="Sort By"
sortByDescending={sortByDescending}
@ -171,7 +170,7 @@ export function EnvironmentListFilters({
);
}
function getConnectionTypeOptions(platformTypes: Filter<PlatformType>[]) {
function getConnectionTypeOptions(platformTypes: PlatformType[]) {
const platformTypeConnectionType = {
[PlatformType.Docker]: [
ConnectionType.API,
@ -204,12 +203,12 @@ function getConnectionTypeOptions(platformTypes: Filter<PlatformType>[]) {
return _.compact(
_.intersection(
...platformTypes.map((p) => platformTypeConnectionType[p.value])
...platformTypes.map((p) => platformTypeConnectionType[p])
).map((c) => connectionTypesDefaultOptions.find((o) => o.value === c))
);
}
function getPlatformTypeOptions(connectionTypes: Filter<ConnectionType>[]) {
function getPlatformTypeOptions(connectionTypes: ConnectionType[]) {
const platformDefaultOptions = [
{ value: PlatformType.Docker, label: 'Docker' },
{ value: PlatformType.Azure, label: 'Azure' },
@ -244,7 +243,7 @@ function getPlatformTypeOptions(connectionTypes: Filter<ConnectionType>[]) {
return _.compact(
_.intersection(
...connectionTypes.map((p) => connectionTypePlatformType[p.value])
...connectionTypes.map((p) => connectionTypePlatformType[p])
).map((c) => platformDefaultOptions.find((o) => o.value === c))
);
}

View file

@ -2,18 +2,19 @@ import { components, OptionProps } from 'react-select';
import { useLocalStorage } from '@/react/hooks/useLocalStorage';
import { Select } from '@@/form-components/ReactSelect';
import { Filter } from './types';
import {
type Option as OptionType,
PortainerSelect,
} from '@@/form-components/PortainerSelect';
interface Props<TValue = number> {
filterOptions?: Filter<TValue>[];
onChange: (filterOptions: Filter<TValue>[]) => void;
filterOptions?: OptionType<TValue>[];
onChange: (value: TValue[]) => void;
placeHolder: string;
value: Filter<TValue>[];
value: TValue[];
}
function Option<TValue = number>(props: OptionProps<Filter<TValue>, true>) {
function Option<TValue = number>(props: OptionProps<OptionType<TValue>, true>) {
const { isSelected, label } = props;
return (
<div>
@ -21,8 +22,10 @@ function Option<TValue = number>(props: OptionProps<Filter<TValue>, true>) {
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
>
<input type="checkbox" checked={isSelected} onChange={() => null} />{' '}
<label>{label}</label>
<div className="flex items-center gap-2">
<input type="checkbox" checked={isSelected} onChange={() => null} />
<label className="whitespace-nowrap">{label}</label>
</div>
</components.Option>
</div>
);
@ -35,14 +38,14 @@ export function HomepageFilter<TValue = number>({
value,
}: Props<TValue>) {
return (
<Select
closeMenuOnSelect={false}
<PortainerSelect<TValue>
placeholder={placeHolder}
options={filterOptions}
value={value}
isMulti
components={{ Option }}
onChange={(option) => onChange([...option])}
bindToBody
/>
);
}

View file

@ -1,19 +1,18 @@
import clsx from 'clsx';
import { Select } from '@@/form-components/ReactSelect';
import { Option, PortainerSelect } from '@@/form-components/PortainerSelect';
import { TableHeaderSortIcons } from '@@/datatables/TableHeaderSortIcons';
import { Filter } from './types';
import styles from './SortbySelector.module.css';
interface Props {
filterOptions: Filter[];
onChange: (filterOptions: Filter) => void;
filterOptions: Option<string>[];
onChange: (value: string) => void;
onDescending: () => void;
placeHolder: string;
sortByDescending: boolean;
sortByButton: boolean;
value?: Filter;
value: string;
}
export function SortbySelector({
@ -28,10 +27,10 @@ export function SortbySelector({
const sorted = sortByButton && !!value;
return (
<div className="flex items-center justify-end gap-1">
<Select
<PortainerSelect
placeholder={placeHolder}
options={filterOptions}
onChange={(option) => onChange(option as Filter)}
onChange={(option) => onChange(option || '')}
isClearable
value={value}
/>

View file

@ -158,6 +158,7 @@ function EdgeKeyInfo({
commands={commands}
isNomadTokenVisible
asyncMode={asyncMode}
showMetaFields
>
<FormControl label="Portainer API server URL">
<Input value={url} readOnly />

View file

@ -5,8 +5,12 @@ import { error as notifyError } from '@/portainer/services/notifications';
import { EnvironmentGroup, EnvironmentGroupId } from './types';
import { getGroup, getGroups } from './environment-groups.service';
export function useGroups() {
return useQuery<EnvironmentGroup[]>(['environment-groups'], getGroups);
export function useGroups<T = EnvironmentGroup[]>({
select,
}: { select?: (group: EnvironmentGroup[]) => T } = {}) {
return useQuery(['environment-groups'], getGroups, {
select,
});
}
export function useGroup<T = EnvironmentGroup>(

View file

@ -6,9 +6,8 @@ import { EnvironmentGroupId } from '@/react/portainer/environments/environment-g
import { FormControl } from '@@/form-components/FormControl';
import { Select } from '@@/form-components/Input';
export function GroupField() {
const [fieldProps, metaProps, helpers] =
useField<EnvironmentGroupId>('meta.groupId');
export function GroupField({ name = 'meta.groupId' }: { name?: string }) {
const [fieldProps, metaProps, helpers] = useField<EnvironmentGroupId>(name);
const groupsQuery = useGroups();
if (!groupsQuery.data) {
@ -23,7 +22,7 @@ export function GroupField() {
return (
<FormControl label="Group" errors={metaProps.error}>
<Select
name="meta.groupId"
name={name}
options={options}
value={fieldProps.value}
onChange={(e) => handleChange(e.target.value)}