diff --git a/api/http/handler/settings/settings_public.go b/api/http/handler/settings/settings_public.go index dcbf636ef..63436ce6a 100644 --- a/api/http/handler/settings/settings_public.go +++ b/api/http/handler/settings/settings_public.go @@ -33,6 +33,11 @@ type publicSettingsResponse struct { // Whether team sync is enabled TeamSync bool `json:"TeamSync" example:"true"` + // Whether FDO is enabled + IsFDOEnabled bool + // Whether AMT is enabled + IsAMTEnabled bool + Edge struct { // Whether the device has been started in edge async mode AsyncMode bool @@ -76,6 +81,8 @@ func generatePublicSettings(appSettings *portainer.Settings) *publicSettingsResp EnableTelemetry: appSettings.EnableTelemetry, KubeconfigExpiry: appSettings.KubeconfigExpiry, Features: appSettings.FeatureFlagSettings, + IsFDOEnabled: appSettings.EnableEdgeComputeFeatures && appSettings.FDOConfiguration.Enabled, + IsAMTEnabled: appSettings.EnableEdgeComputeFeatures && appSettings.OpenAMTConfiguration.Enabled, } publicSettings.Edge.AsyncMode = appSettings.Edge.AsyncMode diff --git a/app/docker/components/imageRegistry/por-image-registry-rate-limits.controller.js b/app/docker/components/imageRegistry/por-image-registry-rate-limits.controller.js index 809f1ba52..05c80ec63 100644 --- a/app/docker/components/imageRegistry/por-image-registry-rate-limits.controller.js +++ b/app/docker/components/imageRegistry/por-image-registry-rate-limits.controller.js @@ -1,4 +1,4 @@ -import EndpointHelper from '@/portainer/helpers/endpointHelper'; +import { isAgentEnvironment, isLocalEnvironment } from '@/react/portainer/environments/utils'; export default class porImageRegistryContainerController { /* @ngInject */ @@ -25,7 +25,7 @@ export default class porImageRegistryContainerController { async fetchRateLimits() { this.pullRateLimits = null; - if (!EndpointHelper.isAgentEndpoint(this.endpoint) && !EndpointHelper.isLocalEndpoint(this.endpoint)) { + if (!isAgentEnvironment(this.endpoint.Type) && !isLocalEnvironment(this.endpoint)) { this.setValidity(true); return; } diff --git a/app/edge/__module.js b/app/edge/__module.js index 49ceb3ce5..5406fbf2a 100644 --- a/app/edge/__module.js +++ b/app/edge/__module.js @@ -113,11 +113,7 @@ angular $stateRegistryProvider.register({ name: 'edge.devices', url: '/devices', - views: { - 'content@': { - component: 'edgeDevicesView', - }, - }, + abstract: true, }); if (process.env.PORTAINER_EDITION === 'BE') { diff --git a/app/edge/react/views/index.ts b/app/edge/react/views/index.ts index c7175f189..4397b58d8 100644 --- a/app/edge/react/views/index.ts +++ b/app/edge/react/views/index.ts @@ -1,7 +1,6 @@ import angular from 'angular'; import { r2a } from '@/react-tools/react2angular'; -import { ListView } from '@/react/edge/edge-devices/ListView'; import { withCurrentUser } from '@/react-tools/withCurrentUser'; import { withReactQuery } from '@/react-tools/withReactQuery'; import { withUIRouter } from '@/react-tools/withUIRouter'; @@ -12,8 +11,4 @@ export const viewsModule = angular .component( 'waitingRoomView', r2a(withUIRouter(withReactQuery(withCurrentUser(WaitingRoomView))), []) - ) - .component( - 'edgeDevicesView', - r2a(withUIRouter(withReactQuery(withCurrentUser(ListView))), []) ).name; diff --git a/app/portainer/__module.js b/app/portainer/__module.js index 101d56f37..232e2ce08 100644 --- a/app/portainer/__module.js +++ b/app/portainer/__module.js @@ -175,7 +175,10 @@ angular var endpoint = { name: 'portainer.endpoints.endpoint', - url: '/:id', + url: '/:id?redirectTo', + params: { + redirectTo: '', + }, views: { 'content@': { templateUrl: './views/endpoints/edit/endpoint.html', diff --git a/app/portainer/components/datatables/datatable.css b/app/portainer/components/datatables/datatable.css index bd39ca3bf..a380a4d2b 100644 --- a/app/portainer/components/datatables/datatable.css +++ b/app/portainer/components/datatables/datatable.css @@ -51,7 +51,7 @@ .datatable .searchBar { border: 1px solid var(--border-searchbar); - background: var(--bg-searchbar) !important; + background: var(--bg-searchbar); border-radius: 5px; padding: 4px 10px; font-size: 14px; @@ -64,10 +64,9 @@ } .toolBar .searchBar { - flex: right; margin-right: 10px; - height: 30px; display: inline-flex; + min-height: 30px; } .datatable .searchBar input[type='text'] { @@ -112,12 +111,15 @@ .datatable .footer .paginationControls { float: right; - margin: 10px 15px 5px 0; + margin: 10px 0 5px 0; } .datatable .footer .paginationControls .limitSelector { font-size: 12px; - margin-right: 15px; +} + +.datatable .footer .paginationControls .limitSelector:not(:last-child) { + margin-right: 10px; } .datatable .footer .paginationControls .pagination { diff --git a/app/portainer/helpers/endpointHelper.js b/app/portainer/helpers/endpointHelper.js index 42c9dbff8..25ff1a704 100644 --- a/app/portainer/helpers/endpointHelper.js +++ b/app/portainer/helpers/endpointHelper.js @@ -8,10 +8,6 @@ function findAssociatedGroup(endpoint, groups) { } export default class EndpointHelper { - static isLocalEndpoint(endpoint) { - return endpoint.URL.includes('unix://') || endpoint.URL.includes('npipe://') || endpoint.Type === PortainerEndpointTypes.KubernetesLocalEnvironment; - } - static isDockerEndpoint(endpoint) { return [PortainerEndpointTypes.DockerEnvironment, PortainerEndpointTypes.AgentOnDockerEnvironment, PortainerEndpointTypes.EdgeAgentOnDockerEnvironment].includes(endpoint.Type); } diff --git a/app/portainer/hostmanagement/open-amt/open-amt.service.ts b/app/portainer/hostmanagement/open-amt/open-amt.service.ts index eabd14a66..64591b46f 100644 --- a/app/portainer/hostmanagement/open-amt/open-amt.service.ts +++ b/app/portainer/hostmanagement/open-amt/open-amt.service.ts @@ -1,13 +1,11 @@ import axios, { parseAxiosError } from '@/portainer/services/axios'; import { EnvironmentId } from '@/react/portainer/environments/types'; - import { OpenAMTConfiguration, AMTInformation, AuthorizationResponse, - Device, DeviceFeatures, -} from './model'; +} from '@/react/edge/edge-devices/open-amt/types'; const BASE_URL = '/open_amt'; @@ -34,42 +32,6 @@ export async function getAMTInfo(environmentId: EnvironmentId) { } } -export async function activateDevice(environmentId: EnvironmentId) { - try { - await axios.post(`${BASE_URL}/${environmentId}/activate`); - } catch (e) { - throw parseAxiosError(e as Error, 'Unable to activate device'); - } -} - -export async function getDevices(environmentId: EnvironmentId) { - try { - const { data: devices } = await axios.get( - `${BASE_URL}/${environmentId}/devices` - ); - - return devices; - } catch (e) { - throw parseAxiosError(e as Error, 'Unable to retrieve device information'); - } -} - -export async function executeDeviceAction( - environmentId: EnvironmentId, - deviceGUID: string, - action: string -) { - try { - const actionPayload = { action }; - await axios.post( - `${BASE_URL}/${environmentId}/devices/${deviceGUID}/action`, - actionPayload - ); - } catch (e) { - throw parseAxiosError(e as Error, 'Unable to execute device action'); - } -} - export async function enableDeviceFeatures( environmentId: EnvironmentId, deviceGUID: string, diff --git a/app/portainer/hostmanagement/open-amt/queries.ts b/app/portainer/hostmanagement/open-amt/queries.ts deleted file mode 100644 index ab6f9ba5e..000000000 --- a/app/portainer/hostmanagement/open-amt/queries.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { useMutation } from 'react-query'; - -import { activateDevice } from './open-amt.service'; - -export const activateDeviceMutationKey = [ - 'environments', - 'open-amt', - 'activate', -]; - -export function useActivateDeviceMutation() { - return useMutation(activateDevice, { - mutationKey: activateDeviceMutationKey, - meta: { - message: 'Unable to associate with OpenAMT', - }, - }); -} diff --git a/app/portainer/models/settings.js b/app/portainer/models/settings.js index 2e9b1ca02..717e5a2da 100644 --- a/app/portainer/models/settings.js +++ b/app/portainer/models/settings.js @@ -38,6 +38,8 @@ export function PublicSettingsViewModel(settings) { this.Edge = new EdgeSettingsViewModel(settings.Edge); this.DefaultRegistry = settings.DefaultRegistry; this.ShowKomposeBuildOption = settings.ShowKomposeBuildOption; + this.IsAMTEnabled = settings.IsAMTEnabled; + this.IsFDOEnabled = settings.IsFDOEnabled; } export function InternalAuthSettingsViewModel(data) { diff --git a/app/portainer/react/components/index.ts b/app/portainer/react/components/index.ts index a40d89bc6..1526fd5d7 100644 --- a/app/portainer/react/components/index.ts +++ b/app/portainer/react/components/index.ts @@ -98,7 +98,14 @@ export const componentsModule = angular ) .component( 'datatableSearchbar', - r2a(SearchBar, ['data-cy', 'onChange', 'value', 'placeholder']) + r2a(SearchBar, [ + 'data-cy', + 'onChange', + 'value', + 'placeholder', + 'children', + 'className', + ]) ) .component('badgeIcon', r2a(BadgeIcon, ['icon', 'size'])) .component( diff --git a/app/portainer/services/api/dockerhubService.js b/app/portainer/services/api/dockerhubService.js index 6326fa9a8..b49c7eb7c 100644 --- a/app/portainer/services/api/dockerhubService.js +++ b/app/portainer/services/api/dockerhubService.js @@ -1,5 +1,5 @@ -import EndpointHelper from 'Portainer/helpers/endpointHelper'; -import { PortainerEndpointTypes } from 'Portainer/models/endpoint/models'; +import { PortainerEndpointTypes } from '@/portainer/models/endpoint/models'; +import { isLocalEnvironment } from '@/react/portainer/environments/utils'; angular.module('portainer.app').factory('DockerHubService', DockerHubService); @@ -10,7 +10,7 @@ function DockerHubService(Endpoints, AgentDockerhub) { }; function checkRateLimits(endpoint, registryId) { - if (EndpointHelper.isLocalEndpoint(endpoint)) { + if (isLocalEnvironment(endpoint)) { return Endpoints.dockerhubLimits({ id: endpoint.Id, registryId }).$promise; } diff --git a/app/portainer/views/endpoints/edit/endpointController.js b/app/portainer/views/endpoints/edit/endpointController.js index 54c064201..d1058663d 100644 --- a/app/portainer/views/endpoints/edit/endpointController.js +++ b/app/portainer/views/endpoints/edit/endpointController.js @@ -252,7 +252,7 @@ function EndpointController( EndpointService.updateEndpoint(endpoint.Id, payload).then( function success() { Notifications.success('Environment updated', $scope.endpoint.Name); - $state.go('portainer.endpoints', {}, { reload: true }); + $state.go($state.params.redirectTo || 'portainer.endpoints', {}, { reload: true }); }, function error(err) { Notifications.error('Failure', err, 'Unable to update environment'); diff --git a/app/react/components/LinkButton.tsx b/app/react/components/LinkButton.tsx index 825bc8538..f63ca07cd 100644 --- a/app/react/components/LinkButton.tsx +++ b/app/react/components/LinkButton.tsx @@ -8,29 +8,26 @@ export function LinkButton({ to, params, disabled, - children, className, + children, + title = '', ...props }: ComponentProps & ComponentProps) { - const button = ( + return ( ); - - if (disabled) { - return button; - } - - return ( - - {button} - - ); } diff --git a/app/react/components/buttons/Button.tsx b/app/react/components/buttons/Button.tsx index 6151f0348..e1ec3a29f 100644 --- a/app/react/components/buttons/Button.tsx +++ b/app/react/components/buttons/Button.tsx @@ -25,7 +25,9 @@ type Color = | 'none'; type Size = 'xsmall' | 'small' | 'medium' | 'large'; -export interface Props extends AriaAttributes, AutomationTestingProps { +export interface Props + extends AriaAttributes, + AutomationTestingProps { icon?: ReactNode | ComponentType; color?: Color; @@ -34,10 +36,12 @@ export interface Props extends AriaAttributes, AutomationTestingProps { title?: string; className?: string; type?: Type; + as?: ComponentType | string; onClick?: MouseEventHandler; + props?: TasProps; } -export function Button({ +export function Button({ type = 'button', color = 'primary', size = 'small', @@ -47,11 +51,13 @@ export function Button({ title, icon, children, - + as = 'button', + props, ...ariaProps -}: PropsWithChildren) { +}: PropsWithChildren>) { + const Component = as as 'button'; return ( - + ); } diff --git a/app/react/components/datatables/FilterSearchBar.tsx b/app/react/components/datatables/FilterSearchBar.tsx deleted file mode 100644 index f9560147d..000000000 --- a/app/react/components/datatables/FilterSearchBar.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { Search } from 'lucide-react'; - -import { useLocalStorage } from '@/react/hooks/useLocalStorage'; - -interface Props { - value: string; - placeholder?: string; - onChange(value: string): void; -} - -export function FilterSearchBar({ - value, - placeholder = 'Search...', - onChange, -}: Props) { - return ( -
- - onChange(e.target.value)} - placeholder={placeholder} - data-cy="home-environmentSearch" - /> -
- ); -} - -export function useSearchBarState( - key: string -): [string, (value: string) => void] { - const filterKey = keyBuilder(key); - const [value, setValue] = useLocalStorage(filterKey, '', sessionStorage); - - return [value, setValue]; - - function keyBuilder(key: string) { - return `datatable_text_filter_${key}`; - } -} diff --git a/app/react/components/datatables/SearchBar.tsx b/app/react/components/datatables/SearchBar.tsx index de9762ff9..7370e9d1d 100644 --- a/app/react/components/datatables/SearchBar.tsx +++ b/app/react/components/datatables/SearchBar.tsx @@ -1,13 +1,19 @@ -import { Search } from 'lucide-react'; +import { ReactNode } from 'react'; +import { Search, X } from 'lucide-react'; +import clsx from 'clsx'; import { useLocalStorage } from '@/react/hooks/useLocalStorage'; import { AutomationTestingProps } from '@/types'; import { useDebounce } from '@/react/hooks/useDebounce'; +import { Button } from '@@/buttons'; + interface Props extends AutomationTestingProps { value: string; placeholder?: string; onChange(value: string): void; + className?: string; + children?: ReactNode; } export function SearchBar({ @@ -15,11 +21,19 @@ export function SearchBar({ placeholder = 'Search...', onChange, 'data-cy': dataCy, + className, + children, }: Props) { const [searchValue, setSearchValue] = useDebounce(value, onChange); + function onClear() { + setSearchValue(''); + } + return ( -
+
+ {children} +
); } diff --git a/app/react/components/datatables/TableTitle.tsx b/app/react/components/datatables/TableTitle.tsx index 4bb14d938..a8de73758 100644 --- a/app/react/components/datatables/TableTitle.tsx +++ b/app/react/components/datatables/TableTitle.tsx @@ -1,4 +1,5 @@ import { ComponentType, PropsWithChildren, ReactNode } from 'react'; +import clsx from 'clsx'; import { Icon } from '@@/Icon'; @@ -6,6 +7,7 @@ interface Props { icon?: ReactNode | ComponentType; label: string; description?: ReactNode; + className?: string; } export function TableTitle({ @@ -13,9 +15,10 @@ export function TableTitle({ label, children, description, + className, }: PropsWithChildren) { return ( -
+
{icon && ( diff --git a/app/react/components/form-components/PortainerSelect.tsx b/app/react/components/form-components/PortainerSelect.tsx index 56386ae10..199cdbd9b 100644 --- a/app/react/components/form-components/PortainerSelect.tsx +++ b/app/react/components/form-components/PortainerSelect.tsx @@ -5,7 +5,7 @@ import { AutomationTestingProps } from '@/types'; import { Select as ReactSelect } from '@@/form-components/ReactSelect'; -interface Option { +export interface Option { value: TValue; label: string; } diff --git a/app/react/components/form-components/ReactSelect.css b/app/react/components/form-components/ReactSelect.css index 851119e7a..f910a55ea 100644 --- a/app/react/components/form-components/ReactSelect.css +++ b/app/react/components/form-components/ReactSelect.css @@ -13,6 +13,16 @@ --single-value-option-text-color: var(--white-color); } +.portainer-selector__placeholder { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} + +.portainer-selector__indicator-separator { + display: none; +} + /* input style */ .portainer-selector-root .portainer-selector__control { border-color: var(--border-form-control-color); diff --git a/app/react/components/modals/Modal/CloseButton.tsx b/app/react/components/modals/Modal/CloseButton.tsx index dd4261622..22ccbddfd 100644 --- a/app/react/components/modals/Modal/CloseButton.tsx +++ b/app/react/components/modals/Modal/CloseButton.tsx @@ -12,7 +12,7 @@ export function CloseButton({ return ( - - - - {isOpenAMTEnabled && ( - - )} - - {showWaitingRoomLink && ( - - - - )} -
- ); - - async function onDeleteEdgeDeviceClick() { - const confirmed = await confirmDestructiveAsync({ - title: 'Are you sure ?', - message: - 'This action will remove all configurations associated to your environment(s). Continue?', - buttons: { - confirm: { - label: 'Remove', - className: 'btn-danger', - }, - }, - }); - - if (!confirmed) { - return; - } - - await Promise.all( - selectedItems.map(async (environment) => { - try { - await deleteEndpoint(environment.Id); - - notifications.success( - 'Environment successfully removed', - environment.Name - ); - } catch (err) { - notifications.error( - 'Failure', - err as Error, - 'Unable to remove environment' - ); - } - }) - ); - - await router.stateService.reload(); - } - - async function onAddNewDeviceClick() { - const result = isFDOEnabled - ? await promptAsync({ - title: 'How would you like to add an Edge Device?', - inputType: 'radio', - inputOptions: [ - { - text: 'Provision bare-metal using Intel FDO', - value: DeployType.FDO, - }, - { - text: 'Deploy agent manually', - value: DeployType.MANUAL, - }, - ], - buttons: { - confirm: { - label: 'Confirm', - className: 'btn-primary', - }, - }, - }) - : DeployType.MANUAL; - - switch (result) { - case DeployType.FDO: - router.stateService.go('portainer.endpoints.importDevice'); - break; - case DeployType.MANUAL: - router.stateService.go('portainer.wizard.endpoints', { - edgeDevice: true, - }); - break; - default: - break; - } - } - - async function onAssociateOpenAMTClick(selectedItems: Environment[]) { - const selectedEnvironment = selectedItems[0]; - - const confirmed = await confirmAsync({ - title: '', - message: `Associate ${selectedEnvironment.Name} with OpenAMT`, - buttons: { - cancel: { - label: 'Cancel', - className: 'btn-default', - }, - confirm: { - label: 'Confirm', - className: 'btn-primary', - }, - }, - }); - - if (!confirmed) { - return; - } - - activateDeviceMutation.mutate(selectedEnvironment.Id, { - onSuccess() { - notifications.notifySuccess( - 'Successfully associated with OpenAMT', - selectedEnvironment.Name - ); - }, - }); - } -} diff --git a/app/react/edge/edge-devices/ListView/EdgeDevicesDatatable/EdgeDevicesDatatableSettings.tsx b/app/react/edge/edge-devices/ListView/EdgeDevicesDatatable/EdgeDevicesDatatableSettings.tsx deleted file mode 100644 index 03acc5a4d..000000000 --- a/app/react/edge/edge-devices/ListView/EdgeDevicesDatatable/EdgeDevicesDatatableSettings.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { TableSettingsMenuAutoRefresh } from '@@/datatables/TableSettingsMenuAutoRefresh'; -import { RefreshableTableSettings } from '@@/datatables/types'; - -interface Props { - settings: RefreshableTableSettings; -} - -export function EdgeDevicesDatatableSettings({ settings }: Props) { - return ( - - ); - - function handleRefreshRateChange(autoRefreshRate: number) { - settings.setAutoRefreshRate(autoRefreshRate); - } -} diff --git a/app/react/edge/edge-devices/ListView/EdgeDevicesDatatable/columns/RowContext.tsx b/app/react/edge/edge-devices/ListView/EdgeDevicesDatatable/columns/RowContext.tsx deleted file mode 100644 index 859b182f2..000000000 --- a/app/react/edge/edge-devices/ListView/EdgeDevicesDatatable/columns/RowContext.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { EnvironmentGroup } from '@/react/portainer/environments/environment-groups/types'; - -import { createRowContext } from '@@/datatables/RowContext'; - -interface RowContextState { - isOpenAmtEnabled: boolean; - groups: EnvironmentGroup[]; -} - -const { RowProvider, useRowContext } = createRowContext(); - -export { RowProvider, useRowContext }; diff --git a/app/react/edge/edge-devices/ListView/EdgeDevicesDatatable/columns/actions.tsx b/app/react/edge/edge-devices/ListView/EdgeDevicesDatatable/columns/actions.tsx deleted file mode 100644 index 01eaf1c74..000000000 --- a/app/react/edge/edge-devices/ListView/EdgeDevicesDatatable/columns/actions.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { CellProps, Column } from 'react-table'; -import { MenuItem, MenuLink } from '@reach/menu-button'; -import { useRouter, useSref } from '@uirouter/react'; - -import { Environment } from '@/react/portainer/environments/types'; -import { snapshotEndpoint } from '@/react/portainer/environments/environment.service'; -import * as notifications from '@/portainer/services/notifications'; -import { getDashboardRoute } from '@/react/portainer/environments/utils'; - -import { ActionsMenu } from '@@/datatables/ActionsMenu'; - -export const actions: Column = { - Header: 'Actions', - accessor: () => 'actions', - id: 'actions', - disableFilters: true, - canHide: true, - disableResizing: true, - width: '5px', - sortType: 'string', - Filter: () => null, - Cell: ActionsCell, -}; - -export function ActionsCell({ - row: { original: environment }, -}: CellProps) { - const router = useRouter(); - - const environmentRoute = getDashboardRoute(environment); - const browseLinkProps = useSref(environmentRoute, { - id: environment.Id, - endpointId: environment.Id, - }); - - const snapshotLinkProps = useSref('edge.browse.dashboard', { - environmentId: environment.Id, - }); - - const showRefreshSnapshot = false; // remove and show MenuItem when feature is available - - return ( - - {environment.Edge.AsyncMode ? ( - - Browse Snapshot - - ) : ( - - Browse - - )} - {showRefreshSnapshot && ( - - )} - - ); - - async function handleRefreshSnapshotClick() { - try { - await snapshotEndpoint(environment.Id); - notifications.success('Success', 'Environment updated'); - } catch (err) { - notifications.error( - 'Failure', - err as Error, - 'An error occurred during environment snapshot' - ); - } finally { - await router.stateService.reload(); - } - } -} diff --git a/app/react/edge/edge-devices/ListView/EdgeDevicesDatatable/columns/group.tsx b/app/react/edge/edge-devices/ListView/EdgeDevicesDatatable/columns/group.tsx deleted file mode 100644 index df4bc0a2a..000000000 --- a/app/react/edge/edge-devices/ListView/EdgeDevicesDatatable/columns/group.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Column } from 'react-table'; - -import { Environment } from '@/react/portainer/environments/types'; -import { EnvironmentGroupId } from '@/react/portainer/environments/environment-groups/types'; - -import { DefaultFilter } from '@@/datatables/Filter'; - -import { useRowContext } from './RowContext'; - -export const group: Column = { - Header: 'Group', - accessor: (row) => row.GroupId, - Cell: GroupCell, - id: 'groupName', - Filter: DefaultFilter, - canHide: true, -}; - -function GroupCell({ value }: { value: EnvironmentGroupId }) { - const { groups } = useRowContext(); - const group = groups.find((g) => g.Id === value); - - return group?.Name || ''; -} diff --git a/app/react/edge/edge-devices/ListView/EdgeDevicesDatatable/columns/heartbeat.tsx b/app/react/edge/edge-devices/ListView/EdgeDevicesDatatable/columns/heartbeat.tsx deleted file mode 100644 index 23ea61e3b..000000000 --- a/app/react/edge/edge-devices/ListView/EdgeDevicesDatatable/columns/heartbeat.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { CellProps, Column } from 'react-table'; -import clsx from 'clsx'; - -import { Environment } from '@/react/portainer/environments/types'; -import { useHasHeartbeat } from '@/react/edge/hooks/useHasHeartbeat'; - -export const heartbeat: Column = { - Header: 'Heartbeat', - accessor: 'Status', - id: 'status', - Cell: StatusCell, - disableFilters: true, - canHide: true, -}; - -export function StatusCell({ - row: { original: environment }, -}: CellProps) { - return ; -} - -function EdgeIndicator({ environment }: { environment: Environment }) { - const isValid = useHasHeartbeat(environment); - - if (isValid === null) { - return null; - } - - const associated = !!environment.EdgeID; - if (!associated) { - return ( - - - associated - - - ); - } - - return ( - - - heartbeat - - - ); -} diff --git a/app/react/edge/edge-devices/ListView/EdgeDevicesDatatable/columns/index.tsx b/app/react/edge/edge-devices/ListView/EdgeDevicesDatatable/columns/index.tsx deleted file mode 100644 index 8e431f7ef..000000000 --- a/app/react/edge/edge-devices/ListView/EdgeDevicesDatatable/columns/index.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { name } from './name'; -import { heartbeat } from './heartbeat'; -import { group } from './group'; -import { actions } from './actions'; - -export const columns = [name, heartbeat, group, actions]; diff --git a/app/react/edge/edge-devices/ListView/EdgeDevicesDatatable/columns/name.tsx b/app/react/edge/edge-devices/ListView/EdgeDevicesDatatable/columns/name.tsx deleted file mode 100644 index 197a2ba9e..000000000 --- a/app/react/edge/edge-devices/ListView/EdgeDevicesDatatable/columns/name.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { CellProps, Column } from 'react-table'; - -import { Environment } from '@/react/portainer/environments/types'; - -import { Link } from '@@/Link'; -import { ExpandingCell } from '@@/datatables/ExpandingCell'; - -import { useRowContext } from './RowContext'; - -export const name: Column = { - Header: 'Name', - accessor: (row) => row.Name, - id: 'name', - Cell: NameCell, - disableFilters: true, - Filter: () => null, - canHide: false, - sortType: 'string', -}; - -export function NameCell({ value: name, row }: CellProps) { - const { isOpenAmtEnabled } = useRowContext(); - const showExpandedRow = !!( - isOpenAmtEnabled && - row.original.AMTDeviceGUID && - row.original.AMTDeviceGUID.length > 0 - ); - return ( - - - {name} - - - ); -} diff --git a/app/react/edge/edge-devices/ListView/EdgeDevicesDatatable/datatable-store.ts b/app/react/edge/edge-devices/ListView/EdgeDevicesDatatable/datatable-store.ts deleted file mode 100644 index ae008d07d..000000000 --- a/app/react/edge/edge-devices/ListView/EdgeDevicesDatatable/datatable-store.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { - refreshableSettings, - hiddenColumnsSettings, - createPersistedStore, -} from '@@/datatables/types'; - -import { TableSettings } from './types'; - -export function createStore(storageKey: string) { - return createPersistedStore(storageKey, 'Name', (set) => ({ - ...hiddenColumnsSettings(set), - ...refreshableSettings(set), - })); -} diff --git a/app/react/edge/edge-devices/ListView/EdgeDevicesDatatable/types.ts b/app/react/edge/edge-devices/ListView/EdgeDevicesDatatable/types.ts deleted file mode 100644 index 708f39c69..000000000 --- a/app/react/edge/edge-devices/ListView/EdgeDevicesDatatable/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { - BasicTableSettings, - RefreshableTableSettings, - SettableColumnsTableSettings, -} from '@@/datatables/types'; - -export interface TableSettings - extends BasicTableSettings, - SettableColumnsTableSettings, - RefreshableTableSettings {} diff --git a/app/react/edge/edge-devices/ListView/ListView.tsx b/app/react/edge/edge-devices/ListView/ListView.tsx deleted file mode 100644 index 1f7d47731..000000000 --- a/app/react/edge/edge-devices/ListView/ListView.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { useIsMutating } from 'react-query'; - -import { useSettings } from '@/react/portainer/settings/queries'; -import { useGroups } from '@/react/portainer/environments/environment-groups/queries'; -import { activateDeviceMutationKey } from '@/portainer/hostmanagement/open-amt/queries'; - -import { PageHeader } from '@@/PageHeader'; -import { ViewLoading } from '@@/ViewLoading'; - -import { EdgeDevicesDatatable } from './EdgeDevicesDatatable/EdgeDevicesDatatable'; - -export function ListView() { - const isActivatingDevice = useIsActivatingDevice(); - const settingsQuery = useSettings(); - const groupsQuery = useGroups(); - - if (!settingsQuery.data || !groupsQuery.data) { - return null; - } - - const settings = settingsQuery.data; - - return ( - <> - - - {isActivatingDevice ? ( - - ) : ( - - )} - - ); -} - -function useIsActivatingDevice() { - const count = useIsMutating({ mutationKey: activateDeviceMutationKey }); - return count > 0; -} diff --git a/app/react/edge/edge-devices/ListView/index.ts b/app/react/edge/edge-devices/ListView/index.ts deleted file mode 100644 index dd06dfd19..000000000 --- a/app/react/edge/edge-devices/ListView/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ListView } from './ListView'; diff --git a/app/portainer/hostmanagement/open-amt/model.ts b/app/react/edge/edge-devices/open-amt/types.ts similarity index 78% rename from app/portainer/hostmanagement/open-amt/model.ts rename to app/react/edge/edge-devices/open-amt/types.ts index 00e5841a6..59a3d9951 100644 --- a/app/portainer/hostmanagement/open-amt/model.ts +++ b/app/react/edge/edge-devices/open-amt/types.ts @@ -31,10 +31,21 @@ export interface DeviceFeatures { userConsent: string; } +export enum PowerStateCode { + On = 2, + SleepLight = 3, + SleepDeep = 4, + OffHard = 6, + Hibernate = 7, + OffSoft = 8, + PowerCycle = 9, + OffHardGraceful = 13, +} + export type Device = { guid: string; hostname: string; - powerState: number; + powerState: PowerStateCode; connectionStatus: boolean; features?: DeviceFeatures; }; diff --git a/app/react/edge/edge-devices/open-amt/useAMTDevices.tsx b/app/react/edge/edge-devices/open-amt/useAMTDevices.tsx new file mode 100644 index 000000000..0b57379d6 --- /dev/null +++ b/app/react/edge/edge-devices/open-amt/useAMTDevices.tsx @@ -0,0 +1,33 @@ +import { useQuery } from 'react-query'; + +import { EnvironmentId } from '@/react/portainer/environments/types'; +import { withError } from '@/react-tools/react-query'; +import axios, { parseAxiosError } from '@/portainer/services/axios'; + +import { Device } from './types'; + +export function useAMTDevices( + environmentId: EnvironmentId, + { enabled }: { enabled?: boolean } = {} +) { + return useQuery( + ['amt_devices', environmentId], + () => getDevices(environmentId), + { + ...withError('Failed retrieving AMT devices'), + enabled, + } + ); +} + +async function getDevices(environmentId: EnvironmentId) { + try { + const { data: devices } = await axios.get( + `/open_amt/${environmentId}/devices` + ); + + return devices; + } catch (e) { + throw parseAxiosError(e as Error, 'Unable to retrieve device information'); + } +} diff --git a/app/react/edge/edge-devices/open-amt/useActivateDevicesMutation.ts b/app/react/edge/edge-devices/open-amt/useActivateDevicesMutation.ts new file mode 100644 index 000000000..79e78a30c --- /dev/null +++ b/app/react/edge/edge-devices/open-amt/useActivateDevicesMutation.ts @@ -0,0 +1,22 @@ +import { useMutation } from 'react-query'; + +import { promiseSequence } from '@/portainer/helpers/promise-utils'; +import { mutationOptions, withError } from '@/react-tools/react-query'; +import { EnvironmentId } from '@/react/portainer/environments/types'; +import axios, { parseAxiosError } from '@/portainer/services/axios'; + +export function useActivateDevicesMutation() { + return useMutation( + (environmentIds: EnvironmentId[]) => + promiseSequence(environmentIds.map((id) => () => activateDevice(id))), + mutationOptions(withError('Unable to associate with OpenAMT')) + ); +} + +async function activateDevice(environmentId: EnvironmentId) { + try { + await axios.post(`/open_amt/${environmentId}/activate`); + } catch (e) { + throw parseAxiosError(e as Error, 'Unable to activate device'); + } +} diff --git a/app/react/edge/edge-devices/open-amt/useExecuteAMTDeviceActionMutation.tsx b/app/react/edge/edge-devices/open-amt/useExecuteAMTDeviceActionMutation.tsx new file mode 100644 index 000000000..c1ed8c547 --- /dev/null +++ b/app/react/edge/edge-devices/open-amt/useExecuteAMTDeviceActionMutation.tsx @@ -0,0 +1,40 @@ +import { useMutation, useQueryClient } from 'react-query'; + +import axios, { parseAxiosError } from '@/portainer/services/axios'; +import { withError } from '@/react-tools/react-query'; +import { EnvironmentId } from '@/react/portainer/environments/types'; + +export enum DeviceAction { + PowerOn = 'power on', + PowerOff = 'power off', + Restart = 'restart', +} + +export function useExecuteAMTDeviceActionMutation() { + const queryClient = useQueryClient(); + return useMutation(executeDeviceAction, { + onSuccess(_data, { environmentId }) { + queryClient.invalidateQueries([['amt_devices', environmentId]]); + }, + ...withError('Unable to execute device action'), + }); +} + +async function executeDeviceAction({ + action, + deviceGUID, + environmentId, +}: { + environmentId: EnvironmentId; + deviceGUID: string; + action: DeviceAction; +}) { + try { + await axios.post( + `/open_amt/${environmentId}/devices/${deviceGUID}/action`, + { action } + ); + } catch (e) { + throw parseAxiosError(e as Error, 'Unable to execute device action'); + } +} diff --git a/app/react/portainer/HomeView/EnvironmentList/AMTButton/AMTButton.tsx b/app/react/portainer/HomeView/EnvironmentList/AMTButton/AMTButton.tsx new file mode 100644 index 000000000..02c68a193 --- /dev/null +++ b/app/react/portainer/HomeView/EnvironmentList/AMTButton/AMTButton.tsx @@ -0,0 +1,55 @@ +import { Link } from 'lucide-react'; +import { useState } from 'react'; + +import { Environment } from '@/react/portainer/environments/types'; +import { useSettings } from '@/react/portainer/settings/queries'; +import { Query } from '@/react/portainer/environments/queries/useEnvironmentList'; +import { isEdgeEnvironment } from '@/react/portainer/environments/utils'; + +import { Button } from '@@/buttons'; + +import { AssociateAMTDialog } from './AssociateAMTDialog'; + +export function AMTButton({ + environments, + envQueryParams, +}: { + environments: Environment[]; + envQueryParams: Query; +}) { + const [isOpenDialog, setOpenDialog] = useState(false); + const isOpenAmtEnabledQuery = useSettings( + (settings) => + settings.EnableEdgeComputeFeatures && + settings.openAMTConfiguration.enabled + ); + + const isOpenAMTEnabled = !!isOpenAmtEnabledQuery.data; + + if (!isOpenAMTEnabled) { + return null; + } + + const edgeEnvironments = environments.filter((env) => + isEdgeEnvironment(env.Type) + ); + + return ( + <> + + {isOpenDialog && ( + env.Id)} + onClose={() => setOpenDialog(false)} + envQueryParams={envQueryParams} + /> + )} + + ); + + function openDialog() { + setOpenDialog(true); + } +} diff --git a/app/react/portainer/HomeView/EnvironmentList/AMTButton/AssociateAMTDialog.tsx b/app/react/portainer/HomeView/EnvironmentList/AMTButton/AssociateAMTDialog.tsx new file mode 100644 index 000000000..f5ba1371d --- /dev/null +++ b/app/react/portainer/HomeView/EnvironmentList/AMTButton/AssociateAMTDialog.tsx @@ -0,0 +1,118 @@ +import { useState } from 'react'; +import clsx from 'clsx'; + +import { useActivateDevicesMutation } from '@/react/edge/edge-devices/open-amt/useActivateDevicesMutation'; +import { usePaginationLimitState } from '@/react/hooks/usePaginationLimitState'; +import { Query } from '@/react/portainer/environments/queries/useEnvironmentList'; +import { EdgeTypes, Environment } from '@/react/portainer/environments/types'; +import { useEnvironmentList } from '@/react/portainer/environments/queries'; +import { useListSelection } from '@/react/hooks/useListSelection'; + +import { Checkbox } from '@@/form-components/Checkbox'; +import { Modal } from '@@/modals/Modal'; +import { PaginationControls } from '@@/PaginationControls'; +import { Button, LoadingButton } from '@@/buttons'; + +interface Props { + envQueryParams: Query; + onClose: () => void; + selectedItems: Array; +} + +const storageKey = 'home_endpoints'; + +export function AssociateAMTDialog({ + selectedItems, + onClose, + envQueryParams, +}: Props) { + const activateDeviceMutation = useActivateDevicesMutation(); + const [page, setPage] = useState(1); + const [pageLimit, setPageLimit] = usePaginationLimitState(storageKey); + + const [selection, toggleSelection] = + useListSelection(selectedItems); + + const { environments, totalCount, isLoading } = useEnvironmentList({ + ...envQueryParams, + page, + pageLimit, + types: EdgeTypes, + }); + const isAllPageSelected = + !isLoading && environments.every((env) => selection.includes(env.Id)); + + return ( + + + + + Select the environments to add to associate to OpenAMT. You may select + across multiple pages. + +
+ +
+
+
+ {environments.map((env) => ( +
+ + toggleSelection(env.Id, !selection.includes(env.Id)) + } + /> +
+ ))} +
+
+ +
+
+
+ + + + Associate Devices + + +
+ ); + + function handleSelectAll() { + environments.forEach((env) => toggleSelection(env.Id, !isAllPageSelected)); + } + + function handleSubmit() { + activateDeviceMutation.mutate(selection, { + onSuccess() { + onClose(); + }, + }); + } +} diff --git a/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/AgentDetails.tsx b/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/AgentDetails.tsx new file mode 100644 index 000000000..b1abf5899 --- /dev/null +++ b/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/AgentDetails.tsx @@ -0,0 +1,22 @@ +import { Globe } from 'lucide-react'; + +import { Environment } from '@/react/portainer/environments/types'; +import { isAgentEnvironment } from '@/react/portainer/environments/utils'; + +export function AgentDetails({ environment }: { environment: Environment }) { + if (!isAgentEnvironment(environment.Type)) { + return null; + } + + return ( + <> + {environment.Agent.Version} + {environment.Edge.AsyncMode && ( + + + )} + + ); +} diff --git a/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/AgentVersionTag.tsx b/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/AgentVersionTag.tsx deleted file mode 100644 index 5bd108ad0..000000000 --- a/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/AgentVersionTag.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { Zap } from 'lucide-react'; - -import { EnvironmentType } from '@/react/portainer/environments/types'; -import { - isAgentEnvironment, - isEdgeEnvironment, -} from '@/react/portainer/environments/utils'; - -interface Props { - type: EnvironmentType; - version: string; -} - -export function AgentVersionTag({ type, version }: Props) { - if (!isAgentEnvironment(type)) { - return null; - } - - return ( - - - ); -} diff --git a/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/EditButtons.tsx b/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/EditButtons.tsx index 96970900f..ba0b1054f 100644 --- a/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/EditButtons.tsx +++ b/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/EditButtons.tsx @@ -21,11 +21,11 @@ export function EditButtons({ environment }: { environment: Environment }) { const configRoute = getConfigRoute(environment); return ( - + -
{children[0] || null}
-
- {children[1] || null} -
-
{children[2] || null}
+ {children.map((child, index) => ( +
+ {child} +
+ ))}
); } diff --git a/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/EnvironmentBrowseButtons.tsx b/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/EnvironmentBrowseButtons.tsx index 7d0119c2b..9c5a00217 100644 --- a/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/EnvironmentBrowseButtons.tsx +++ b/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/EnvironmentBrowseButtons.tsx @@ -11,6 +11,7 @@ import { Icon } from '@@/Icon'; import { LinkButton } from '@@/LinkButton'; type BrowseStatus = 'snapshot' | 'connected' | 'disconnected'; + export function EnvironmentBrowseButtons({ environment, onClickBrowse, @@ -23,7 +24,7 @@ export function EnvironmentBrowseButtons({ const isEdgeAsync = checkEdgeAsync(environment); const browseStatus = getStatus(isActive, isEdgeAsync); return ( -
+
{isBE && ( Browse snapshot )} Live connect @@ -85,7 +87,7 @@ function BrowseStatusTag({ status }: { status: BrowseStatus }) { function Disconnected() { return ( -
+
Disconnected
@@ -94,7 +96,7 @@ function Disconnected() { function Connected() { return ( -
+
Connected
@@ -103,7 +105,7 @@ function Connected() { function Snapshot() { return ( -
+
Browsing Snapshot
diff --git a/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/EnvironmentItem.tsx b/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/EnvironmentItem.tsx index fc2a5181a..c71d2c3a4 100644 --- a/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/EnvironmentItem.tsx +++ b/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/EnvironmentItem.tsx @@ -1,5 +1,5 @@ import _ from 'lodash'; -import { Tag, Globe, Activity } from 'lucide-react'; +import { Tag, Activity } from 'lucide-react'; import { isoDateFromTimestamp, @@ -24,9 +24,10 @@ import { Link } from '@@/Link'; import { EnvironmentIcon } from './EnvironmentIcon'; import { EnvironmentStats } from './EnvironmentStats'; import { EngineVersion } from './EngineVersion'; -import { AgentVersionTag } from './AgentVersionTag'; +import { EnvironmentTypeTag } from './EnvironmentTypeTag'; import { EnvironmentBrowseButtons } from './EnvironmentBrowseButtons'; import { EditButtons } from './EditButtons'; +import { AgentDetails } from './AgentDetails'; interface Props { environment: Environment; @@ -48,84 +49,82 @@ export function EnvironmentItem({ const tags = useEnvironmentTagNames(environment.TagIds); return ( - - + + {/* + Buttons are extracted out of the main button because it causes errors with react and accessibility issues + see https://stackoverflow.com/questions/66409964/warning-validatedomnesting-a-cannot-appear-as-a-descendant-of-a + */} +
+
+
- - - +
+
); } diff --git a/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/EnvironmentStatsDocker.tsx b/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/EnvironmentStatsDocker.tsx index ea2c5e3ac..f6a23110a 100644 --- a/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/EnvironmentStatsDocker.tsx +++ b/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/EnvironmentStatsDocker.tsx @@ -1,16 +1,19 @@ import { - Layers, - Shuffle, - Database, - List, - HardDrive, Box, - Power, + Cpu, + Database, + HardDrive, Heart, + Layers, + List, + Power, + Shuffle, } from 'lucide-react'; +import Memory from '@/assets/ico/memory.svg?c'; import { addPlural } from '@/portainer/helpers/strings'; import { DockerSnapshot } from '@/react/docker/snapshots/types'; +import { humanize } from '@/portainer/filters/filters'; import { StatsItem } from '@@/StatsItem'; @@ -49,6 +52,13 @@ export function EnvironmentStatsDocker({ snapshot }: Props) { /> + + + + {snapshot.Swarm && ( +
); @@ -472,80 +379,6 @@ export function EnvironmentList({ onClickBrowse, onRefresh }: Props) { } } -function getConnectionTypeOptions(platformTypes: Filter[]) { - const platformTypeConnectionType = { - [PlatformType.Docker]: [ - ConnectionType.API, - ConnectionType.Agent, - ConnectionType.EdgeAgent, - ConnectionType.EdgeDevice, - ], - [PlatformType.Azure]: [ConnectionType.API], - [PlatformType.Kubernetes]: [ - ConnectionType.Agent, - ConnectionType.EdgeAgent, - ConnectionType.EdgeDevice, - ], - [PlatformType.Nomad]: [ConnectionType.EdgeAgent, ConnectionType.EdgeDevice], - }; - - const connectionTypesDefaultOptions = [ - { value: ConnectionType.API, label: 'API' }, - { value: ConnectionType.Agent, label: 'Agent' }, - { value: ConnectionType.EdgeAgent, label: 'Edge Agent' }, - ]; - - if (platformTypes.length === 0) { - return connectionTypesDefaultOptions; - } - - return _.compact( - _.intersection( - ...platformTypes.map((p) => platformTypeConnectionType[p.value]) - ).map((c) => connectionTypesDefaultOptions.find((o) => o.value === c)) - ); -} - -function getPlatformTypeOptions(connectionTypes: Filter[]) { - const platformDefaultOptions = [ - { value: PlatformType.Docker, label: 'Docker' }, - { value: PlatformType.Azure, label: 'Azure' }, - { value: PlatformType.Kubernetes, label: 'Kubernetes' }, - ]; - - if (isBE) { - platformDefaultOptions.push({ - value: PlatformType.Nomad, - label: 'Nomad', - }); - } - - if (connectionTypes.length === 0) { - return platformDefaultOptions; - } - - const connectionTypePlatformType = { - [ConnectionType.API]: [PlatformType.Docker, PlatformType.Azure], - [ConnectionType.Agent]: [PlatformType.Docker, PlatformType.Kubernetes], - [ConnectionType.EdgeAgent]: [ - PlatformType.Kubernetes, - PlatformType.Nomad, - PlatformType.Docker, - ], - [ConnectionType.EdgeDevice]: [ - PlatformType.Nomad, - PlatformType.Docker, - PlatformType.Kubernetes, - ], - }; - - return _.compact( - _.intersection( - ...connectionTypes.map((p) => connectionTypePlatformType[p.value]) - ).map((c) => platformDefaultOptions.find((o) => o.value === c)) - ); -} - function renderItems( isLoading: boolean, totalCount: number, diff --git a/app/react/portainer/HomeView/EnvironmentList/EnvironmentListFilters.tsx b/app/react/portainer/HomeView/EnvironmentList/EnvironmentListFilters.tsx new file mode 100644 index 000000000..65338698a --- /dev/null +++ b/app/react/portainer/HomeView/EnvironmentList/EnvironmentListFilters.tsx @@ -0,0 +1,246 @@ +import _ from 'lodash'; + +import { useTags } from '@/portainer/tags/queries'; + +import { useAgentVersionsList } from '../../environments/queries/useAgentVersionsList'; +import { EnvironmentStatus, PlatformType } from '../../environments/types'; +import { isBE } from '../../feature-flags/feature-flags.service'; +import { useGroups } from '../../environments/environment-groups/queries'; + +import { HomepageFilter } from './HomepageFilter'; +import { SortbySelector } from './SortbySelector'; +import { ConnectionType, Filter } from './types'; +import styles from './EnvironmentList.module.css'; + +const status = [ + { value: EnvironmentStatus.Up, label: 'Up' }, + { value: EnvironmentStatus.Down, label: 'Down' }, +]; + +const sortByOptions = [ + { value: 1, label: 'Name' }, + { value: 2, label: 'Group' }, + { value: 3, label: 'Status' }, +]; + +export function EnvironmentListFilters({ + agentVersions, + clearFilter, + connectionTypes, + groupOnChange, + groupState, + platformTypes, + setAgentVersions, + setConnectionTypes, + setPlatformTypes, + sortByButton, + sortByDescending, + sortByState, + sortOnDescending, + sortOnchange, + statusOnChange, + statusState, + tagOnChange, + tagState, +}: { + platformTypes: Filter[]; + setPlatformTypes: (value: Filter[]) => void; + + connectionTypes: Filter[]; + setConnectionTypes: (value: Filter[]) => void; + + statusState: Filter[]; + statusOnChange: (filterOptions: Filter[]) => void; + + tagOnChange: (filterOptions: Filter[]) => void; + tagState: Filter[]; + + groupOnChange: (filterOptions: Filter[]) => void; + groupState: Filter[]; + + setAgentVersions: (value: Filter[]) => void; + agentVersions: Filter[]; + + sortByState: Filter | undefined; + sortOnchange: (filterOptions: Filter) => void; + + sortOnDescending: () => void; + sortByDescending: boolean; + + sortByButton: boolean; + + clearFilter: () => void; +}) { + const agentVersionsQuery = useAgentVersionsList(); + const connectionTypeOptions = getConnectionTypeOptions(platformTypes); + const platformTypeOptions = getPlatformTypeOptions(connectionTypes); + + const groupsQuery = useGroups(); + const groupOptions = [...(groupsQuery.data || [])]; + const uniqueGroup = [ + ...new Map(groupOptions.map((item) => [item.Id, item])).values(), + ].map(({ Id: value, Name: label }) => ({ + value, + label, + })); + + const tagsQuery = useTags(); + const tagOptions = [...(tagsQuery.tags || [])]; + const uniqueTag = [ + ...new Map(tagOptions.map((item) => [item.ID, item])).values(), + ].map(({ ID: value, Name: label }) => ({ + value, + label, + })); + + return ( +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + filterOptions={ + agentVersionsQuery.data?.map((v) => ({ + label: v, + value: v, + })) || [] + } + onChange={setAgentVersions} + placeHolder="Agent Version" + value={agentVersions} + /> +
+ + +
+ +
+
+ ); +} + +function getConnectionTypeOptions(platformTypes: Filter[]) { + const platformTypeConnectionType = { + [PlatformType.Docker]: [ + ConnectionType.API, + ConnectionType.Agent, + ConnectionType.EdgeAgent, + ConnectionType.EdgeDevice, + ], + [PlatformType.Azure]: [ConnectionType.API], + [PlatformType.Kubernetes]: [ + ConnectionType.Agent, + ConnectionType.EdgeAgent, + ConnectionType.EdgeDevice, + ], + [PlatformType.Nomad]: [ConnectionType.EdgeAgent, ConnectionType.EdgeDevice], + }; + + const connectionTypesDefaultOptions = [ + { value: ConnectionType.API, label: 'API' }, + { value: ConnectionType.Agent, label: 'Agent' }, + { value: ConnectionType.EdgeAgent, label: 'Edge Agent' }, + ]; + + if (platformTypes.length === 0) { + return connectionTypesDefaultOptions; + } + + return _.compact( + _.intersection( + ...platformTypes.map((p) => platformTypeConnectionType[p.value]) + ).map((c) => connectionTypesDefaultOptions.find((o) => o.value === c)) + ); +} + +function getPlatformTypeOptions(connectionTypes: Filter[]) { + const platformDefaultOptions = [ + { value: PlatformType.Docker, label: 'Docker' }, + { value: PlatformType.Azure, label: 'Azure' }, + { value: PlatformType.Kubernetes, label: 'Kubernetes' }, + ]; + + if (isBE) { + platformDefaultOptions.push({ + value: PlatformType.Nomad, + label: 'Nomad', + }); + } + + if (connectionTypes.length === 0) { + return platformDefaultOptions; + } + + const connectionTypePlatformType = { + [ConnectionType.API]: [PlatformType.Docker, PlatformType.Azure], + [ConnectionType.Agent]: [PlatformType.Docker, PlatformType.Kubernetes], + [ConnectionType.EdgeAgent]: [ + PlatformType.Kubernetes, + PlatformType.Nomad, + PlatformType.Docker, + ], + [ConnectionType.EdgeDevice]: [ + PlatformType.Nomad, + PlatformType.Docker, + PlatformType.Kubernetes, + ], + }; + + return _.compact( + _.intersection( + ...connectionTypes.map((p) => connectionTypePlatformType[p.value]) + ).map((c) => platformDefaultOptions.find((o) => o.value === c)) + ); +} diff --git a/app/react/portainer/HomeView/EnvironmentList/KubeconfigButton/KubeconfigButton.tsx b/app/react/portainer/HomeView/EnvironmentList/KubeconfigButton/KubeconfigButton.tsx index b49b58ed3..2104e41c1 100644 --- a/app/react/portainer/HomeView/EnvironmentList/KubeconfigButton/KubeconfigButton.tsx +++ b/app/react/portainer/HomeView/EnvironmentList/KubeconfigButton/KubeconfigButton.tsx @@ -28,11 +28,12 @@ export function KubeconfigButton({ environments, envQueryParams }: Props) { diff --git a/app/react/portainer/HomeView/EnvironmentList/SortbySelector.tsx b/app/react/portainer/HomeView/EnvironmentList/SortbySelector.tsx index 155d8b96e..200c1fa8c 100644 --- a/app/react/portainer/HomeView/EnvironmentList/SortbySelector.tsx +++ b/app/react/portainer/HomeView/EnvironmentList/SortbySelector.tsx @@ -27,29 +27,26 @@ export function SortbySelector({ }: Props) { const sorted = sortByButton && !!value; return ( -
-
- onChange(option as Filter)} + isClearable + value={value} + /> + +
); } diff --git a/app/react/portainer/HomeView/EnvironmentList/types.ts b/app/react/portainer/HomeView/EnvironmentList/types.ts index 755ef9247..81c81af68 100644 --- a/app/react/portainer/HomeView/EnvironmentList/types.ts +++ b/app/react/portainer/HomeView/EnvironmentList/types.ts @@ -2,3 +2,10 @@ export interface Filter { value: T; label: string; } + +export enum ConnectionType { + API, + Agent, + EdgeAgent, + EdgeDevice, +} diff --git a/app/react/portainer/environments/queries/index.ts b/app/react/portainer/environments/queries/index.ts new file mode 100644 index 000000000..992a9561d --- /dev/null +++ b/app/react/portainer/environments/queries/index.ts @@ -0,0 +1,2 @@ +export { useEnvironment } from './useEnvironment'; +export { useEnvironmentList } from './useEnvironmentList'; diff --git a/app/react/portainer/environments/utils/index.ts b/app/react/portainer/environments/utils/index.ts index 54294d725..d814b2a6d 100644 --- a/app/react/portainer/environments/utils/index.ts +++ b/app/react/portainer/environments/utils/index.ts @@ -59,6 +59,14 @@ export function isUnassociatedEdgeEnvironment(env: Environment) { return isEdgeEnvironment(env.Type) && !env.EdgeID; } +export function isLocalEnvironment(environment: Environment) { + return ( + environment.URL.includes('unix://') || + environment.URL.includes('npipe://') || + environment.Type === EnvironmentType.KubernetesLocal + ); +} + export function getDashboardRoute(environment: Environment) { if (isEdgeEnvironment(environment.Type)) { if (!environment.EdgeID) { diff --git a/app/react/portainer/settings/EdgeComputeView/SettingsOpenAMT/SettingsOpenAMT.tsx b/app/react/portainer/settings/EdgeComputeView/SettingsOpenAMT/SettingsOpenAMT.tsx index abff3bbab..760fbea7a 100644 --- a/app/react/portainer/settings/EdgeComputeView/SettingsOpenAMT/SettingsOpenAMT.tsx +++ b/app/react/portainer/settings/EdgeComputeView/SettingsOpenAMT/SettingsOpenAMT.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import { Formik, Field, Form } from 'formik'; import { Laptop } from 'lucide-react'; -import { OpenAMTConfiguration } from '@/portainer/hostmanagement/open-amt/model'; +import { OpenAMTConfiguration } from '@/react/edge/edge-devices/open-amt/types'; import { Switch } from '@@/form-components/SwitchField/Switch'; import { FormControl } from '@@/form-components/FormControl'; diff --git a/app/react/sidebar/EdgeComputeSidebar.tsx b/app/react/sidebar/EdgeComputeSidebar.tsx index 3b0563390..e7614898b 100644 --- a/app/react/sidebar/EdgeComputeSidebar.tsx +++ b/app/react/sidebar/EdgeComputeSidebar.tsx @@ -1,17 +1,13 @@ import { Box, Clock, LayoutGrid, Layers } from 'lucide-react'; +import { isBE } from '../portainer/feature-flags/feature-flags.service'; + import { SidebarItem } from './SidebarItem'; import { SidebarSection } from './SidebarSection'; export function EdgeComputeSidebar() { return ( - + {isBE && ( + + )} ); }