mirror of https://github.com/portainer/portainer
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 hookpull/8619/head
parent
03712966e4
commit
70710cfeb7
|
@ -1,19 +1,5 @@
|
||||||
<form class="form-horizontal">
|
<form class="form-horizontal">
|
||||||
<div class="col-sm-12 form-section-title"> Edge Groups </div>
|
<edge-groups-selector value="$ctrl.model.EdgeGroups" items="$ctrl.edgeGroups" on-change="($ctrl.onChangeGroups)"></edge-groups-selector>
|
||||||
<div class="form-group">
|
|
||||||
<div class="col-sm-12">
|
|
||||||
<edge-groups-selector value="$ctrl.model.EdgeGroups" items="$ctrl.edgeGroups" on-change="($ctrl.onChangeGroups)"></edge-groups-selector>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="col-sm-12 vertical-center help-block small text-warning" ng-if="$ctrl.model.DeploymentType === undefined">
|
|
||||||
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> There are no available deployment types when there is more than one type of environment in your edge group
|
|
||||||
selection (e.g. Kubernetes and Docker environments). Please select edge groups that have environments of the same type.
|
|
||||||
</p>
|
|
||||||
<p class="col-sm-12 vertical-center help-block small text-warning" ng-if="$ctrl.model.DeploymentType === $ctrl.EditorType.Compose && $ctrl.hasKubeEndpoint()">
|
|
||||||
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Edge groups with kubernetes environments no longer support compose deployment types in Portainer. Please select
|
|
||||||
edge groups that only have docker environments when using compose deployment types.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<edge-stack-deployment-type-selector
|
<edge-stack-deployment-type-selector
|
||||||
allow-kube-to-select-compose="$ctrl.allowKubeToSelectCompose"
|
allow-kube-to-select-compose="$ctrl.allowKubeToSelectCompose"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<form class="form-horizontal" name="EdgeGroupForm" ng-submit="$ctrl.formAction()">
|
<form class="form-horizontal" name="EdgeGroupForm" ng-submit="$ctrl.handleSubmit()">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="group_name" class="col-sm-3 col-lg-2 control-label required text-left"> Name </label>
|
<label for="group_name" class="col-sm-3 col-lg-2 control-label required text-left"> Name </label>
|
||||||
<div class="col-sm-9 col-lg-10">
|
<div class="col-sm-9 col-lg-10">
|
||||||
|
|
|
@ -37,6 +37,7 @@ export class EdgeGroupFormController {
|
||||||
this.onChangeDynamic = this.onChangeDynamic.bind(this);
|
this.onChangeDynamic = this.onChangeDynamic.bind(this);
|
||||||
this.onChangeModel = this.onChangeModel.bind(this);
|
this.onChangeModel = this.onChangeModel.bind(this);
|
||||||
this.onChangePartialMatch = this.onChangePartialMatch.bind(this);
|
this.onChangePartialMatch = this.onChangePartialMatch.bind(this);
|
||||||
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
|
|
||||||
$scope.$watch(
|
$scope.$watch(
|
||||||
() => this.model,
|
() => this.model,
|
||||||
|
@ -118,6 +119,10 @@ export class EdgeGroupFormController {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleSubmit() {
|
||||||
|
this.formAction(this.model);
|
||||||
|
}
|
||||||
|
|
||||||
$onInit() {
|
$onInit() {
|
||||||
this.getTags();
|
this.getTags();
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,13 @@ export const componentsModule = angular
|
||||||
.module('portainer.edge.react.components', [])
|
.module('portainer.edge.react.components', [])
|
||||||
.component(
|
.component(
|
||||||
'edgeGroupsSelector',
|
'edgeGroupsSelector',
|
||||||
r2a(EdgeGroupsSelector, ['items', 'onChange', 'value'])
|
r2a(withReactQuery(EdgeGroupsSelector), [
|
||||||
|
'onChange',
|
||||||
|
'value',
|
||||||
|
'error',
|
||||||
|
'horizontal',
|
||||||
|
'isGroupVisible',
|
||||||
|
])
|
||||||
)
|
)
|
||||||
.component(
|
.component(
|
||||||
'edgeScriptForm',
|
'edgeScriptForm',
|
||||||
|
@ -21,6 +27,7 @@ export const componentsModule = angular
|
||||||
'commands',
|
'commands',
|
||||||
'isNomadTokenVisible',
|
'isNomadTokenVisible',
|
||||||
'asyncMode',
|
'asyncMode',
|
||||||
|
'showMetaFields',
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
.component(
|
.component(
|
||||||
|
|
|
@ -21,7 +21,6 @@ export class CreateEdgeGroupController {
|
||||||
};
|
};
|
||||||
|
|
||||||
this.createGroup = this.createGroup.bind(this);
|
this.createGroup = this.createGroup.bind(this);
|
||||||
this.createGroupAsync = this.createGroupAsync.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async $onInit() {
|
async $onInit() {
|
||||||
|
@ -31,20 +30,18 @@ export class CreateEdgeGroupController {
|
||||||
this.state.loaded = true;
|
this.state.loaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
createGroup() {
|
async createGroup(model) {
|
||||||
return this.$async(this.createGroupAsync);
|
return this.$async(async () => {
|
||||||
}
|
this.state.actionInProgress = true;
|
||||||
|
try {
|
||||||
async createGroupAsync() {
|
await this.EdgeGroupService.create(model);
|
||||||
this.state.actionInProgress = true;
|
this.Notifications.success('Success', 'Edge group successfully created');
|
||||||
try {
|
this.$state.go('edge.groups');
|
||||||
await this.EdgeGroupService.create(this.model);
|
} catch (err) {
|
||||||
this.Notifications.success('Success', 'Edge group successfully created');
|
this.Notifications.error('Failure', err, 'Unable to create edge group');
|
||||||
this.$state.go('edge.groups');
|
} finally {
|
||||||
} catch (err) {
|
this.state.actionInProgress = false;
|
||||||
this.Notifications.error('Failure', err, 'Unable to create edge group');
|
}
|
||||||
} finally {
|
});
|
||||||
this.state.actionInProgress = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,6 @@ export class EditEdgeGroupController {
|
||||||
};
|
};
|
||||||
|
|
||||||
this.updateGroup = this.updateGroup.bind(this);
|
this.updateGroup = this.updateGroup.bind(this);
|
||||||
this.updateGroupAsync = this.updateGroupAsync.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async $onInit() {
|
async $onInit() {
|
||||||
|
@ -28,20 +27,18 @@ export class EditEdgeGroupController {
|
||||||
this.state.loaded = true;
|
this.state.loaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateGroup() {
|
updateGroup(group) {
|
||||||
return this.$async(this.updateGroupAsync);
|
return this.$async(async () => {
|
||||||
}
|
this.state.actionInProgress = true;
|
||||||
|
try {
|
||||||
async updateGroupAsync() {
|
await this.EdgeGroupService.update(group);
|
||||||
this.state.actionInProgress = true;
|
this.Notifications.success('Success', 'Edge group successfully updated');
|
||||||
try {
|
this.$state.go('edge.groups');
|
||||||
await this.EdgeGroupService.update(this.model);
|
} catch (err) {
|
||||||
this.Notifications.success('Success', 'Edge group successfully updated');
|
this.Notifications.error('Failure', err, 'Unable to update edge group');
|
||||||
this.$state.go('edge.groups');
|
} finally {
|
||||||
} catch (err) {
|
this.state.actionInProgress = false;
|
||||||
this.Notifications.error('Failure', err, 'Unable to update edge group');
|
}
|
||||||
} finally {
|
});
|
||||||
this.state.actionInProgress = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,19 +39,7 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- !name-input -->
|
<!-- !name-input -->
|
||||||
|
|
||||||
<div class="col-sm-12 form-section-title"> Edge Groups </div>
|
<edge-groups-selector ng-if="!$ctrl.noGroups" value="$ctrl.formValues.Groups" on-change="($ctrl.onChangeGroups)" items="$ctrl.edgeGroups"></edge-groups-selector>
|
||||||
<div class="form-group" ng-if="$ctrl.edgeGroups">
|
|
||||||
<div class="col-sm-12">
|
|
||||||
<edge-groups-selector ng-if="!$ctrl.noGroups" value="$ctrl.formValues.Groups" on-change="($ctrl.onChangeGroups)" items="$ctrl.edgeGroups"></edge-groups-selector>
|
|
||||||
</div>
|
|
||||||
<div ng-if="$ctrl.noGroups" class="col-sm-12 small text-muted">
|
|
||||||
No Edge groups are available. Head over to the <a ui-sref="edge.groups">Edge groups view</a> to create one.
|
|
||||||
</div>
|
|
||||||
<p class="col-sm-12 vertical-center help-block small text-warning" ng-if="$ctrl.formValues.DeploymentType === undefined">
|
|
||||||
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> There are no available deployment types when there is more than one type of environment in your edge
|
|
||||||
group selection (e.g. Kubernetes and Docker environments). Please select edge groups that have environments of the same type.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<edge-stack-deployment-type-selector
|
<edge-stack-deployment-type-selector
|
||||||
value="$ctrl.formValues.DeploymentType"
|
value="$ctrl.formValues.DeploymentType"
|
||||||
|
|
|
@ -161,6 +161,7 @@ export const componentsModule = angular
|
||||||
'options',
|
'options',
|
||||||
'isMulti',
|
'isMulti',
|
||||||
'isClearable',
|
'isClearable',
|
||||||
|
'components',
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
.component(
|
.component(
|
||||||
|
|
|
@ -14,14 +14,14 @@ const tagKeys = {
|
||||||
tag: (id: TagId) => [...tagKeys.all, id] as const,
|
tag: (id: TagId) => [...tagKeys.all, id] as const,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useTags<T = Tag>(select?: (tags: Tag[]) => T[]) {
|
export function useTags<T = Tag[]>({
|
||||||
const { data, isLoading } = useQuery(tagKeys.all, () => getTags(), {
|
select,
|
||||||
|
}: { select?: (tags: Tag[]) => T } = {}) {
|
||||||
|
return useQuery(tagKeys.all, () => getTags(), {
|
||||||
staleTime: 50,
|
staleTime: 50,
|
||||||
select,
|
select,
|
||||||
...withError('Failed to retrieve tags'),
|
...withError('Failed to retrieve tags'),
|
||||||
});
|
});
|
||||||
|
|
||||||
return { tags: data, isLoading };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useCreateTagMutation() {
|
export function useCreateTagMutation() {
|
||||||
|
|
|
@ -22,17 +22,17 @@ interface Option {
|
||||||
|
|
||||||
export function TagSelector({ value, allowCreate = false, onChange }: Props) {
|
export function TagSelector({ value, allowCreate = false, onChange }: Props) {
|
||||||
// change the struct because react-select has a bug with Creatable (https://github.com/JedWatson/react-select/issues/3417#issuecomment-461868989)
|
// change the struct because react-select has a bug with Creatable (https://github.com/JedWatson/react-select/issues/3417#issuecomment-461868989)
|
||||||
const tagsQuery = useTags((tags) =>
|
const tagsQuery = useTags({
|
||||||
tags.map((opt) => ({ label: opt.Name, value: opt.ID }))
|
select: (tags) => tags?.map((opt) => ({ label: opt.Name, value: opt.ID })),
|
||||||
);
|
});
|
||||||
|
|
||||||
const createTagMutation = useCreateTagMutation();
|
const createTagMutation = useCreateTagMutation();
|
||||||
|
|
||||||
if (!tagsQuery.tags) {
|
if (!tagsQuery.data) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { tags } = tagsQuery;
|
const { data: tags } = tagsQuery;
|
||||||
|
|
||||||
const selectedTags = _.compact(
|
const selectedTags = _.compact(
|
||||||
value.map((id) => tags.find((tag) => tag.value === id))
|
value.map((id) => tags.find((tag) => tag.value === id))
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
import { OptionsOrGroups } from 'react-select';
|
import {
|
||||||
|
GroupBase,
|
||||||
|
OptionsOrGroups,
|
||||||
|
SelectComponentsConfig,
|
||||||
|
} from 'react-select';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
import { AutomationTestingProps } from '@/types';
|
import { AutomationTestingProps } from '@/types';
|
||||||
|
@ -10,9 +14,10 @@ export interface Option<TValue> {
|
||||||
label: string;
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Group<TValue> = { label: string; options: Option<TValue>[] };
|
type Options<TValue> = OptionsOrGroups<
|
||||||
|
Option<TValue>,
|
||||||
type Options<TValue> = OptionsOrGroups<Option<TValue>, Group<TValue>>;
|
GroupBase<Option<TValue>>
|
||||||
|
>;
|
||||||
|
|
||||||
interface SharedProps extends AutomationTestingProps {
|
interface SharedProps extends AutomationTestingProps {
|
||||||
name?: string;
|
name?: string;
|
||||||
|
@ -28,6 +33,11 @@ interface MultiProps<TValue> extends SharedProps {
|
||||||
onChange(value: readonly TValue[]): void;
|
onChange(value: readonly TValue[]): void;
|
||||||
options: Options<TValue>;
|
options: Options<TValue>;
|
||||||
isMulti: true;
|
isMulti: true;
|
||||||
|
components?: SelectComponentsConfig<
|
||||||
|
Option<TValue>,
|
||||||
|
true,
|
||||||
|
GroupBase<Option<TValue>>
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SingleProps<TValue> extends SharedProps {
|
interface SingleProps<TValue> extends SharedProps {
|
||||||
|
@ -35,6 +45,11 @@ interface SingleProps<TValue> extends SharedProps {
|
||||||
onChange(value: TValue | null): void;
|
onChange(value: TValue | null): void;
|
||||||
options: Options<TValue>;
|
options: Options<TValue>;
|
||||||
isMulti?: never;
|
isMulti?: never;
|
||||||
|
components?: SelectComponentsConfig<
|
||||||
|
Option<TValue>,
|
||||||
|
false,
|
||||||
|
GroupBase<Option<TValue>>
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props<TValue> = MultiProps<TValue> | SingleProps<TValue>;
|
type Props<TValue> = MultiProps<TValue> | SingleProps<TValue>;
|
||||||
|
@ -66,6 +81,7 @@ export function SingleSelect<TValue = string>({
|
||||||
placeholder,
|
placeholder,
|
||||||
isClearable,
|
isClearable,
|
||||||
bindToBody,
|
bindToBody,
|
||||||
|
components,
|
||||||
}: SingleProps<TValue>) {
|
}: SingleProps<TValue>) {
|
||||||
const selectedValue =
|
const selectedValue =
|
||||||
value || (typeof value === 'number' && value === 0)
|
value || (typeof value === 'number' && value === 0)
|
||||||
|
@ -86,6 +102,7 @@ export function SingleSelect<TValue = string>({
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
isDisabled={disabled}
|
isDisabled={disabled}
|
||||||
menuPortalTarget={bindToBody ? document.body : undefined}
|
menuPortalTarget={bindToBody ? document.body : undefined}
|
||||||
|
components={components}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -124,6 +141,7 @@ export function MultiSelect<TValue = string>({
|
||||||
disabled,
|
disabled,
|
||||||
isClearable,
|
isClearable,
|
||||||
bindToBody,
|
bindToBody,
|
||||||
|
components,
|
||||||
}: Omit<MultiProps<TValue>, 'isMulti'>) {
|
}: Omit<MultiProps<TValue>, 'isMulti'>) {
|
||||||
const selectedOptions = findSelectedOptions(options, value);
|
const selectedOptions = findSelectedOptions(options, value);
|
||||||
return (
|
return (
|
||||||
|
@ -142,12 +160,13 @@ export function MultiSelect<TValue = string>({
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
isDisabled={disabled}
|
isDisabled={disabled}
|
||||||
menuPortalTarget={bindToBody ? document.body : undefined}
|
menuPortalTarget={bindToBody ? document.body : undefined}
|
||||||
|
components={components}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isGroup<TValue>(
|
function isGroup<TValue>(
|
||||||
option: Option<TValue> | Group<TValue>
|
option: Option<TValue> | GroupBase<Option<TValue>>
|
||||||
): option is Group<TValue> {
|
): option is GroupBase<Option<TValue>> {
|
||||||
return 'options' in option;
|
return 'options' in option;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,9 @@ const edgePropertiesFormInitialValues: ScriptFormValues = {
|
||||||
nomadToken: '',
|
nomadToken: '',
|
||||||
authEnabled: true,
|
authEnabled: true,
|
||||||
tlsEnabled: false,
|
tlsEnabled: false,
|
||||||
|
edgeGroupsIds: [],
|
||||||
|
group: 0,
|
||||||
|
tagsIds: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@ -23,6 +26,7 @@ interface Props {
|
||||||
commands: CommandTab[] | Partial<Record<OS, CommandTab[]>>;
|
commands: CommandTab[] | Partial<Record<OS, CommandTab[]>>;
|
||||||
isNomadTokenVisible?: boolean;
|
isNomadTokenVisible?: boolean;
|
||||||
asyncMode?: boolean;
|
asyncMode?: boolean;
|
||||||
|
showMetaFields?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EdgeScriptForm({
|
export function EdgeScriptForm({
|
||||||
|
@ -30,6 +34,7 @@ export function EdgeScriptForm({
|
||||||
commands,
|
commands,
|
||||||
isNomadTokenVisible,
|
isNomadTokenVisible,
|
||||||
asyncMode,
|
asyncMode,
|
||||||
|
showMetaFields,
|
||||||
children,
|
children,
|
||||||
}: PropsWithChildren<Props>) {
|
}: PropsWithChildren<Props>) {
|
||||||
const showOsSelector = !(commands instanceof Array);
|
const showOsSelector = !(commands instanceof Array);
|
||||||
|
@ -50,6 +55,7 @@ export function EdgeScriptForm({
|
||||||
isNomadTokenVisible && values.platform === 'nomad'
|
isNomadTokenVisible && values.platform === 'nomad'
|
||||||
}
|
}
|
||||||
hideIdGetter={edgeInfo.id !== undefined}
|
hideIdGetter={edgeInfo.id !== undefined}
|
||||||
|
showMetaFields={showMetaFields}
|
||||||
/>
|
/>
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
{showOsSelector && (
|
{showOsSelector && (
|
||||||
|
|
|
@ -1,9 +1,14 @@
|
||||||
import { useFormikContext, Field } from 'formik';
|
import { useFormikContext, Field } from 'formik';
|
||||||
|
|
||||||
|
import { GroupField } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/MetadataFieldset/GroupsField';
|
||||||
|
|
||||||
import { FormControl } from '@@/form-components/FormControl';
|
import { FormControl } from '@@/form-components/FormControl';
|
||||||
import { Input } from '@@/form-components/Input';
|
import { Input } from '@@/form-components/Input';
|
||||||
import { SwitchField } from '@@/form-components/SwitchField';
|
import { SwitchField } from '@@/form-components/SwitchField';
|
||||||
import { TextTip } from '@@/Tip/TextTip';
|
import { TextTip } from '@@/Tip/TextTip';
|
||||||
|
import { TagSelector } from '@@/TagSelector';
|
||||||
|
|
||||||
|
import { EdgeGroupsSelector } from '../../edge-stacks/components/EdgeGroupsSelector';
|
||||||
|
|
||||||
import { NomadTokenField } from './NomadTokenField';
|
import { NomadTokenField } from './NomadTokenField';
|
||||||
import { ScriptFormValues } from './types';
|
import { ScriptFormValues } from './types';
|
||||||
|
@ -11,16 +16,36 @@ import { ScriptFormValues } from './types';
|
||||||
interface Props {
|
interface Props {
|
||||||
isNomadTokenVisible?: boolean;
|
isNomadTokenVisible?: boolean;
|
||||||
hideIdGetter?: boolean;
|
hideIdGetter?: boolean;
|
||||||
|
showMetaFields?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EdgeScriptSettingsFieldset({
|
export function EdgeScriptSettingsFieldset({
|
||||||
isNomadTokenVisible,
|
isNomadTokenVisible,
|
||||||
hideIdGetter,
|
hideIdGetter,
|
||||||
|
showMetaFields,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { values, setFieldValue } = useFormikContext<ScriptFormValues>();
|
const { values, setFieldValue } = useFormikContext<ScriptFormValues>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{showMetaFields && (
|
||||||
|
<>
|
||||||
|
<GroupField name="group" />
|
||||||
|
|
||||||
|
<EdgeGroupsSelector
|
||||||
|
value={values.edgeGroupsIds}
|
||||||
|
onChange={(value) => setFieldValue('edgeGroupsIds', value)}
|
||||||
|
isGroupVisible={(group) => !group.Dynamic}
|
||||||
|
horizontal
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TagSelector
|
||||||
|
value={values.tagsIds}
|
||||||
|
onChange={(value) => setFieldValue('tagsIds', value)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{!hideIdGetter && (
|
{!hideIdGetter && (
|
||||||
<>
|
<>
|
||||||
<FormControl
|
<FormControl
|
||||||
|
|
|
@ -52,14 +52,6 @@ export const commandsTabs: Record<string, CommandTab> = {
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
function buildDockerEnvVars(envVars: string, defaultVars: string[]) {
|
|
||||||
const vars = defaultVars.concat(
|
|
||||||
envVars.split(',').filter((s) => s.length > 0)
|
|
||||||
);
|
|
||||||
|
|
||||||
return vars.map((s) => `-e ${s}`).join(' \\\n ');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildLinuxStandaloneCommand(
|
export function buildLinuxStandaloneCommand(
|
||||||
agentVersion: string,
|
agentVersion: string,
|
||||||
edgeKey: string,
|
edgeKey: string,
|
||||||
|
@ -70,16 +62,16 @@ export function buildLinuxStandaloneCommand(
|
||||||
) {
|
) {
|
||||||
const { allowSelfSignedCertificates, edgeIdGenerator, envVars } = properties;
|
const { allowSelfSignedCertificates, edgeIdGenerator, envVars } = properties;
|
||||||
|
|
||||||
const env = buildDockerEnvVars(
|
const env = buildDockerEnvVars(envVars, [
|
||||||
envVars,
|
...buildDefaultDockerEnvVars(
|
||||||
buildDefaultEnvVars(
|
|
||||||
edgeKey,
|
edgeKey,
|
||||||
allowSelfSignedCertificates,
|
allowSelfSignedCertificates,
|
||||||
!edgeIdGenerator ? edgeId : undefined,
|
!edgeIdGenerator ? edgeId : undefined,
|
||||||
agentSecret,
|
agentSecret,
|
||||||
useAsyncMode
|
useAsyncMode
|
||||||
)
|
),
|
||||||
);
|
...metaEnvVars(properties),
|
||||||
|
]);
|
||||||
|
|
||||||
return `${
|
return `${
|
||||||
edgeIdGenerator ? `PORTAINER_EDGE_ID=$(${edgeIdGenerator}) \n\n` : ''
|
edgeIdGenerator ? `PORTAINER_EDGE_ID=$(${edgeIdGenerator}) \n\n` : ''
|
||||||
|
@ -106,16 +98,16 @@ export function buildWindowsStandaloneCommand(
|
||||||
) {
|
) {
|
||||||
const { allowSelfSignedCertificates, edgeIdGenerator, envVars } = properties;
|
const { allowSelfSignedCertificates, edgeIdGenerator, envVars } = properties;
|
||||||
|
|
||||||
const env = buildDockerEnvVars(
|
const env = buildDockerEnvVars(envVars, [
|
||||||
envVars,
|
...buildDefaultDockerEnvVars(
|
||||||
buildDefaultEnvVars(
|
|
||||||
edgeKey,
|
edgeKey,
|
||||||
allowSelfSignedCertificates,
|
allowSelfSignedCertificates,
|
||||||
edgeIdGenerator ? '$Env:PORTAINER_EDGE_ID' : edgeId,
|
edgeIdGenerator ? '$Env:PORTAINER_EDGE_ID' : edgeId,
|
||||||
agentSecret,
|
agentSecret,
|
||||||
useAsyncMode
|
useAsyncMode
|
||||||
)
|
),
|
||||||
);
|
...metaEnvVars(properties),
|
||||||
|
]);
|
||||||
|
|
||||||
return `${
|
return `${
|
||||||
edgeIdGenerator
|
edgeIdGenerator
|
||||||
|
@ -144,7 +136,7 @@ export function buildLinuxSwarmCommand(
|
||||||
const { allowSelfSignedCertificates, edgeIdGenerator, envVars } = properties;
|
const { allowSelfSignedCertificates, edgeIdGenerator, envVars } = properties;
|
||||||
|
|
||||||
const env = buildDockerEnvVars(envVars, [
|
const env = buildDockerEnvVars(envVars, [
|
||||||
...buildDefaultEnvVars(
|
...buildDefaultDockerEnvVars(
|
||||||
edgeKey,
|
edgeKey,
|
||||||
allowSelfSignedCertificates,
|
allowSelfSignedCertificates,
|
||||||
!edgeIdGenerator ? edgeId : undefined,
|
!edgeIdGenerator ? edgeId : undefined,
|
||||||
|
@ -152,6 +144,7 @@ export function buildLinuxSwarmCommand(
|
||||||
useAsyncMode
|
useAsyncMode
|
||||||
),
|
),
|
||||||
'AGENT_CLUSTER_ADDR=tasks.portainer_edge_agent',
|
'AGENT_CLUSTER_ADDR=tasks.portainer_edge_agent',
|
||||||
|
...metaEnvVars(properties),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return `${
|
return `${
|
||||||
|
@ -186,7 +179,7 @@ export function buildWindowsSwarmCommand(
|
||||||
const { allowSelfSignedCertificates, edgeIdGenerator, envVars } = properties;
|
const { allowSelfSignedCertificates, edgeIdGenerator, envVars } = properties;
|
||||||
|
|
||||||
const env = buildDockerEnvVars(envVars, [
|
const env = buildDockerEnvVars(envVars, [
|
||||||
...buildDefaultEnvVars(
|
...buildDefaultDockerEnvVars(
|
||||||
edgeKey,
|
edgeKey,
|
||||||
allowSelfSignedCertificates,
|
allowSelfSignedCertificates,
|
||||||
edgeIdGenerator ? '$Env:PORTAINER_EDGE_ID' : edgeId,
|
edgeIdGenerator ? '$Env:PORTAINER_EDGE_ID' : edgeId,
|
||||||
|
@ -194,6 +187,7 @@ export function buildWindowsSwarmCommand(
|
||||||
useAsyncMode
|
useAsyncMode
|
||||||
),
|
),
|
||||||
'AGENT_CLUSTER_ADDR=tasks.portainer_edge_agent',
|
'AGENT_CLUSTER_ADDR=tasks.portainer_edge_agent',
|
||||||
|
...metaEnvVars(properties),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return `${
|
return `${
|
||||||
|
@ -229,17 +223,18 @@ export function buildLinuxKubernetesCommand(
|
||||||
const { allowSelfSignedCertificates, edgeIdGenerator, envVars } = properties;
|
const { allowSelfSignedCertificates, edgeIdGenerator, envVars } = properties;
|
||||||
|
|
||||||
const agentShortVersion = getAgentShortVersion(agentVersion);
|
const agentShortVersion = getAgentShortVersion(agentVersion);
|
||||||
let envVarsTrimmed = envVars.trim();
|
const allEnvVars = buildEnvVars(
|
||||||
if (useAsyncMode) {
|
envVars,
|
||||||
envVarsTrimmed += `EDGE_ASYNC=1`;
|
_.compact([useAsyncMode && 'EDGE_ASYNC=1', ...metaEnvVars(properties)])
|
||||||
}
|
);
|
||||||
|
|
||||||
const idEnvVar = edgeIdGenerator
|
const idEnvVar = edgeIdGenerator
|
||||||
? `PORTAINER_EDGE_ID=$(${edgeIdGenerator}) \n\n`
|
? `PORTAINER_EDGE_ID=$(${edgeIdGenerator}) \n\n`
|
||||||
: '';
|
: '';
|
||||||
const edgeIdVar = !edgeIdGenerator && edgeId ? edgeId : '$PORTAINER_EDGE_ID';
|
const edgeIdVar = !edgeIdGenerator && edgeId ? edgeId : '$PORTAINER_EDGE_ID';
|
||||||
const selfSigned = allowSelfSignedCertificates ? '1' : '0';
|
const selfSigned = allowSelfSignedCertificates ? '1' : '0';
|
||||||
|
|
||||||
return `${idEnvVar}curl https://downloads.portainer.io/ee${agentShortVersion}/portainer-edge-agent-setup.sh | bash -s -- "${edgeIdVar}" "${edgeKey}" "${selfSigned}" "${agentSecret}" "${envVarsTrimmed}"`;
|
return `${idEnvVar}curl https://downloads.portainer.io/ee${agentShortVersion}/portainer-edge-agent-setup.sh | bash -s -- "${edgeIdVar}" "${edgeKey}" "${selfSigned}" "${agentSecret}" "${allEnvVars}"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildLinuxNomadCommand(
|
export function buildLinuxNomadCommand(
|
||||||
|
@ -259,10 +254,11 @@ export function buildLinuxNomadCommand(
|
||||||
} = properties;
|
} = properties;
|
||||||
|
|
||||||
const agentShortVersion = getAgentShortVersion(agentVersion);
|
const agentShortVersion = getAgentShortVersion(agentVersion);
|
||||||
let envVarsTrimmed = envVars.trim();
|
|
||||||
if (useAsyncMode) {
|
const allEnvVars = buildEnvVars(
|
||||||
envVarsTrimmed += `EDGE_ASYNC=1`;
|
envVars,
|
||||||
}
|
_.compact([useAsyncMode && 'EDGE_ASYNC=1', ...metaEnvVars(properties)])
|
||||||
|
);
|
||||||
|
|
||||||
const selfSigned = allowSelfSignedCertificates ? '1' : '0';
|
const selfSigned = allowSelfSignedCertificates ? '1' : '0';
|
||||||
const idEnvVar = edgeIdGenerator
|
const idEnvVar = edgeIdGenerator
|
||||||
|
@ -270,10 +266,16 @@ export function buildLinuxNomadCommand(
|
||||||
: '';
|
: '';
|
||||||
const edgeIdVar = !edgeIdGenerator && edgeId ? edgeId : '$PORTAINER_EDGE_ID';
|
const edgeIdVar = !edgeIdGenerator && edgeId ? edgeId : '$PORTAINER_EDGE_ID';
|
||||||
|
|
||||||
return `${idEnvVar}curl https://downloads.portainer.io/ee${agentShortVersion}/portainer-edge-agent-nomad-setup.sh | bash -s -- "${nomadToken}" "${edgeIdVar}" "${edgeKey}" "${selfSigned}" "${envVarsTrimmed}" "${agentSecret}" "${tlsEnabled}"`;
|
return `${idEnvVar}curl https://downloads.portainer.io/ee${agentShortVersion}/portainer-edge-agent-nomad-setup.sh | bash -s -- "${nomadToken}" "${edgeIdVar}" "${edgeKey}" "${selfSigned}" "${allEnvVars}" "${agentSecret}" "${tlsEnabled}"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildDefaultEnvVars(
|
function buildDockerEnvVars(envVars: string, moreVars: string[]) {
|
||||||
|
const vars = moreVars.concat(envVars.split(',').filter((s) => s.length > 0));
|
||||||
|
|
||||||
|
return vars.map((s) => `-e ${s}`).join(' \\\n ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDefaultDockerEnvVars(
|
||||||
edgeKey: string,
|
edgeKey: string,
|
||||||
allowSelfSignedCerts: boolean,
|
allowSelfSignedCerts: boolean,
|
||||||
edgeId = '$PORTAINER_EDGE_ID',
|
edgeId = '$PORTAINER_EDGE_ID',
|
||||||
|
@ -289,3 +291,22 @@ function buildDefaultEnvVars(
|
||||||
useAsyncMode ? 'EDGE_ASYNC=1' : '',
|
useAsyncMode ? 'EDGE_ASYNC=1' : '',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ENV_VAR_SEPARATOR = ',';
|
||||||
|
const VAR_LIST_SEPARATOR = ':';
|
||||||
|
function buildEnvVars(envVars: string, moreVars: string[]) {
|
||||||
|
return _.compact([envVars.trim(), ...moreVars]).join(ENV_VAR_SEPARATOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
function metaEnvVars({
|
||||||
|
edgeGroupsIds,
|
||||||
|
group,
|
||||||
|
tagsIds,
|
||||||
|
}: Pick<ScriptFormValues, 'edgeGroupsIds' | 'tagsIds' | 'group'>) {
|
||||||
|
return _.compact([
|
||||||
|
edgeGroupsIds.length &&
|
||||||
|
`EDGE_GROUPS=${edgeGroupsIds.join(VAR_LIST_SEPARATOR)}`,
|
||||||
|
group && `PORTAINER_GROUP=${group}`,
|
||||||
|
tagsIds.length && `PORTAINER_TAGS=${tagsIds.join(VAR_LIST_SEPARATOR)}`,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
import { TagId } from '@/portainer/tags/types';
|
||||||
|
import { EnvironmentGroupId } from '@/react/portainer/environments/environment-groups/types';
|
||||||
|
|
||||||
|
import { EdgeGroup } from '../../edge-groups/types';
|
||||||
|
|
||||||
export type Platform = 'standalone' | 'swarm' | 'k8s' | 'nomad';
|
export type Platform = 'standalone' | 'swarm' | 'k8s' | 'nomad';
|
||||||
export type OS = 'win' | 'linux';
|
export type OS = 'win' | 'linux';
|
||||||
|
|
||||||
|
@ -13,6 +18,10 @@ export interface ScriptFormValues {
|
||||||
platform: Platform;
|
platform: Platform;
|
||||||
|
|
||||||
edgeIdGenerator?: string;
|
edgeIdGenerator?: string;
|
||||||
|
|
||||||
|
group: EnvironmentGroupId;
|
||||||
|
edgeGroupsIds: Array<EdgeGroup['Id']>;
|
||||||
|
tagsIds: Array<TagId>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EdgeInfo {
|
export interface EdgeInfo {
|
||||||
|
|
|
@ -12,27 +12,24 @@ import { useSearchBarState } from '@@/datatables/SearchBar';
|
||||||
import { useAssociateDeviceMutation, useLicenseOverused } from '../queries';
|
import { useAssociateDeviceMutation, useLicenseOverused } from '../queries';
|
||||||
|
|
||||||
import { columns } from './columns';
|
import { columns } from './columns';
|
||||||
|
import { Filter } from './Filter';
|
||||||
|
import { useEnvironments } from './useEnvironments';
|
||||||
|
|
||||||
const storageKey = 'edge-devices-waiting-room';
|
const storageKey = 'edge-devices-waiting-room';
|
||||||
|
|
||||||
const settingsStore = createPersistedStore(storageKey, 'Name');
|
const settingsStore = createPersistedStore(storageKey, 'Name');
|
||||||
|
|
||||||
interface Props {
|
export function Datatable() {
|
||||||
devices: Environment[];
|
|
||||||
isLoading: boolean;
|
|
||||||
totalCount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Datatable({ devices, isLoading, totalCount }: Props) {
|
|
||||||
const associateMutation = useAssociateDeviceMutation();
|
const associateMutation = useAssociateDeviceMutation();
|
||||||
const licenseOverused = useLicenseOverused();
|
const licenseOverused = useLicenseOverused();
|
||||||
const settings = useStore(settingsStore);
|
const settings = useStore(settingsStore);
|
||||||
const [search, setSearch] = useSearchBarState(storageKey);
|
const [search, setSearch] = useSearchBarState(storageKey);
|
||||||
|
const { data: environments, totalCount, isLoading } = useEnvironments();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GenericDatatable
|
<GenericDatatable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataset={devices}
|
dataset={environments}
|
||||||
initialPageSize={settings.pageSize}
|
initialPageSize={settings.pageSize}
|
||||||
onPageSizeChange={settings.setPageSize}
|
onPageSizeChange={settings.setPageSize}
|
||||||
initialSortBy={settings.sortBy}
|
initialSortBy={settings.sortBy}
|
||||||
|
@ -62,6 +59,7 @@ export function Datatable({ devices, isLoading, totalCount }: Props) {
|
||||||
)}
|
)}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
totalCount={totalCount}
|
totalCount={totalCount}
|
||||||
|
description={<Filter />}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { HomepageFilter } from '@/react/portainer/HomeView/EnvironmentList/HomepageFilter';
|
||||||
|
import { useGroups } from '@/react/portainer/environments/environment-groups/queries';
|
||||||
|
import { useEdgeGroups } from '@/react/edge/edge-groups/queries/useEdgeGroups';
|
||||||
|
import { useTags } from '@/portainer/tags/queries';
|
||||||
|
|
||||||
|
import { useFilterStore } from './filter-store';
|
||||||
|
|
||||||
|
export function Filter() {
|
||||||
|
const edgeGroupsQuery = useEdgeGroups();
|
||||||
|
const groupsQuery = useGroups();
|
||||||
|
const tagsQuery = useTags();
|
||||||
|
|
||||||
|
const filterStore = useFilterStore();
|
||||||
|
|
||||||
|
if (!edgeGroupsQuery.data || !groupsQuery.data || !tagsQuery.data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex w-full gap-5 [&>*]:w-1/5">
|
||||||
|
<HomepageFilter
|
||||||
|
onChange={(f) => filterStore.setEdgeGroups(f)}
|
||||||
|
placeHolder="Edge groups"
|
||||||
|
value={filterStore.edgeGroups}
|
||||||
|
filterOptions={edgeGroupsQuery.data.map((g) => ({
|
||||||
|
label: g.Name,
|
||||||
|
value: g.Id,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
<HomepageFilter
|
||||||
|
onChange={(f) => filterStore.setGroups(f)}
|
||||||
|
placeHolder="Group"
|
||||||
|
value={filterStore.groups}
|
||||||
|
filterOptions={groupsQuery.data.map((g) => ({
|
||||||
|
label: g.Name,
|
||||||
|
value: g.Id,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
<HomepageFilter
|
||||||
|
onChange={(f) => filterStore.setTags(f)}
|
||||||
|
placeHolder="Tags"
|
||||||
|
value={filterStore.tags}
|
||||||
|
filterOptions={tagsQuery.data.map((g) => ({
|
||||||
|
label: g.Name,
|
||||||
|
value: g.ID,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
import { Column } from 'react-table';
|
import { CellProps, Column } from 'react-table';
|
||||||
|
|
||||||
import { Environment } from '@/react/portainer/environments/types';
|
import { WaitingRoomEnvironment } from '../types';
|
||||||
|
|
||||||
export const columns: readonly Column<Environment>[] = [
|
export const columns: readonly Column<WaitingRoomEnvironment>[] = [
|
||||||
{
|
{
|
||||||
Header: 'Name',
|
Header: 'Name',
|
||||||
accessor: (row) => row.Name,
|
accessor: (row) => row.Name,
|
||||||
|
@ -21,4 +21,35 @@ export const columns: readonly Column<Environment>[] = [
|
||||||
canHide: false,
|
canHide: false,
|
||||||
sortType: 'string',
|
sortType: 'string',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Header: 'Edge Groups',
|
||||||
|
accessor: (row) => row.EdgeGroups || [],
|
||||||
|
Cell: ({ value }: CellProps<WaitingRoomEnvironment, string[]>) =>
|
||||||
|
value.join(', ') || '-',
|
||||||
|
id: 'edge-groups',
|
||||||
|
disableFilters: true,
|
||||||
|
Filter: () => null,
|
||||||
|
canHide: false,
|
||||||
|
sortType: 'string',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Group',
|
||||||
|
accessor: (row) => row.Group || '-',
|
||||||
|
id: 'group',
|
||||||
|
disableFilters: true,
|
||||||
|
Filter: () => null,
|
||||||
|
canHide: false,
|
||||||
|
sortType: 'string',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Tags',
|
||||||
|
accessor: (row) => row.Tags || [],
|
||||||
|
Cell: ({ value }: CellProps<WaitingRoomEnvironment, string[]>) =>
|
||||||
|
value.join(', ') || '-',
|
||||||
|
id: 'tags',
|
||||||
|
disableFilters: true,
|
||||||
|
Filter: () => null,
|
||||||
|
canHide: false,
|
||||||
|
sortType: 'string',
|
||||||
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
import createStore from 'zustand';
|
||||||
|
import { persist } from 'zustand/middleware';
|
||||||
|
|
||||||
|
import { keyBuilder } from '@/react/hooks/useLocalStorage';
|
||||||
|
|
||||||
|
interface TableFiltersStore {
|
||||||
|
groups: number[];
|
||||||
|
setGroups(value: number[]): void;
|
||||||
|
edgeGroups: number[];
|
||||||
|
setEdgeGroups(value: number[]): void;
|
||||||
|
tags: number[];
|
||||||
|
setTags(value: number[]): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useFilterStore = createStore<TableFiltersStore>()(
|
||||||
|
persist(
|
||||||
|
(set) => ({
|
||||||
|
edgeGroups: [],
|
||||||
|
setEdgeGroups(edgeGroups: number[]) {
|
||||||
|
set({ edgeGroups });
|
||||||
|
},
|
||||||
|
groups: [],
|
||||||
|
setGroups(groups: number[]) {
|
||||||
|
set({ groups });
|
||||||
|
},
|
||||||
|
tags: [],
|
||||||
|
setTags(tags: number[]) {
|
||||||
|
set({ tags });
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: keyBuilder('edge-devices-meta-filters'),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
|
@ -0,0 +1,74 @@
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import { useTags } from '@/portainer/tags/queries';
|
||||||
|
import { useEdgeGroups } from '@/react/edge/edge-groups/queries/useEdgeGroups';
|
||||||
|
import { useGroups } from '@/react/portainer/environments/environment-groups/queries';
|
||||||
|
import { useEnvironmentList } from '@/react/portainer/environments/queries';
|
||||||
|
import { EdgeTypes } from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
|
import { WaitingRoomEnvironment } from '../types';
|
||||||
|
|
||||||
|
import { useFilterStore } from './filter-store';
|
||||||
|
|
||||||
|
export function useEnvironments() {
|
||||||
|
const filterStore = useFilterStore();
|
||||||
|
const edgeGroupsQuery = useEdgeGroups();
|
||||||
|
|
||||||
|
const filterByEnvironmentsIds = filterStore.edgeGroups.length
|
||||||
|
? _.compact(
|
||||||
|
filterStore.edgeGroups.flatMap(
|
||||||
|
(groupId) =>
|
||||||
|
edgeGroupsQuery.data?.find((g) => g.Id === groupId)?.Endpoints
|
||||||
|
)
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const environmentsQuery = useEnvironmentList({
|
||||||
|
edgeDeviceUntrusted: true,
|
||||||
|
excludeSnapshots: true,
|
||||||
|
types: EdgeTypes,
|
||||||
|
tagIds: filterStore.tags.length ? filterStore.tags : undefined,
|
||||||
|
groupIds: filterStore.groups.length ? filterStore.groups : undefined,
|
||||||
|
endpointIds: filterByEnvironmentsIds,
|
||||||
|
});
|
||||||
|
|
||||||
|
const groupsQuery = useGroups({
|
||||||
|
select: (groups) =>
|
||||||
|
Object.fromEntries(groups.map((g) => [g.Id, g.Name] as const)),
|
||||||
|
});
|
||||||
|
const environmentEdgeGroupsQuery = useEdgeGroups({
|
||||||
|
select: (groups) =>
|
||||||
|
_.groupBy(
|
||||||
|
groups.flatMap((group) => {
|
||||||
|
const envs = group.Endpoints;
|
||||||
|
return envs.map((id) => ({ id, group: group.Name }));
|
||||||
|
}),
|
||||||
|
(env) => env.id
|
||||||
|
),
|
||||||
|
});
|
||||||
|
const tagsQuery = useTags({
|
||||||
|
select: (tags) =>
|
||||||
|
Object.fromEntries(tags.map((tag) => [tag.ID, tag.Name] as const)),
|
||||||
|
});
|
||||||
|
|
||||||
|
const envs: Array<WaitingRoomEnvironment> =
|
||||||
|
environmentsQuery.environments.map((env) => ({
|
||||||
|
...env,
|
||||||
|
Group: groupsQuery.data?.[env.GroupId] || '',
|
||||||
|
EdgeGroups:
|
||||||
|
environmentEdgeGroupsQuery.data?.[env.Id]?.map((env) => env.group) ||
|
||||||
|
[],
|
||||||
|
Tags:
|
||||||
|
_.compact(env.TagIds?.map((tagId) => tagsQuery.data?.[tagId])) || [],
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: envs,
|
||||||
|
isLoading:
|
||||||
|
environmentsQuery.isLoading ||
|
||||||
|
groupsQuery.isLoading ||
|
||||||
|
environmentEdgeGroupsQuery.isLoading ||
|
||||||
|
tagsQuery.isLoading,
|
||||||
|
totalCount: environmentsQuery.totalCount,
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,5 +1,3 @@
|
||||||
import { useEnvironmentList } from '@/react/portainer/environments/queries/useEnvironmentList';
|
|
||||||
import { EdgeTypes } from '@/react/portainer/environments/types';
|
|
||||||
import { withLimitToBE } from '@/react/hooks/useLimitToBE';
|
import { withLimitToBE } from '@/react/hooks/useLimitToBE';
|
||||||
|
|
||||||
import { InformationPanel } from '@@/InformationPanel';
|
import { InformationPanel } from '@@/InformationPanel';
|
||||||
|
@ -11,12 +9,6 @@ import { Datatable } from './Datatable';
|
||||||
export default withLimitToBE(WaitingRoomView);
|
export default withLimitToBE(WaitingRoomView);
|
||||||
|
|
||||||
function WaitingRoomView() {
|
function WaitingRoomView() {
|
||||||
const { environments, isLoading, totalCount } = useEnvironmentList({
|
|
||||||
edgeDeviceUntrusted: true,
|
|
||||||
excludeSnapshots: true,
|
|
||||||
types: EdgeTypes,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
|
@ -35,11 +27,7 @@ function WaitingRoomView() {
|
||||||
</TextTip>
|
</TextTip>
|
||||||
</InformationPanel>
|
</InformationPanel>
|
||||||
|
|
||||||
<Datatable
|
<Datatable />
|
||||||
devices={environments}
|
|
||||||
totalCount={totalCount}
|
|
||||||
isLoading={isLoading}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { Environment } from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
|
export type WaitingRoomEnvironment = Environment & {
|
||||||
|
EdgeGroups: string[];
|
||||||
|
Tags: string[];
|
||||||
|
Group: string;
|
||||||
|
};
|
|
@ -1,22 +1,29 @@
|
||||||
import { useQuery } from 'react-query';
|
import { useQuery } from 'react-query';
|
||||||
|
|
||||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||||
|
import { EnvironmentType } from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
import { EdgeGroup } from '../types';
|
import { EdgeGroup } from '../types';
|
||||||
|
|
||||||
|
interface EdgeGroupListItemResponse extends EdgeGroup {
|
||||||
|
EndpointTypes: Array<EnvironmentType>;
|
||||||
|
}
|
||||||
|
|
||||||
async function getEdgeGroups() {
|
async function getEdgeGroups() {
|
||||||
try {
|
try {
|
||||||
const { data } = await axios.get<EdgeGroup[]>('/edge_groups');
|
const { data } = await axios.get<EdgeGroupListItemResponse[]>(
|
||||||
|
'/edge_groups'
|
||||||
|
);
|
||||||
return data;
|
return data;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw parseAxiosError(err as Error, 'Failed fetching edge groups');
|
throw parseAxiosError(err as Error, 'Failed fetching edge groups');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useEdgeGroups<T = EdgeGroup[]>({
|
export function useEdgeGroups<T = EdgeGroupListItemResponse[]>({
|
||||||
select,
|
select,
|
||||||
}: {
|
}: {
|
||||||
select?: (groups: EdgeGroup[]) => T;
|
select?: (groups: EdgeGroupListItemResponse[]) => T;
|
||||||
} = {}) {
|
} = {}) {
|
||||||
return useQuery(['edge', 'groups'], getEdgeGroups, { select });
|
return useQuery(['edge', 'groups'], getEdgeGroups, { select });
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,21 +3,78 @@ import _ from 'lodash';
|
||||||
import { EdgeGroup } from '@/react/edge/edge-groups/types';
|
import { EdgeGroup } from '@/react/edge/edge-groups/types';
|
||||||
|
|
||||||
import { Select } from '@@/form-components/ReactSelect';
|
import { Select } from '@@/form-components/ReactSelect';
|
||||||
|
import { FormSection } from '@@/form-components/FormSection';
|
||||||
|
import { FormError } from '@@/form-components/FormError';
|
||||||
|
import { Link } from '@@/Link';
|
||||||
|
import { FormControl } from '@@/form-components/FormControl';
|
||||||
|
|
||||||
|
import { useEdgeGroups } from '../../edge-groups/queries/useEdgeGroups';
|
||||||
|
|
||||||
type SingleValue = EdgeGroup['Id'];
|
type SingleValue = EdgeGroup['Id'];
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
items: EdgeGroup[];
|
|
||||||
value: SingleValue[];
|
value: SingleValue[];
|
||||||
onChange: (value: SingleValue[]) => void;
|
onChange: (value: SingleValue[]) => void;
|
||||||
|
error?: string | string[];
|
||||||
|
horizontal?: boolean;
|
||||||
|
isGroupVisible?(group: EdgeGroup): boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EdgeGroupsSelector({ items, value, onChange }: Props) {
|
export function EdgeGroupsSelector({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
error,
|
||||||
|
horizontal,
|
||||||
|
isGroupVisible = () => true,
|
||||||
|
}: Props) {
|
||||||
|
const selector = (
|
||||||
|
<InnerSelector
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
isGroupVisible={isGroupVisible}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return horizontal ? (
|
||||||
|
<FormControl errors={error} label="Edge Groups">
|
||||||
|
{selector}
|
||||||
|
</FormControl>
|
||||||
|
) : (
|
||||||
|
<FormSection title="Edge Groups">
|
||||||
|
<div className="form-group">
|
||||||
|
<div className="col-sm-12">{selector} </div>
|
||||||
|
{error && (
|
||||||
|
<div className="col-sm-12">
|
||||||
|
<FormError>{error}</FormError>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</FormSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function InnerSelector({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
isGroupVisible,
|
||||||
|
}: {
|
||||||
|
isGroupVisible(group: EdgeGroup): boolean;
|
||||||
|
value: SingleValue[];
|
||||||
|
onChange: (value: SingleValue[]) => void;
|
||||||
|
}) {
|
||||||
|
const edgeGroupsQuery = useEdgeGroups();
|
||||||
|
|
||||||
|
if (!edgeGroupsQuery.data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = edgeGroupsQuery.data.filter(isGroupVisible);
|
||||||
|
|
||||||
const valueGroups = _.compact(
|
const valueGroups = _.compact(
|
||||||
value.map((id) => items.find((item) => item.Id === id))
|
value.map((id) => items.find((item) => item.Id === id))
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return items.length ? (
|
||||||
<Select
|
<Select
|
||||||
aria-label="Edge groups"
|
aria-label="Edge groups"
|
||||||
options={items}
|
options={items}
|
||||||
|
@ -31,5 +88,10 @@ export function EdgeGroupsSelector({ items, value, onChange }: Props) {
|
||||||
placeholder="Select one or multiple group(s)"
|
placeholder="Select one or multiple group(s)"
|
||||||
closeMenuOnSelect={false}
|
closeMenuOnSelect={false}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="small text-muted">
|
||||||
|
No Edge groups are available. Head over to the{' '}
|
||||||
|
<Link to="edge.groups">Edge groups view</Link> to create one.
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -130,17 +130,21 @@ export function EnvironmentItem({
|
||||||
}
|
}
|
||||||
|
|
||||||
function useEnvironmentTagNames(tagIds?: TagId[]) {
|
function useEnvironmentTagNames(tagIds?: TagId[]) {
|
||||||
const { tags, isLoading } = useTags((tags) => {
|
const tagsQuery = useTags({
|
||||||
if (!tagIds) {
|
select: (tags) => {
|
||||||
return [];
|
if (!tagIds) {
|
||||||
}
|
return [];
|
||||||
return _.compact(
|
}
|
||||||
tagIds
|
return _.compact(
|
||||||
.map((id) => tags.find((tag) => tag.ID === id))
|
tagIds
|
||||||
.map((tag) => tag?.Name)
|
.map((id) => tags.find((tag) => tag.ID === id))
|
||||||
);
|
.map((tag) => tag?.Name)
|
||||||
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data: tags, isLoading } = tagsQuery;
|
||||||
|
|
||||||
if (tags && tags.length > 0) {
|
if (tags && tags.length > 0) {
|
||||||
return tags.join(', ');
|
return tags.join(', ');
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,7 @@ import { PaginationControls } from '@@/PaginationControls';
|
||||||
import { SearchBar, useSearchBarState } from '@@/datatables/SearchBar';
|
import { SearchBar, useSearchBarState } from '@@/datatables/SearchBar';
|
||||||
|
|
||||||
import { useHomePageFilter } from './HomepageFilter';
|
import { useHomePageFilter } from './HomepageFilter';
|
||||||
import { ConnectionType, Filter } from './types';
|
import { ConnectionType } from './types';
|
||||||
import { EnvironmentItem } from './EnvironmentItem';
|
import { EnvironmentItem } from './EnvironmentItem';
|
||||||
import { KubeconfigButton } from './KubeconfigButton';
|
import { KubeconfigButton } from './KubeconfigButton';
|
||||||
import { NoEnvironmentsInfoPanel } from './NoEnvironmentsInfoPanel';
|
import { NoEnvironmentsInfoPanel } from './NoEnvironmentsInfoPanel';
|
||||||
|
@ -48,15 +48,16 @@ export function EnvironmentList({ onClickBrowse, onRefresh }: Props) {
|
||||||
const { isAdmin } = useUser();
|
const { isAdmin } = useUser();
|
||||||
const currentEnvStore = useStore(environmentStore);
|
const currentEnvStore = useStore(environmentStore);
|
||||||
|
|
||||||
const [platformTypes, setPlatformTypes] = useHomePageFilter<
|
const [platformTypes, setPlatformTypes] = useHomePageFilter<PlatformType[]>(
|
||||||
Filter<PlatformType>[]
|
'platformType',
|
||||||
>('platformType', []);
|
[]
|
||||||
|
);
|
||||||
const [searchBarValue, setSearchBarValue] = useSearchBarState(storageKey);
|
const [searchBarValue, setSearchBarValue] = useSearchBarState(storageKey);
|
||||||
const [pageLimit, setPageLimit] = usePaginationLimitState(storageKey);
|
const [pageLimit, setPageLimit] = usePaginationLimitState(storageKey);
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
|
|
||||||
const [connectionTypes, setConnectionTypes] = useHomePageFilter<
|
const [connectionTypes, setConnectionTypes] = useHomePageFilter<
|
||||||
Filter<ConnectionType>[]
|
ConnectionType[]
|
||||||
>('connectionTypes', []);
|
>('connectionTypes', []);
|
||||||
|
|
||||||
const [statusFilter, setStatusFilter] = useHomePageFilter<
|
const [statusFilter, setStatusFilter] = useHomePageFilter<
|
||||||
|
@ -77,20 +78,17 @@ export function EnvironmentList({ onClickBrowse, onRefresh }: Props) {
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
const [statusState, setStatusState] = useHomePageFilter<Filter[]>(
|
const [statusState, setStatusState] = useHomePageFilter<number[]>(
|
||||||
'status_state',
|
'status_state',
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
const [tagState, setTagState] = useHomePageFilter<Filter[]>('tag_state', []);
|
const [tagState, setTagState] = useHomePageFilter<number[]>('tag_state', []);
|
||||||
const [groupState, setGroupState] = useHomePageFilter<Filter[]>(
|
const [groupState, setGroupState] = useHomePageFilter<number[]>(
|
||||||
'group_state',
|
'group_state',
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
const [sortByState, setSortByState] = useHomePageFilter<Filter | undefined>(
|
|
||||||
'sort_by_state',
|
const [agentVersions, setAgentVersions] = useHomePageFilter<string[]>(
|
||||||
undefined
|
|
||||||
);
|
|
||||||
const [agentVersions, setAgentVersions] = useHomePageFilter<Filter<string>[]>(
|
|
||||||
'agentVersions',
|
'agentVersions',
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
@ -98,17 +96,14 @@ export function EnvironmentList({ onClickBrowse, onRefresh }: Props) {
|
||||||
const groupsQuery = useGroups();
|
const groupsQuery = useGroups();
|
||||||
|
|
||||||
const environmentsQueryParams: EnvironmentsQueryParams = {
|
const environmentsQueryParams: EnvironmentsQueryParams = {
|
||||||
types: getTypes(
|
types: getTypes(platformTypes, connectionTypes),
|
||||||
platformTypes.map((p) => p.value),
|
|
||||||
connectionTypes.map((p) => p.value)
|
|
||||||
),
|
|
||||||
search: searchBarValue,
|
search: searchBarValue,
|
||||||
status: statusFilter,
|
status: statusFilter,
|
||||||
tagIds: tagFilter?.length ? tagFilter : undefined,
|
tagIds: tagFilter?.length ? tagFilter : undefined,
|
||||||
groupIds: groupFilter,
|
groupIds: groupFilter,
|
||||||
provisioned: true,
|
provisioned: true,
|
||||||
tagsPartialMatch: true,
|
tagsPartialMatch: true,
|
||||||
agentVersions: agentVersions.map((a) => a.value),
|
agentVersions,
|
||||||
updateInformation: isBE,
|
updateInformation: isBE,
|
||||||
edgeAsync: getEdgeAsyncValue(connectionTypes),
|
edgeAsync: getEdgeAsyncValue(connectionTypes),
|
||||||
};
|
};
|
||||||
|
@ -202,11 +197,11 @@ export function EnvironmentList({ onClickBrowse, onRefresh }: Props) {
|
||||||
setAgentVersions={setAgentVersions}
|
setAgentVersions={setAgentVersions}
|
||||||
agentVersions={agentVersions}
|
agentVersions={agentVersions}
|
||||||
clearFilter={clearFilter}
|
clearFilter={clearFilter}
|
||||||
sortOnchange={sortOnchange}
|
sortOnChange={sortOnchange}
|
||||||
sortOnDescending={sortOnDescending}
|
sortOnDescending={sortOnDescending}
|
||||||
sortByDescending={sortByDescending}
|
sortByDescending={sortByDescending}
|
||||||
sortByButton={sortByButton}
|
sortByButton={sortByButton}
|
||||||
sortByState={sortByState}
|
sortByState={sortByFilter}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
@ -305,50 +300,32 @@ export function EnvironmentList({ onClickBrowse, onRefresh }: Props) {
|
||||||
return _.intersection(selectedTypesByConnection, selectedTypesByPlatform);
|
return _.intersection(selectedTypesByConnection, selectedTypesByPlatform);
|
||||||
}
|
}
|
||||||
|
|
||||||
function statusOnChange(filterOptions: Filter[]) {
|
function statusOnChange(value: number[]) {
|
||||||
setStatusState(filterOptions);
|
setStatusState(value);
|
||||||
if (filterOptions.length === 0) {
|
if (value.length === 0) {
|
||||||
setStatusFilter([]);
|
setStatusFilter([]);
|
||||||
} else {
|
} else {
|
||||||
const filteredStatus = [
|
const filteredStatus = [...new Set(value)];
|
||||||
...new Set(
|
|
||||||
filterOptions.map(
|
|
||||||
(filterOptions: { value: number }) => filterOptions.value
|
|
||||||
)
|
|
||||||
),
|
|
||||||
];
|
|
||||||
setStatusFilter(filteredStatus);
|
setStatusFilter(filteredStatus);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function groupOnChange(filterOptions: Filter[]) {
|
function groupOnChange(value: number[]) {
|
||||||
setGroupState(filterOptions);
|
setGroupState(value);
|
||||||
if (filterOptions.length === 0) {
|
if (value.length === 0) {
|
||||||
setGroupFilter([]);
|
setGroupFilter([]);
|
||||||
} else {
|
} else {
|
||||||
const filteredGroups = [
|
const filteredGroups = [...new Set(value)];
|
||||||
...new Set(
|
|
||||||
filterOptions.map(
|
|
||||||
(filterOptions: { value: number }) => filterOptions.value
|
|
||||||
)
|
|
||||||
),
|
|
||||||
];
|
|
||||||
setGroupFilter(filteredGroups);
|
setGroupFilter(filteredGroups);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function tagOnChange(filterOptions: Filter[]) {
|
function tagOnChange(value: number[]) {
|
||||||
setTagState(filterOptions);
|
setTagState(value);
|
||||||
if (filterOptions.length === 0) {
|
if (value.length === 0) {
|
||||||
setTagFilter([]);
|
setTagFilter([]);
|
||||||
} else {
|
} else {
|
||||||
const filteredTags = [
|
const filteredTags = [...new Set(value)];
|
||||||
...new Set(
|
|
||||||
filterOptions.map(
|
|
||||||
(filterOptions: { value: number }) => filterOptions.value
|
|
||||||
)
|
|
||||||
),
|
|
||||||
];
|
|
||||||
setTagFilter(filteredTags);
|
setTagFilter(filteredTags);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -365,16 +342,9 @@ export function EnvironmentList({ onClickBrowse, onRefresh }: Props) {
|
||||||
setConnectionTypes([]);
|
setConnectionTypes([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortOnchange(filterOptions: Filter) {
|
function sortOnchange(value: string) {
|
||||||
if (filterOptions !== null) {
|
setSortByFilter(value);
|
||||||
setSortByFilter(filterOptions.label);
|
setSortByButton(!!value);
|
||||||
setSortByButton(true);
|
|
||||||
setSortByState(filterOptions);
|
|
||||||
} else {
|
|
||||||
setSortByFilter('');
|
|
||||||
setSortByButton(true);
|
|
||||||
setSortByState(undefined);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortOnDescending() {
|
function sortOnDescending() {
|
||||||
|
@ -407,14 +377,13 @@ function renderItems(
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEdgeAsyncValue(connectionTypes: Filter<ConnectionType>[]) {
|
function getEdgeAsyncValue(connectionTypes: ConnectionType[]) {
|
||||||
const hasEdgeAsync = connectionTypes.some(
|
const hasEdgeAsync = connectionTypes.some(
|
||||||
(connectionType) => connectionType.value === ConnectionType.EdgeAgentAsync
|
(connectionType) => connectionType === ConnectionType.EdgeAgentAsync
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasEdgeStandard = connectionTypes.some(
|
const hasEdgeStandard = connectionTypes.some(
|
||||||
(connectionType) =>
|
(connectionType) => connectionType === ConnectionType.EdgeAgentStandard
|
||||||
connectionType.value === ConnectionType.EdgeAgentStandard
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// If both are selected, we don't want to filter on either, and same for if both are not selected
|
// 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 { HomepageFilter } from './HomepageFilter';
|
||||||
import { SortbySelector } from './SortbySelector';
|
import { SortbySelector } from './SortbySelector';
|
||||||
import { ConnectionType, Filter } from './types';
|
import { ConnectionType } from './types';
|
||||||
import styles from './EnvironmentList.module.css';
|
import styles from './EnvironmentList.module.css';
|
||||||
|
|
||||||
const status = [
|
const status = [
|
||||||
|
@ -17,11 +17,10 @@ const status = [
|
||||||
{ value: EnvironmentStatus.Down, label: 'Down' },
|
{ value: EnvironmentStatus.Down, label: 'Down' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const sortByOptions = [
|
const sortByOptions = ['Name', 'Group', 'Status'].map((v) => ({
|
||||||
{ value: 1, label: 'Name' },
|
value: v,
|
||||||
{ value: 2, label: 'Group' },
|
label: v,
|
||||||
{ value: 3, label: 'Status' },
|
}));
|
||||||
];
|
|
||||||
|
|
||||||
export function EnvironmentListFilters({
|
export function EnvironmentListFilters({
|
||||||
agentVersions,
|
agentVersions,
|
||||||
|
@ -37,32 +36,32 @@ export function EnvironmentListFilters({
|
||||||
sortByDescending,
|
sortByDescending,
|
||||||
sortByState,
|
sortByState,
|
||||||
sortOnDescending,
|
sortOnDescending,
|
||||||
sortOnchange,
|
sortOnChange,
|
||||||
statusOnChange,
|
statusOnChange,
|
||||||
statusState,
|
statusState,
|
||||||
tagOnChange,
|
tagOnChange,
|
||||||
tagState,
|
tagState,
|
||||||
}: {
|
}: {
|
||||||
platformTypes: Filter<PlatformType>[];
|
platformTypes: PlatformType[];
|
||||||
setPlatformTypes: (value: Filter<PlatformType>[]) => void;
|
setPlatformTypes: (value: PlatformType[]) => void;
|
||||||
|
|
||||||
connectionTypes: Filter<ConnectionType>[];
|
connectionTypes: ConnectionType[];
|
||||||
setConnectionTypes: (value: Filter<ConnectionType>[]) => void;
|
setConnectionTypes: (value: ConnectionType[]) => void;
|
||||||
|
|
||||||
statusState: Filter<number>[];
|
statusState: number[];
|
||||||
statusOnChange: (filterOptions: Filter[]) => void;
|
statusOnChange: (value: number[]) => void;
|
||||||
|
|
||||||
tagOnChange: (filterOptions: Filter[]) => void;
|
tagOnChange: (value: number[]) => void;
|
||||||
tagState: Filter<number>[];
|
tagState: number[];
|
||||||
|
|
||||||
groupOnChange: (filterOptions: Filter[]) => void;
|
groupOnChange: (value: number[]) => void;
|
||||||
groupState: Filter<number>[];
|
groupState: number[];
|
||||||
|
|
||||||
setAgentVersions: (value: Filter<string>[]) => void;
|
setAgentVersions: (value: string[]) => void;
|
||||||
agentVersions: Filter<string>[];
|
agentVersions: string[];
|
||||||
|
|
||||||
sortByState: Filter<number> | undefined;
|
sortByState: string;
|
||||||
sortOnchange: (filterOptions: Filter) => void;
|
sortOnChange: (value: string) => void;
|
||||||
|
|
||||||
sortOnDescending: () => void;
|
sortOnDescending: () => void;
|
||||||
sortByDescending: boolean;
|
sortByDescending: boolean;
|
||||||
|
@ -85,7 +84,7 @@ export function EnvironmentListFilters({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const tagsQuery = useTags();
|
const tagsQuery = useTags();
|
||||||
const tagOptions = [...(tagsQuery.tags || [])];
|
const tagOptions = [...(tagsQuery.data || [])];
|
||||||
const uniqueTag = [
|
const uniqueTag = [
|
||||||
...new Map(tagOptions.map((item) => [item.ID, item])).values(),
|
...new Map(tagOptions.map((item) => [item.ID, item])).values(),
|
||||||
].map(({ ID: value, Name: label }) => ({
|
].map(({ ID: value, Name: label }) => ({
|
||||||
|
@ -136,7 +135,7 @@ export function EnvironmentListFilters({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.filterLeft}>
|
<div className={styles.filterLeft}>
|
||||||
<HomepageFilter<string>
|
<HomepageFilter
|
||||||
filterOptions={
|
filterOptions={
|
||||||
agentVersionsQuery.data?.map((v) => ({
|
agentVersionsQuery.data?.map((v) => ({
|
||||||
label: v,
|
label: v,
|
||||||
|
@ -159,7 +158,7 @@ export function EnvironmentListFilters({
|
||||||
<div className={styles.filterRight}>
|
<div className={styles.filterRight}>
|
||||||
<SortbySelector
|
<SortbySelector
|
||||||
filterOptions={sortByOptions}
|
filterOptions={sortByOptions}
|
||||||
onChange={sortOnchange}
|
onChange={sortOnChange}
|
||||||
onDescending={sortOnDescending}
|
onDescending={sortOnDescending}
|
||||||
placeHolder="Sort By"
|
placeHolder="Sort By"
|
||||||
sortByDescending={sortByDescending}
|
sortByDescending={sortByDescending}
|
||||||
|
@ -171,7 +170,7 @@ export function EnvironmentListFilters({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getConnectionTypeOptions(platformTypes: Filter<PlatformType>[]) {
|
function getConnectionTypeOptions(platformTypes: PlatformType[]) {
|
||||||
const platformTypeConnectionType = {
|
const platformTypeConnectionType = {
|
||||||
[PlatformType.Docker]: [
|
[PlatformType.Docker]: [
|
||||||
ConnectionType.API,
|
ConnectionType.API,
|
||||||
|
@ -204,12 +203,12 @@ function getConnectionTypeOptions(platformTypes: Filter<PlatformType>[]) {
|
||||||
|
|
||||||
return _.compact(
|
return _.compact(
|
||||||
_.intersection(
|
_.intersection(
|
||||||
...platformTypes.map((p) => platformTypeConnectionType[p.value])
|
...platformTypes.map((p) => platformTypeConnectionType[p])
|
||||||
).map((c) => connectionTypesDefaultOptions.find((o) => o.value === c))
|
).map((c) => connectionTypesDefaultOptions.find((o) => o.value === c))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPlatformTypeOptions(connectionTypes: Filter<ConnectionType>[]) {
|
function getPlatformTypeOptions(connectionTypes: ConnectionType[]) {
|
||||||
const platformDefaultOptions = [
|
const platformDefaultOptions = [
|
||||||
{ value: PlatformType.Docker, label: 'Docker' },
|
{ value: PlatformType.Docker, label: 'Docker' },
|
||||||
{ value: PlatformType.Azure, label: 'Azure' },
|
{ value: PlatformType.Azure, label: 'Azure' },
|
||||||
|
@ -244,7 +243,7 @@ function getPlatformTypeOptions(connectionTypes: Filter<ConnectionType>[]) {
|
||||||
|
|
||||||
return _.compact(
|
return _.compact(
|
||||||
_.intersection(
|
_.intersection(
|
||||||
...connectionTypes.map((p) => connectionTypePlatformType[p.value])
|
...connectionTypes.map((p) => connectionTypePlatformType[p])
|
||||||
).map((c) => platformDefaultOptions.find((o) => o.value === c))
|
).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 { useLocalStorage } from '@/react/hooks/useLocalStorage';
|
||||||
|
|
||||||
import { Select } from '@@/form-components/ReactSelect';
|
import {
|
||||||
|
type Option as OptionType,
|
||||||
import { Filter } from './types';
|
PortainerSelect,
|
||||||
|
} from '@@/form-components/PortainerSelect';
|
||||||
|
|
||||||
interface Props<TValue = number> {
|
interface Props<TValue = number> {
|
||||||
filterOptions?: Filter<TValue>[];
|
filterOptions?: OptionType<TValue>[];
|
||||||
onChange: (filterOptions: Filter<TValue>[]) => void;
|
onChange: (value: TValue[]) => void;
|
||||||
placeHolder: string;
|
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;
|
const { isSelected, label } = props;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -21,8 +22,10 @@ function Option<TValue = number>(props: OptionProps<Filter<TValue>, true>) {
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<input type="checkbox" checked={isSelected} onChange={() => null} />{' '}
|
<div className="flex items-center gap-2">
|
||||||
<label>{label}</label>
|
<input type="checkbox" checked={isSelected} onChange={() => null} />
|
||||||
|
<label className="whitespace-nowrap">{label}</label>
|
||||||
|
</div>
|
||||||
</components.Option>
|
</components.Option>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -35,14 +38,14 @@ export function HomepageFilter<TValue = number>({
|
||||||
value,
|
value,
|
||||||
}: Props<TValue>) {
|
}: Props<TValue>) {
|
||||||
return (
|
return (
|
||||||
<Select
|
<PortainerSelect<TValue>
|
||||||
closeMenuOnSelect={false}
|
|
||||||
placeholder={placeHolder}
|
placeholder={placeHolder}
|
||||||
options={filterOptions}
|
options={filterOptions}
|
||||||
value={value}
|
value={value}
|
||||||
isMulti
|
isMulti
|
||||||
components={{ Option }}
|
components={{ Option }}
|
||||||
onChange={(option) => onChange([...option])}
|
onChange={(option) => onChange([...option])}
|
||||||
|
bindToBody
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,18 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { Select } from '@@/form-components/ReactSelect';
|
import { Option, PortainerSelect } from '@@/form-components/PortainerSelect';
|
||||||
import { TableHeaderSortIcons } from '@@/datatables/TableHeaderSortIcons';
|
import { TableHeaderSortIcons } from '@@/datatables/TableHeaderSortIcons';
|
||||||
|
|
||||||
import { Filter } from './types';
|
|
||||||
import styles from './SortbySelector.module.css';
|
import styles from './SortbySelector.module.css';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
filterOptions: Filter[];
|
filterOptions: Option<string>[];
|
||||||
onChange: (filterOptions: Filter) => void;
|
onChange: (value: string) => void;
|
||||||
onDescending: () => void;
|
onDescending: () => void;
|
||||||
placeHolder: string;
|
placeHolder: string;
|
||||||
sortByDescending: boolean;
|
sortByDescending: boolean;
|
||||||
sortByButton: boolean;
|
sortByButton: boolean;
|
||||||
value?: Filter;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SortbySelector({
|
export function SortbySelector({
|
||||||
|
@ -28,10 +27,10 @@ export function SortbySelector({
|
||||||
const sorted = sortByButton && !!value;
|
const sorted = sortByButton && !!value;
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-end gap-1">
|
<div className="flex items-center justify-end gap-1">
|
||||||
<Select
|
<PortainerSelect
|
||||||
placeholder={placeHolder}
|
placeholder={placeHolder}
|
||||||
options={filterOptions}
|
options={filterOptions}
|
||||||
onChange={(option) => onChange(option as Filter)}
|
onChange={(option) => onChange(option || '')}
|
||||||
isClearable
|
isClearable
|
||||||
value={value}
|
value={value}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -158,6 +158,7 @@ function EdgeKeyInfo({
|
||||||
commands={commands}
|
commands={commands}
|
||||||
isNomadTokenVisible
|
isNomadTokenVisible
|
||||||
asyncMode={asyncMode}
|
asyncMode={asyncMode}
|
||||||
|
showMetaFields
|
||||||
>
|
>
|
||||||
<FormControl label="Portainer API server URL">
|
<FormControl label="Portainer API server URL">
|
||||||
<Input value={url} readOnly />
|
<Input value={url} readOnly />
|
||||||
|
|
|
@ -5,8 +5,12 @@ import { error as notifyError } from '@/portainer/services/notifications';
|
||||||
import { EnvironmentGroup, EnvironmentGroupId } from './types';
|
import { EnvironmentGroup, EnvironmentGroupId } from './types';
|
||||||
import { getGroup, getGroups } from './environment-groups.service';
|
import { getGroup, getGroups } from './environment-groups.service';
|
||||||
|
|
||||||
export function useGroups() {
|
export function useGroups<T = EnvironmentGroup[]>({
|
||||||
return useQuery<EnvironmentGroup[]>(['environment-groups'], getGroups);
|
select,
|
||||||
|
}: { select?: (group: EnvironmentGroup[]) => T } = {}) {
|
||||||
|
return useQuery(['environment-groups'], getGroups, {
|
||||||
|
select,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useGroup<T = EnvironmentGroup>(
|
export function useGroup<T = EnvironmentGroup>(
|
||||||
|
|
|
@ -6,9 +6,8 @@ import { EnvironmentGroupId } from '@/react/portainer/environments/environment-g
|
||||||
import { FormControl } from '@@/form-components/FormControl';
|
import { FormControl } from '@@/form-components/FormControl';
|
||||||
import { Select } from '@@/form-components/Input';
|
import { Select } from '@@/form-components/Input';
|
||||||
|
|
||||||
export function GroupField() {
|
export function GroupField({ name = 'meta.groupId' }: { name?: string }) {
|
||||||
const [fieldProps, metaProps, helpers] =
|
const [fieldProps, metaProps, helpers] = useField<EnvironmentGroupId>(name);
|
||||||
useField<EnvironmentGroupId>('meta.groupId');
|
|
||||||
|
|
||||||
const groupsQuery = useGroups();
|
const groupsQuery = useGroups();
|
||||||
if (!groupsQuery.data) {
|
if (!groupsQuery.data) {
|
||||||
|
@ -23,7 +22,7 @@ export function GroupField() {
|
||||||
return (
|
return (
|
||||||
<FormControl label="Group" errors={metaProps.error}>
|
<FormControl label="Group" errors={metaProps.error}>
|
||||||
<Select
|
<Select
|
||||||
name="meta.groupId"
|
name={name}
|
||||||
options={options}
|
options={options}
|
||||||
value={fieldProps.value}
|
value={fieldProps.value}
|
||||||
onChange={(e) => handleChange(e.target.value)}
|
onChange={(e) => handleChange(e.target.value)}
|
||||||
|
|
Loading…
Reference in New Issue