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:
parent
03712966e4
commit
70710cfeb7
32 changed files with 554 additions and 263 deletions
|
@ -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(', ');
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -158,6 +158,7 @@ function EdgeKeyInfo({
|
|||
commands={commands}
|
||||
isNomadTokenVisible
|
||||
asyncMode={asyncMode}
|
||||
showMetaFields
|
||||
>
|
||||
<FormControl label="Portainer API server URL">
|
||||
<Input value={url} readOnly />
|
||||
|
|
|
@ -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>(
|
||||
|
|
|
@ -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)}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue