diff --git a/app/docker/views/containers/create/createcontainer.css b/app/docker/views/containers/create/createcontainer.css index 9dd901d60..26b8ffec6 100644 --- a/app/docker/views/containers/create/createcontainer.css +++ b/app/docker/views/containers/create/createcontainer.css @@ -6,15 +6,3 @@ .widget .edit-resources button { margin-left: 0; } - -.mt-20 { - margin-top: 20px; -} - -.mt-7 { - margin-top: 7px; -} - -.mt-10 { - margin-top: 10px; -} diff --git a/app/docker/views/containers/create/createcontainer.html b/app/docker/views/containers/create/createcontainer.html index 33ffd0a0b..4d60ac899 100644 --- a/app/docker/views/containers/create/createcontainer.html +++ b/app/docker/views/containers/create/createcontainer.html @@ -707,7 +707,7 @@
Resources
- +
-

Memory soft limit (MB)

+

Memory soft limit (MB)

- +
- +
-
+

Maximum CPU usage

diff --git a/app/edge/devices/components/AMTDevicesDatatable/AMTDevicesDatatable.tsx b/app/edge/EdgeDevices/EdgeDevicesView/AMTDevicesDatatable/AMTDevicesDatatable.tsx similarity index 93% rename from app/edge/devices/components/AMTDevicesDatatable/AMTDevicesDatatable.tsx rename to app/edge/EdgeDevices/EdgeDevicesView/AMTDevicesDatatable/AMTDevicesDatatable.tsx index f04856407..4aaec5656 100644 --- a/app/edge/devices/components/AMTDevicesDatatable/AMTDevicesDatatable.tsx +++ b/app/edge/EdgeDevices/EdgeDevicesView/AMTDevicesDatatable/AMTDevicesDatatable.tsx @@ -8,8 +8,8 @@ import { } from '@/portainer/components/datatables/components'; import { InnerDatatable } from '@/portainer/components/datatables/components/InnerDatatable'; import { Device } from '@/portainer/hostmanagement/open-amt/model'; -import { useAMTDevices } from '@/edge/devices/components/AMTDevicesDatatable/useAMTDevices'; -import { RowProvider } from '@/edge/devices/components/AMTDevicesDatatable/columns/RowContext'; +import { useAMTDevices } from '@/edge/EdgeDevices/EdgeDevicesView/AMTDevicesDatatable/useAMTDevices'; +import { RowProvider } from '@/edge/EdgeDevices/EdgeDevicesView/AMTDevicesDatatable/columns/RowContext'; import { EnvironmentId } from '@/portainer/environments/types'; import PortainerError from '@/portainer/error'; diff --git a/app/edge/devices/components/AMTDevicesDatatable/columns/RowContext.tsx b/app/edge/EdgeDevices/EdgeDevicesView/AMTDevicesDatatable/columns/RowContext.tsx similarity index 100% rename from app/edge/devices/components/AMTDevicesDatatable/columns/RowContext.tsx rename to app/edge/EdgeDevices/EdgeDevicesView/AMTDevicesDatatable/columns/RowContext.tsx diff --git a/app/edge/devices/components/AMTDevicesDatatable/columns/actions.tsx b/app/edge/EdgeDevices/EdgeDevicesView/AMTDevicesDatatable/columns/actions.tsx similarity index 95% rename from app/edge/devices/components/AMTDevicesDatatable/columns/actions.tsx rename to app/edge/EdgeDevices/EdgeDevicesView/AMTDevicesDatatable/columns/actions.tsx index 5801a9801..0082f5ae2 100644 --- a/app/edge/devices/components/AMTDevicesDatatable/columns/actions.tsx +++ b/app/edge/EdgeDevices/EdgeDevicesView/AMTDevicesDatatable/columns/actions.tsx @@ -9,8 +9,14 @@ import { confirmAsync } from '@/portainer/services/modal.service/confirm'; import { executeDeviceAction } from '@/portainer/hostmanagement/open-amt/open-amt.service'; import * as notifications from '@/portainer/services/notifications'; import { ActionsMenuTitle } from '@/portainer/components/datatables/components/ActionsMenuTitle'; -import { useRowContext } from '@/edge/devices/components/AMTDevicesDatatable/columns/RowContext'; -import { DeviceAction } from '@/edge/devices/types'; + +import { useRowContext } from './RowContext'; + +enum DeviceAction { + PowerOn = 'power on', + PowerOff = 'power off', + Restart = 'restart', +} export const actions: Column = { Header: 'Actions', diff --git a/app/edge/devices/components/AMTDevicesDatatable/columns/hostname.tsx b/app/edge/EdgeDevices/EdgeDevicesView/AMTDevicesDatatable/columns/hostname.tsx similarity index 100% rename from app/edge/devices/components/AMTDevicesDatatable/columns/hostname.tsx rename to app/edge/EdgeDevices/EdgeDevicesView/AMTDevicesDatatable/columns/hostname.tsx diff --git a/app/edge/devices/components/AMTDevicesDatatable/columns/index.tsx b/app/edge/EdgeDevices/EdgeDevicesView/AMTDevicesDatatable/columns/index.tsx similarity index 100% rename from app/edge/devices/components/AMTDevicesDatatable/columns/index.tsx rename to app/edge/EdgeDevices/EdgeDevicesView/AMTDevicesDatatable/columns/index.tsx diff --git a/app/edge/devices/components/AMTDevicesDatatable/columns/power-state.tsx b/app/edge/EdgeDevices/EdgeDevicesView/AMTDevicesDatatable/columns/power-state.tsx similarity index 81% rename from app/edge/devices/components/AMTDevicesDatatable/columns/power-state.tsx rename to app/edge/EdgeDevices/EdgeDevicesView/AMTDevicesDatatable/columns/power-state.tsx index cc5301e8a..c84b72024 100644 --- a/app/edge/devices/components/AMTDevicesDatatable/columns/power-state.tsx +++ b/app/edge/EdgeDevices/EdgeDevicesView/AMTDevicesDatatable/columns/power-state.tsx @@ -2,8 +2,27 @@ import { CellProps, Column } from 'react-table'; import clsx from 'clsx'; import { Device } from '@/portainer/hostmanagement/open-amt/model'; -import { useRowContext } from '@/edge/devices/components/AMTDevicesDatatable/columns/RowContext'; -import { PowerState, PowerStateCode } from '@/edge/devices/types'; + +import { useRowContext } from './RowContext'; + +enum PowerState { + Running = 'Running', + Sleep = 'Sleep', + Off = 'Off', + Hibernate = 'Hibernate', + PowerCycle = 'Power Cycle', +} + +enum PowerStateCode { + On = 2, + SleepLight = 3, + SleepDeep = 4, + OffHard = 6, + Hibernate = 7, + OffSoft = 8, + PowerCycle = 9, + OffHardGraceful = 13, +} export const powerState: Column = { Header: 'Power State', diff --git a/app/edge/devices/components/AMTDevicesDatatable/columns/status.tsx b/app/edge/EdgeDevices/EdgeDevicesView/AMTDevicesDatatable/columns/status.tsx similarity index 100% rename from app/edge/devices/components/AMTDevicesDatatable/columns/status.tsx rename to app/edge/EdgeDevices/EdgeDevicesView/AMTDevicesDatatable/columns/status.tsx diff --git a/app/edge/devices/components/AMTDevicesDatatable/useAMTDevices.tsx b/app/edge/EdgeDevices/EdgeDevicesView/AMTDevicesDatatable/useAMTDevices.tsx similarity index 100% rename from app/edge/devices/components/AMTDevicesDatatable/useAMTDevices.tsx rename to app/edge/EdgeDevices/EdgeDevicesView/AMTDevicesDatatable/useAMTDevices.tsx diff --git a/app/edge/devices/components/EdgeDevicesDatatable/EdgeDevicesDatatable.module.css b/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/EdgeDevicesDatatable.module.css similarity index 100% rename from app/edge/devices/components/EdgeDevicesDatatable/EdgeDevicesDatatable.module.css rename to app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/EdgeDevicesDatatable.module.css diff --git a/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/EdgeDevicesDatatable.tsx b/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/EdgeDevicesDatatable.tsx new file mode 100644 index 000000000..c327a0245 --- /dev/null +++ b/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/EdgeDevicesDatatable.tsx @@ -0,0 +1,269 @@ +import { useTable, useExpanded, useSortBy, useFilters } from 'react-table'; +import { useRowSelectColumn } from '@lineup-lite/hooks'; +import _ from 'lodash'; + +import { Environment } from '@/portainer/environments/types'; +import { PaginationControls } from '@/portainer/components/pagination-controls'; +import { + Table, + TableActions, + TableContainer, + TableHeaderRow, + TableRow, + TableSettingsMenu, + TableTitle, + TableTitleActions, +} from '@/portainer/components/datatables/components'; +import { multiple } from '@/portainer/components/datatables/components/filter-types'; +import { useTableSettings } from '@/portainer/components/datatables/components/useTableSettings'; +import { ColumnVisibilityMenu } from '@/portainer/components/datatables/components/ColumnVisibilityMenu'; +import { SearchBar } from '@/portainer/components/datatables/components/SearchBar'; +import { useRowSelect } from '@/portainer/components/datatables/components/useRowSelect'; +import { TableFooter } from '@/portainer/components/datatables/components/TableFooter'; +import { SelectedRowsCount } from '@/portainer/components/datatables/components/SelectedRowsCount'; +import { AMTDevicesDatatable } from '@/edge/EdgeDevices/EdgeDevicesView/AMTDevicesDatatable/AMTDevicesDatatable'; +import { TextTip } from '@/portainer/components/Tip/TextTip'; +import { EnvironmentGroup } from '@/portainer/environment-groups/types'; + +import { EdgeDevicesDatatableActions } from './EdgeDevicesDatatableActions'; +import { EdgeDevicesDatatableSettings } from './EdgeDevicesDatatableSettings'; +import { RowProvider } from './columns/RowContext'; +import { useColumns } from './columns'; +import styles from './EdgeDevicesDatatable.module.css'; +import { EdgeDeviceTableSettings, Pagination } from './types'; + +export interface EdgeDevicesTableProps { + storageKey: string; + isFdoEnabled: boolean; + isOpenAmtEnabled: boolean; + showWaitingRoomLink: boolean; + mpsServer: string; + dataset: Environment[]; + groups: EnvironmentGroup[]; + setLoadingMessage(message: string): void; + pagination: Pagination; + onChangePagination(pagination: Partial): void; + totalCount: number; + search: string; + onChangeSearch(search: string): void; +} + +export function EdgeDevicesDatatable({ + isFdoEnabled, + isOpenAmtEnabled, + showWaitingRoomLink, + mpsServer, + dataset, + onChangeSearch, + search, + groups, + setLoadingMessage, + pagination, + onChangePagination, + totalCount, +}: EdgeDevicesTableProps) { + const { settings, setTableSettings } = + useTableSettings(); + + const columns = useColumns(); + + const { + getTableProps, + getTableBodyProps, + headerGroups, + rows, + prepareRow, + selectedFlatRows, + allColumns, + setHiddenColumns, + } = useTable( + { + defaultCanFilter: false, + columns, + data: dataset, + filterTypes: { multiple }, + initialState: { + hiddenColumns: settings.hiddenColumns, + sortBy: [settings.sortBy], + }, + isRowSelectable() { + return true; + }, + autoResetExpanded: false, + autoResetSelectedRows: false, + getRowId(originalRow: Environment) { + return originalRow.Id.toString(); + }, + selectColumnWidth: 5, + }, + useFilters, + useSortBy, + useExpanded, + useRowSelect, + useRowSelectColumn + ); + + const columnsToHide = allColumns.filter((colInstance) => { + const columnDef = columns.find((c) => c.id === colInstance.id); + return columnDef?.canHide; + }); + + const tableProps = getTableProps(); + const tbodyProps = getTableBodyProps(); + + const someDeviceHasAMTActivated = dataset.some( + (environment) => + environment.AMTDeviceGUID && environment.AMTDeviceGUID !== '' + ); + + const groupsById = _.groupBy(groups, 'Id'); + + return ( +
+
+ + + + + columns={columnsToHide} + onChange={handleChangeColumnsVisibility} + value={settings.hiddenColumns} + /> + + + + + + + row.original)} + isFDOEnabled={isFdoEnabled} + isOpenAMTEnabled={isOpenAmtEnabled} + setLoadingMessage={setLoadingMessage} + showWaitingRoomLink={showWaitingRoomLink} + /> + + {isOpenAmtEnabled && someDeviceHasAMTActivated && ( +
+ + For the KVM function to work you need to have the MPS server + added to your trusted site list, browse to this{' '} + + site + + and add to your trusted site list + +
+ )} + + + + {headerGroups.map((headerGroup) => { + const { key, className, role, style } = + headerGroup.getHeaderGroupProps(); + return ( + + key={key} + className={className} + role={role} + style={style} + headers={headerGroup.headers} + onSortChange={handleSortChange} + /> + ); + })} + + + { + const group = groupsById[row.original.GroupId]; + + return ( + + + cells={row.cells} + key={key} + className={className} + role={role} + style={style} + /> + {row.isExpanded && ( + + + + )} + + ); + }} + /> + +
+ + +
+ + + gotoPage(p)} + totalCount={totalCount} + onPageLimitChange={handlePageSizeChange} + /> + +
+
+
+ ); + + function gotoPage(pageIndex: number) { + onChangePagination({ page: pageIndex }); + } + + function setPageSize(pageSize: number) { + onChangePagination({ pageLimit: pageSize }); + } + + function handlePageSizeChange(pageSize: number) { + setPageSize(pageSize); + setTableSettings((settings) => ({ ...settings, pageSize })); + } + + function handleChangeColumnsVisibility(hiddenColumns: string[]) { + setHiddenColumns(hiddenColumns); + setTableSettings((settings) => ({ ...settings, hiddenColumns })); + } + + function handleSearchBarChange(value: string) { + onChangeSearch(value); + } + + function handleSortChange(id: string, desc: boolean) { + setTableSettings((settings) => ({ + ...settings, + sortBy: { id, desc }, + })); + } +} diff --git a/app/edge/devices/components/EdgeDevicesDatatable/EdgeDevicesDatatableActions.tsx b/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/EdgeDevicesDatatableActions.tsx similarity index 100% rename from app/edge/devices/components/EdgeDevicesDatatable/EdgeDevicesDatatableActions.tsx rename to app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/EdgeDevicesDatatableActions.tsx diff --git a/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/EdgeDevicesDatatableContainer.tsx b/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/EdgeDevicesDatatableContainer.tsx new file mode 100644 index 000000000..275b76ce2 --- /dev/null +++ b/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/EdgeDevicesDatatableContainer.tsx @@ -0,0 +1,119 @@ +import { useState } from 'react'; + +import { + TableSettingsProvider, + useTableSettings, +} from '@/portainer/components/datatables/components/useTableSettings'; +import { useEnvironmentList } from '@/portainer/environments/queries'; +import { Environment } from '@/portainer/environments/types'; +import { useSearchBarState } from '@/portainer/components/datatables/components/SearchBar'; +import { useDebounce } from '@/portainer/hooks/useDebounce'; + +import { + EdgeDevicesDatatable, + EdgeDevicesTableProps, +} from './EdgeDevicesDatatable'; +import { EdgeDeviceTableSettings, Pagination } from './types'; + +export function EdgeDevicesDatatableContainer({ + ...props +}: Omit< + EdgeDevicesTableProps, + | 'dataset' + | 'pagination' + | 'onChangePagination' + | 'totalCount' + | 'search' + | 'onChangeSearch' +>) { + const defaultSettings = { + autoRefreshRate: 0, + hiddenQuickActions: [], + hiddenColumns: [], + pageSize: 10, + sortBy: { id: 'state', desc: false }, + }; + + const storageKey = 'edgeDevices'; + + return ( + + + {({ + environments, + pagination, + totalCount, + setPagination, + search, + setSearch, + }) => ( + + )} + + + ); +} + +interface LoaderProps { + storageKey: string; + children: (options: { + environments: Environment[]; + totalCount: number; + pagination: Pagination; + setPagination(value: Partial): void; + search: string; + setSearch: (value: string) => void; + }) => React.ReactNode; +} + +function Loader({ children, storageKey }: LoaderProps) { + const { settings } = useTableSettings(); + const [pagination, setPagination] = useState({ + pageLimit: settings.pageSize, + page: 1, + }); + + const [search, setSearch] = useSearchBarState(storageKey); + const debouncedSearchValue = useDebounce(search); + + const { environments, isLoading, totalCount } = useEnvironmentList( + { + edgeDeviceFilter: 'trusted', + search: debouncedSearchValue, + ...pagination, + }, + false, + settings.autoRefreshRate * 1000 + ); + + if (isLoading) { + return null; + } + + return ( + <> + {children({ + environments, + totalCount, + pagination, + setPagination: handleSetPagination, + search, + setSearch, + })} + + ); + + function handleSetPagination(value: Partial) { + setPagination((prev) => ({ ...prev, ...value })); + } +} diff --git a/app/edge/devices/components/EdgeDevicesDatatable/EdgeDevicesDatatableSettings.tsx b/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/EdgeDevicesDatatableSettings.tsx similarity index 90% rename from app/edge/devices/components/EdgeDevicesDatatable/EdgeDevicesDatatableSettings.tsx rename to app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/EdgeDevicesDatatableSettings.tsx index e48bd7a45..03ac90388 100644 --- a/app/edge/devices/components/EdgeDevicesDatatable/EdgeDevicesDatatableSettings.tsx +++ b/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/EdgeDevicesDatatableSettings.tsx @@ -1,6 +1,7 @@ import { TableSettingsMenuAutoRefresh } from '@/portainer/components/datatables/components/TableSettingsMenuAutoRefresh'; import { useTableSettings } from '@/portainer/components/datatables/components/useTableSettings'; -import { EdgeDeviceTableSettings } from '@/edge/devices/types'; + +import { EdgeDeviceTableSettings } from './types'; export function EdgeDevicesDatatableSettings() { const { settings, setTableSettings } = diff --git a/app/edge/devices/components/EdgeDevicesDatatable/columns/RowContext.tsx b/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/columns/RowContext.tsx similarity index 100% rename from app/edge/devices/components/EdgeDevicesDatatable/columns/RowContext.tsx rename to app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/columns/RowContext.tsx diff --git a/app/edge/devices/components/EdgeDevicesDatatable/columns/actions.tsx b/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/columns/actions.tsx similarity index 100% rename from app/edge/devices/components/EdgeDevicesDatatable/columns/actions.tsx rename to app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/columns/actions.tsx diff --git a/app/edge/devices/components/EdgeDevicesDatatable/columns/group.tsx b/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/columns/group.tsx similarity index 100% rename from app/edge/devices/components/EdgeDevicesDatatable/columns/group.tsx rename to app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/columns/group.tsx diff --git a/app/edge/devices/components/EdgeDevicesDatatable/columns/heartbeat.tsx b/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/columns/heartbeat.tsx similarity index 100% rename from app/edge/devices/components/EdgeDevicesDatatable/columns/heartbeat.tsx rename to app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/columns/heartbeat.tsx diff --git a/app/edge/devices/components/EdgeDevicesDatatable/columns/index.tsx b/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/columns/index.tsx similarity index 100% rename from app/edge/devices/components/EdgeDevicesDatatable/columns/index.tsx rename to app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/columns/index.tsx diff --git a/app/edge/devices/components/EdgeDevicesDatatable/columns/name.tsx b/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/columns/name.tsx similarity index 90% rename from app/edge/devices/components/EdgeDevicesDatatable/columns/name.tsx rename to app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/columns/name.tsx index 38ed469fa..2a8551f7b 100644 --- a/app/edge/devices/components/EdgeDevicesDatatable/columns/name.tsx +++ b/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/columns/name.tsx @@ -3,7 +3,8 @@ import { CellProps, Column } from 'react-table'; import { Environment } from '@/portainer/environments/types'; import { Link } from '@/portainer/components/Link'; import { ExpandingCell } from '@/portainer/components/datatables/components/ExpandingCell'; -import { useRowContext } from '@/edge/devices/components/EdgeDevicesDatatable/columns/RowContext'; + +import { useRowContext } from './RowContext'; export const name: Column = { Header: 'Name', diff --git a/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/types.ts b/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/types.ts new file mode 100644 index 000000000..fa314b2cd --- /dev/null +++ b/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesDatatable/types.ts @@ -0,0 +1,17 @@ +import { + PaginationTableSettings, + RefreshableTableSettings, + SettableColumnsTableSettings, + SortableTableSettings, +} from '@/portainer/components/datatables/types'; + +export interface Pagination { + pageLimit: number; + page: number; +} + +export interface EdgeDeviceTableSettings + extends SortableTableSettings, + PaginationTableSettings, + SettableColumnsTableSettings, + RefreshableTableSettings {} diff --git a/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesView.tsx b/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesView.tsx new file mode 100644 index 000000000..11ab06296 --- /dev/null +++ b/app/edge/EdgeDevices/EdgeDevicesView/EdgeDevicesView.tsx @@ -0,0 +1,58 @@ +import { useState } from 'react'; + +import { PageHeader } from '@/portainer/components/PageHeader'; +import { useSettings } from '@/portainer/settings/queries'; +import { useGroups } from '@/portainer/environment-groups/queries'; +import { r2a } from '@/react-tools/react2angular'; +import { ViewLoading } from '@/portainer/components/ViewLoading'; + +import { EdgeDevicesDatatableContainer } from './EdgeDevicesDatatable/EdgeDevicesDatatableContainer'; + +export function EdgeDevicesView() { + const [loadingMessage, setLoadingMessage] = useState(''); + + const settingsQuery = useSettings(); + const groupsQuery = useGroups(); + + if (!settingsQuery.data || !groupsQuery.data) { + return null; + } + + const settings = settingsQuery.data; + + return ( + <> + + + {loadingMessage ? ( + + ) : ( + + )} + + ); +} + +export const EdgeDevicesViewAngular = r2a(EdgeDevicesView, []); diff --git a/app/edge/EdgeDevices/EdgeDevicesView/index.ts b/app/edge/EdgeDevices/EdgeDevicesView/index.ts new file mode 100644 index 000000000..ed950962d --- /dev/null +++ b/app/edge/EdgeDevices/EdgeDevicesView/index.ts @@ -0,0 +1 @@ +export { EdgeDevicesView, EdgeDevicesViewAngular } from './EdgeDevicesView'; diff --git a/app/edge/__module.js b/app/edge/__module.js index d1fc1976c..6bcf5112b 100644 --- a/app/edge/__module.js +++ b/app/edge/__module.js @@ -1,14 +1,15 @@ import angular from 'angular'; import edgeStackModule from './views/edge-stacks'; -import edgeDevicesModule from './devices'; import { componentsModule } from './components'; import { WaitingRoomViewAngular } from './EdgeDevices/WaitingRoomView'; import { reactModule } from './react'; +import { EdgeDevicesViewAngular } from './EdgeDevices/EdgeDevicesView'; angular - .module('portainer.edge', [edgeStackModule, edgeDevicesModule, componentsModule, reactModule]) + .module('portainer.edge', [edgeStackModule, componentsModule, reactModule]) .component('waitingRoomView', WaitingRoomViewAngular) + .component('edgeDevicesView', EdgeDevicesViewAngular) .config(function config($stateRegistryProvider) { const edge = { name: 'edge', @@ -113,7 +114,7 @@ angular }, }; - const edgeDevices = { + $stateRegistryProvider.register({ name: 'edge.devices', url: '/devices', views: { @@ -121,7 +122,7 @@ angular component: 'edgeDevicesView', }, }, - }; + }); if (process.env.PORTAINER_EDITION === 'BE') { $stateRegistryProvider.register({ @@ -148,6 +149,4 @@ angular $stateRegistryProvider.register(edgeJobs); $stateRegistryProvider.register(edgeJob); $stateRegistryProvider.register(edgeJobCreation); - - $stateRegistryProvider.register(edgeDevices); }); diff --git a/app/edge/components/EdgeCheckInIntervalField.tsx b/app/edge/components/EdgeCheckInIntervalField.tsx index 056dca1e2..d753af6a8 100644 --- a/app/edge/components/EdgeCheckInIntervalField.tsx +++ b/app/edge/components/EdgeCheckInIntervalField.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'; import { FormControl } from '@/portainer/components/form-components/FormControl'; import { Select } from '@/portainer/components/form-components/Input'; -import { useSettings } from '@/portainer/settings/settings.service'; +import { useSettings } from '@/portainer/settings/queries'; import { r2a } from '@/react-tools/react2angular'; interface Props { diff --git a/app/edge/components/EdgeScriptForm/EdgeScriptForm.tsx b/app/edge/components/EdgeScriptForm/EdgeScriptForm.tsx index dcf0da033..d4824bd9e 100644 --- a/app/edge/components/EdgeScriptForm/EdgeScriptForm.tsx +++ b/app/edge/components/EdgeScriptForm/EdgeScriptForm.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import { useStatus } from '@/portainer/services/api/status.service'; import { r2a } from '@/react-tools/react2angular'; -import { useSettings } from '@/portainer/settings/settings.service'; +import { useSettings } from '@/portainer/settings/queries'; import { EdgePropertiesForm } from './EdgePropertiesForm'; import { ScriptTabs } from './ScriptTabs'; diff --git a/app/edge/devices/components/EdgeDevicesDatatable/EdgeDevicesDatatable.tsx b/app/edge/devices/components/EdgeDevicesDatatable/EdgeDevicesDatatable.tsx deleted file mode 100644 index 603d3370c..000000000 --- a/app/edge/devices/components/EdgeDevicesDatatable/EdgeDevicesDatatable.tsx +++ /dev/null @@ -1,278 +0,0 @@ -import { useEffect } from 'react'; -import { - useTable, - useExpanded, - useSortBy, - useFilters, - useGlobalFilter, - usePagination, -} from 'react-table'; -import { useRowSelectColumn } from '@lineup-lite/hooks'; -import _ from 'lodash'; - -import { Environment } from '@/portainer/environments/types'; -import { PaginationControls } from '@/portainer/components/pagination-controls'; -import { - Table, - TableActions, - TableContainer, - TableHeaderRow, - TableRow, - TableSettingsMenu, - TableTitle, - TableTitleActions, -} from '@/portainer/components/datatables/components'; -import { multiple } from '@/portainer/components/datatables/components/filter-types'; -import { useTableSettings } from '@/portainer/components/datatables/components/useTableSettings'; -import { ColumnVisibilityMenu } from '@/portainer/components/datatables/components/ColumnVisibilityMenu'; -import { useRepeater } from '@/portainer/components/datatables/components/useRepeater'; -import { useDebounce } from '@/portainer/hooks/useDebounce'; -import { - useSearchBarState, - SearchBar, -} from '@/portainer/components/datatables/components/SearchBar'; -import { useRowSelect } from '@/portainer/components/datatables/components/useRowSelect'; -import { TableFooter } from '@/portainer/components/datatables/components/TableFooter'; -import { SelectedRowsCount } from '@/portainer/components/datatables/components/SelectedRowsCount'; -import { EdgeDeviceTableSettings } from '@/edge/devices/types'; -import { EdgeDevicesDatatableSettings } from '@/edge/devices/components/EdgeDevicesDatatable/EdgeDevicesDatatableSettings'; -import { EdgeDevicesDatatableActions } from '@/edge/devices/components/EdgeDevicesDatatable/EdgeDevicesDatatableActions'; -import { AMTDevicesDatatable } from '@/edge/devices/components/AMTDevicesDatatable/AMTDevicesDatatable'; -import { TextTip } from '@/portainer/components/Tip/TextTip'; -import { EnvironmentGroup } from '@/portainer/environment-groups/types'; - -import { RowProvider } from './columns/RowContext'; -import { useColumns } from './columns'; -import styles from './EdgeDevicesDatatable.module.css'; - -export interface EdgeDevicesTableProps { - storageKey: string; - isEnabled: boolean; - isFdoEnabled: boolean; - isOpenAmtEnabled: boolean; - showWaitingRoomLink: boolean; - mpsServer: string; - dataset: Environment[]; - groups: EnvironmentGroup[]; - onRefresh(): Promise; - setLoadingMessage(message: string): void; -} - -export function EdgeDevicesDatatable({ - storageKey, - isFdoEnabled, - isOpenAmtEnabled, - showWaitingRoomLink, - mpsServer, - dataset, - groups, - onRefresh, - setLoadingMessage, -}: EdgeDevicesTableProps) { - const { settings, setTableSettings } = - useTableSettings(); - const [searchBarValue, setSearchBarValue] = useSearchBarState(storageKey); - - const columns = useColumns(); - - useRepeater(settings.autoRefreshRate, onRefresh); - - const { - getTableProps, - getTableBodyProps, - headerGroups, - page, - prepareRow, - selectedFlatRows, - allColumns, - gotoPage, - setPageSize, - setHiddenColumns, - setGlobalFilter, - state: { pageIndex, pageSize }, - } = useTable( - { - defaultCanFilter: false, - columns, - data: dataset, - filterTypes: { multiple }, - initialState: { - pageSize: settings.pageSize || 10, - hiddenColumns: settings.hiddenColumns, - sortBy: [settings.sortBy], - globalFilter: searchBarValue, - }, - isRowSelectable() { - return true; - }, - autoResetExpanded: false, - autoResetSelectedRows: false, - getRowId(originalRow: Environment) { - return originalRow.Id.toString(); - }, - selectColumnWidth: 5, - }, - useFilters, - useGlobalFilter, - useSortBy, - useExpanded, - usePagination, - useRowSelect, - useRowSelectColumn - ); - - const debouncedSearchValue = useDebounce(searchBarValue); - - useEffect(() => { - setGlobalFilter(debouncedSearchValue); - }, [debouncedSearchValue, setGlobalFilter]); - - const columnsToHide = allColumns.filter((colInstance) => { - const columnDef = columns.find((c) => c.id === colInstance.id); - return columnDef?.canHide; - }); - - const tableProps = getTableProps(); - const tbodyProps = getTableBodyProps(); - - const someDeviceHasAMTActivated = dataset.some( - (environment) => - environment.AMTDeviceGUID && environment.AMTDeviceGUID !== '' - ); - - const groupsById = _.groupBy(groups, 'Id'); - - return ( - - - - - columns={columnsToHide} - onChange={handleChangeColumnsVisibility} - value={settings.hiddenColumns} - /> - - - - - - - - - row.original)} - isFDOEnabled={isFdoEnabled} - isOpenAMTEnabled={isOpenAmtEnabled} - setLoadingMessage={setLoadingMessage} - showWaitingRoomLink={showWaitingRoomLink} - /> - - - {someDeviceHasAMTActivated && ( -
- - For the KVM function to work you need to have the MPS server added - to your trusted site list, browse to this{' '} - - site - {' '} - and add to your trusted site list - -
- )} - - - - - - {headerGroups.map((headerGroup) => { - const { key, className, role, style } = - headerGroup.getHeaderGroupProps(); - - return ( - - key={key} - className={className} - role={role} - style={style} - headers={headerGroup.headers} - onSortChange={handleSortChange} - /> - ); - })} - - - {page.map((row) => { - prepareRow(row); - const { key, className, role, style } = row.getRowProps(); - const group = groupsById[row.original.GroupId]; - return ( - - - cells={row.cells} - key={key} - className={className} - role={role} - style={style} - /> - {row.isExpanded && ( - - - - )} - - ); - })} - -
- - -
- - - - gotoPage(p - 1)} - totalCount={dataset.length} - onPageLimitChange={handlePageSizeChange} - /> - -
- ); - - function handlePageSizeChange(pageSize: number) { - setPageSize(pageSize); - setTableSettings((settings) => ({ ...settings, pageSize })); - } - - function handleChangeColumnsVisibility(hiddenColumns: string[]) { - setHiddenColumns(hiddenColumns); - setTableSettings((settings) => ({ ...settings, hiddenColumns })); - } - - function handleSearchBarChange(value: string) { - setSearchBarValue(value); - } - - function handleSortChange(id: string, desc: boolean) { - setTableSettings((settings) => ({ - ...settings, - sortBy: { id, desc }, - })); - } -} diff --git a/app/edge/devices/components/EdgeDevicesDatatable/EdgeDevicesDatatableContainer.tsx b/app/edge/devices/components/EdgeDevicesDatatable/EdgeDevicesDatatableContainer.tsx deleted file mode 100644 index 9427ae0e8..000000000 --- a/app/edge/devices/components/EdgeDevicesDatatable/EdgeDevicesDatatableContainer.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { react2angular } from '@/react-tools/react2angular'; -import { TableSettingsProvider } from '@/portainer/components/datatables/components/useTableSettings'; - -import { - EdgeDevicesDatatable, - EdgeDevicesTableProps, -} from './EdgeDevicesDatatable'; - -export function EdgeDevicesDatatableContainer({ - ...props -}: EdgeDevicesTableProps) { - const defaultSettings = { - autoRefreshRate: 0, - hiddenQuickActions: [], - hiddenColumns: [], - pageSize: 10, - sortBy: { id: 'state', desc: false }, - }; - - const storageKey = 'edgeDevices'; - - return ( - - {/* eslint-disable-next-line react/jsx-props-no-spreading */} - - - ); -} - -export const EdgeDevicesDatatableAngular = react2angular( - EdgeDevicesDatatableContainer, - [ - 'groups', - 'dataset', - 'onRefresh', - 'setLoadingMessage', - 'isFdoEnabled', - 'showWaitingRoomLink', - 'isOpenAmtEnabled', - 'mpsServer', - ] -); diff --git a/app/edge/devices/index.ts b/app/edge/devices/index.ts deleted file mode 100644 index 674188140..000000000 --- a/app/edge/devices/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import angular from 'angular'; - -import { EdgeDevicesDatatableAngular } from '@/edge/devices/components/EdgeDevicesDatatable/EdgeDevicesDatatableContainer'; - -export default angular - .module('portainer.edge.devices', []) - .component('edgeDevicesDatatable', EdgeDevicesDatatableAngular).name; diff --git a/app/edge/devices/types.ts b/app/edge/devices/types.ts deleted file mode 100644 index f27203f1c..000000000 --- a/app/edge/devices/types.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { - PaginationTableSettings, - RefreshableTableSettings, - SettableColumnsTableSettings, - SortableTableSettings, -} from '@/portainer/components/datatables/types'; - -export interface EdgeDeviceTableSettings - extends SortableTableSettings, - PaginationTableSettings, - SettableColumnsTableSettings, - RefreshableTableSettings {} - -export interface FDOProfilesTableSettings - extends SortableTableSettings, - PaginationTableSettings {} - -export enum DeviceAction { - PowerOn = 'power on', - PowerOff = 'power off', - Restart = 'restart', -} - -export enum PowerState { - Running = 'Running', - Sleep = 'Sleep', - Off = 'Off', - Hibernate = 'Hibernate', - PowerCycle = 'Power Cycle', -} - -export enum PowerStateCode { - On = 2, - SleepLight = 3, - SleepDeep = 4, - OffHard = 6, - Hibernate = 7, - OffSoft = 8, - PowerCycle = 9, - OffHardGraceful = 13, -} diff --git a/app/edge/views/edge-devices/edgeDevicesView/edgeDevicesView.html b/app/edge/views/edge-devices/edgeDevicesView/edgeDevicesView.html deleted file mode 100644 index 5f897cf05..000000000 --- a/app/edge/views/edge-devices/edgeDevicesView/edgeDevicesView.html +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - Edge Devices - - -
-
-
-
-
-
-
- - {{ $ctrl.loadingMessage }} - - -
- -
-
- -
-
diff --git a/app/edge/views/edge-devices/edgeDevicesView/edgeDevicesViewController.js b/app/edge/views/edge-devices/edgeDevicesView/edgeDevicesViewController.js deleted file mode 100644 index a12f5c9e2..000000000 --- a/app/edge/views/edge-devices/edgeDevicesView/edgeDevicesViewController.js +++ /dev/null @@ -1,55 +0,0 @@ -import { getEndpoints } from 'Portainer/environments/environment.service'; - -angular.module('portainer.edge').controller('EdgeDevicesViewController', EdgeDevicesViewController); -/* @ngInject */ -export function EdgeDevicesViewController($q, $async, EndpointService, GroupService, SettingsService, ModalService, Notifications) { - var ctrl = this; - - ctrl.edgeDevices = []; - - this.getEnvironments = function () { - return $async(async () => { - try { - const [endpointsResponse, groups] = await Promise.all([ - getEndpoints(0, 100, { - edgeDeviceFilter: 'trusted', - }), - GroupService.groups(), - ]); - ctrl.groups = groups; - ctrl.edgeDevices = endpointsResponse.value; - } catch (err) { - Notifications.error('Failure', err, 'Unable to retrieve edge devices'); - ctrl.edgeDevices = []; - } - }); - }; - - this.getSettings = function () { - return $async(async () => { - try { - const settings = await SettingsService.settings(); - - ctrl.isFDOEnabled = settings && settings.EnableEdgeComputeFeatures && settings.fdoConfiguration && settings.fdoConfiguration.enabled; - ctrl.showWaitingRoomLink = process.env.PORTAINER_EDITION === 'BE' && settings && settings.EnableEdgeComputeFeatures && !settings.TrustOnFirstConnect; - ctrl.isOpenAMTEnabled = settings && settings.EnableEdgeComputeFeatures && settings.openAMTConfiguration && settings.openAMTConfiguration.enabled; - ctrl.mpsServer = ctrl.isOpenAMTEnabled ? settings.openAMTConfiguration.mpsServer : ''; - } catch (err) { - Notifications.error('Failure', err, 'Unable to retrieve settings'); - } - }); - }; - - this.setLoadingMessage = function (message) { - return $async(async () => { - ctrl.loadingMessage = message; - }); - }; - - function initView() { - ctrl.getEnvironments(); - ctrl.getSettings(); - } - - initView(); -} diff --git a/app/edge/views/edge-devices/edgeDevicesView/index.js b/app/edge/views/edge-devices/edgeDevicesView/index.js deleted file mode 100644 index 318850e02..000000000 --- a/app/edge/views/edge-devices/edgeDevicesView/index.js +++ /dev/null @@ -1,8 +0,0 @@ -import angular from 'angular'; - -import { EdgeDevicesViewController } from './edgeDevicesViewController'; - -angular.module('portainer.edge').component('edgeDevicesView', { - templateUrl: './edgeDevicesView.html', - controller: EdgeDevicesViewController, -}); diff --git a/app/portainer/components/PageHeader/index.ts b/app/portainer/components/PageHeader/index.ts index 73071d4f9..ad1421b13 100644 --- a/app/portainer/components/PageHeader/index.ts +++ b/app/portainer/components/PageHeader/index.ts @@ -8,7 +8,7 @@ import { HeaderTitle, HeaderTitleAngular } from './HeaderTitle'; export { PageHeader, Breadcrumbs, HeaderContainer, HeaderContent, HeaderTitle }; -export default angular +export const pageHeaderModule = angular .module('portainer.app.components.header', []) .component('rdHeader', HeaderAngular) diff --git a/app/portainer/components/ViewLoading/ViewLoading.module.css b/app/portainer/components/ViewLoading/ViewLoading.module.css new file mode 100644 index 000000000..09cd87ddd --- /dev/null +++ b/app/portainer/components/ViewLoading/ViewLoading.module.css @@ -0,0 +1,13 @@ +.root { + width: 100%; + height: 100%; + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.message { + margin-top: 25px; +} diff --git a/app/portainer/components/ViewLoading/ViewLoading.stories.tsx b/app/portainer/components/ViewLoading/ViewLoading.stories.tsx new file mode 100644 index 000000000..e9c8c3743 --- /dev/null +++ b/app/portainer/components/ViewLoading/ViewLoading.stories.tsx @@ -0,0 +1,21 @@ +import { Meta, Story } from '@storybook/react'; + +import { ViewLoading } from './ViewLoading'; + +export default { + component: ViewLoading, + title: 'Components/ViewLoading', +} as Meta; + +interface Args { + message: string; +} + +function Template({ message }: Args) { + return ; +} + +export const Example: Story = Template.bind({}); +Example.args = { + message: 'Loading...', +}; diff --git a/app/portainer/components/ViewLoading/ViewLoading.tsx b/app/portainer/components/ViewLoading/ViewLoading.tsx new file mode 100644 index 000000000..42dd968bb --- /dev/null +++ b/app/portainer/components/ViewLoading/ViewLoading.tsx @@ -0,0 +1,30 @@ +import clsx from 'clsx'; + +import { r2a } from '@/react-tools/react2angular'; + +import styles from './ViewLoading.module.css'; + +interface Props { + message?: string; +} + +export function ViewLoading({ message }: Props) { + return ( +
+
+
+
+
+
+
+ {message && ( + + {message} + + + )} +
+ ); +} + +export const ViewLoadingAngular = r2a(ViewLoading, ['message']); diff --git a/app/portainer/components/ViewLoading/index.ts b/app/portainer/components/ViewLoading/index.ts new file mode 100644 index 000000000..2be09c0c9 --- /dev/null +++ b/app/portainer/components/ViewLoading/index.ts @@ -0,0 +1 @@ +export { ViewLoading, ViewLoadingAngular } from './ViewLoading'; diff --git a/app/portainer/components/index.js b/app/portainer/components/index.js index 4b7589e3d..cd4366b9f 100644 --- a/app/portainer/components/index.js +++ b/app/portainer/components/index.js @@ -8,17 +8,19 @@ import porAccessManagementModule from './accessManagement'; import formComponentsModule from './form-components'; import widgetModule from './widget'; import boxSelectorModule from './BoxSelector'; -import headerModule from './PageHeader'; +import { pageHeaderModule } from './PageHeader'; import { ReactExampleAngular } from './ReactExample'; import { TooltipAngular } from './Tip/Tooltip'; import { beFeatureIndicatorAngular } from './BEFeatureIndicator'; import { InformationPanelAngular } from './InformationPanel'; import { ForcePasswordUpdateHintAngular, PasswordCheckHintAngular } from './PasswordCheckHint'; +import { ViewLoadingAngular } from './ViewLoading'; export default angular - .module('portainer.app.components', [headerModule, boxSelectorModule, widgetModule, sidebarModule, gitFormModule, porAccessManagementModule, formComponentsModule]) + .module('portainer.app.components', [pageHeaderModule, boxSelectorModule, widgetModule, sidebarModule, gitFormModule, porAccessManagementModule, formComponentsModule]) .component('informationPanel', InformationPanelAngular) + .component('viewLoading', ViewLoadingAngular) .component('portainerTooltip', TooltipAngular) .component('reactExample', ReactExampleAngular) .component('beFeatureIndicator', beFeatureIndicatorAngular) diff --git a/app/portainer/components/pagination-controls/ItemsPerPageSelector.tsx b/app/portainer/components/pagination-controls/ItemsPerPageSelector.tsx index 20ad2e0c9..8fdbacaba 100644 --- a/app/portainer/components/pagination-controls/ItemsPerPageSelector.tsx +++ b/app/portainer/components/pagination-controls/ItemsPerPageSelector.tsx @@ -1,7 +1,7 @@ interface Props { value: number; onChange(value: number): void; - showAll: boolean; + showAll?: boolean; } export function ItemsPerPageSelector({ value, onChange, showAll }: Props) { diff --git a/app/portainer/components/pagination-controls/PageButton.tsx b/app/portainer/components/pagination-controls/PageButton.tsx index 6c8080c7f..6bcd6a757 100644 --- a/app/portainer/components/pagination-controls/PageButton.tsx +++ b/app/portainer/components/pagination-controls/PageButton.tsx @@ -21,6 +21,7 @@ export function PageButton({ diff --git a/app/portainer/components/pagination-controls/PageInput.tsx b/app/portainer/components/pagination-controls/PageInput.tsx new file mode 100644 index 000000000..d8b230dd9 --- /dev/null +++ b/app/portainer/components/pagination-controls/PageInput.tsx @@ -0,0 +1,58 @@ +import { useFormik } from 'formik'; +import { ChangeEvent, KeyboardEvent } from 'react'; +import { object, number } from 'yup'; + +import { Button } from '../Button'; +import { Input } from '../form-components/Input'; + +interface Values { + page: number | ''; +} + +interface Props { + onChange(page: number): void; + totalPages: number; +} + +export function PageInput({ onChange, totalPages }: Props) { + const { handleSubmit, setFieldValue, values, isValid } = useFormik({ + initialValues: { page: '' }, + onSubmit: async ({ page }) => page && onChange(page), + validateOnMount: true, + validationSchema: () => + object({ page: number().required().max(totalPages).min(1) }), + }); + + return ( +
+ + + +
+ ); + + function preventNotNumber(e: KeyboardEvent) { + if (e.key.match(/^\D$/)) { + e.preventDefault(); + } + } + + function handleChange(e: ChangeEvent) { + const value = parseInt(e.target.value, 10); + setFieldValue('page', Number.isNaN(value) ? '' : value); + } +} diff --git a/app/portainer/components/pagination-controls/PageSelector.tsx b/app/portainer/components/pagination-controls/PageSelector.tsx index f298ec303..728a54791 100644 --- a/app/portainer/components/pagination-controls/PageSelector.tsx +++ b/app/portainer/components/pagination-controls/PageSelector.tsx @@ -1,5 +1,6 @@ import { generatePagesArray } from './generatePagesArray'; import { PageButton } from './PageButton'; +import { PageInput } from './PageInput'; interface Props { boundaryLinks?: boolean; @@ -9,6 +10,7 @@ interface Props { onPageChange(page: number): void; totalCount: number; maxSize: number; + isInputVisible?: boolean; } export function PageSelector({ @@ -19,6 +21,7 @@ export function PageSelector({ maxSize = 5, directionLinks = true, boundaryLinks = false, + isInputVisible = false, }: Props) { const pages = generatePagesArray( currentPage, @@ -33,55 +36,63 @@ export function PageSelector({ } return ( -
    - {boundaryLinks ? ( - - « - - ) : null} - {directionLinks ? ( - - ‹ - - ) : null} - {pages.map((pageNumber, index) => ( - - {pageNumber} - - ))} + <> + {isInputVisible && ( + onPageChange(page)} + totalPages={Math.ceil(totalCount / itemsPerPage)} + /> + )} +
      + {boundaryLinks ? ( + + « + + ) : null} + {directionLinks ? ( + + ‹ + + ) : null} + {pages.map((pageNumber, index) => ( + + {pageNumber} + + ))} - {directionLinks ? ( - - › - - ) : null} - {boundaryLinks ? ( - - » - - ) : null} -
    + {directionLinks ? ( + + › + + ) : null} + {boundaryLinks ? ( + + » + + ) : null} +
+ ); } diff --git a/app/portainer/components/pagination-controls/PaginationControls.tsx b/app/portainer/components/pagination-controls/PaginationControls.tsx index 83c9a0be4..d482ba7e8 100644 --- a/app/portainer/components/pagination-controls/PaginationControls.tsx +++ b/app/portainer/components/pagination-controls/PaginationControls.tsx @@ -6,8 +6,9 @@ interface Props { onPageLimitChange(value: number): void; page: number; pageLimit: number; - showAll: boolean; + showAll?: boolean; totalCount: number; + isPageInputVisible?: boolean; } export function PaginationControls({ @@ -17,15 +18,17 @@ export function PaginationControls({ showAll, onPageChange, totalCount, + isPageInputVisible, }: Props) { return (
-
+
+ {pageLimit !== 0 && ( )} - +
); diff --git a/app/portainer/environments/queries.ts b/app/portainer/environments/queries.ts index 977e402b1..6f03c9ac0 100644 --- a/app/portainer/environments/queries.ts +++ b/app/portainer/environments/queries.ts @@ -14,9 +14,11 @@ interface Query extends EnvironmentsQueryParams { pageLimit?: number; } -export function useEnvironmentList(query: Query = {}, refetchOffline = false) { - const { page = 1, pageLimit = 100 } = query; - +export function useEnvironmentList( + { page = 1, pageLimit = 100, ...query }: Query = {}, + refetchOffline = false, + refreshRate = 0 +) { const { isLoading, data } = useQuery( ['environments', { page, pageLimit, ...query }], async () => { @@ -26,6 +28,10 @@ export function useEnvironmentList(query: Query = {}, refetchOffline = false) { { keepPreviousData: true, refetchInterval: (data) => { + if (refreshRate) { + return refreshRate; + } + if (!data || !refetchOffline) { return false; } diff --git a/app/portainer/settings/edge-compute/AutomaticEdgeEnvCreation/AutoEnvCreationSettingsForm.tsx b/app/portainer/settings/edge-compute/AutomaticEdgeEnvCreation/AutoEnvCreationSettingsForm.tsx index 0d9a045fb..191f1d232 100644 --- a/app/portainer/settings/edge-compute/AutomaticEdgeEnvCreation/AutoEnvCreationSettingsForm.tsx +++ b/app/portainer/settings/edge-compute/AutomaticEdgeEnvCreation/AutoEnvCreationSettingsForm.tsx @@ -8,7 +8,7 @@ import { FormSectionTitle } from '@/portainer/components/form-components/FormSec import { Input } from '@/portainer/components/form-components/Input'; import { baseHref } from '@/portainer/helpers/pathHelper'; import { notifySuccess } from '@/portainer/services/notifications'; -import { useUpdateSettingsMutation } from '@/portainer/settings/settings.service'; +import { useUpdateSettingsMutation } from '@/portainer/settings/queries'; import { Settings } from '../types'; diff --git a/app/portainer/settings/edge-compute/AutomaticEdgeEnvCreation/AutomaticEdgeEnvCreation.tsx b/app/portainer/settings/edge-compute/AutomaticEdgeEnvCreation/AutomaticEdgeEnvCreation.tsx index f498c443e..84aa18fb0 100644 --- a/app/portainer/settings/edge-compute/AutomaticEdgeEnvCreation/AutomaticEdgeEnvCreation.tsx +++ b/app/portainer/settings/edge-compute/AutomaticEdgeEnvCreation/AutomaticEdgeEnvCreation.tsx @@ -4,8 +4,7 @@ import { useEffect } from 'react'; import { Widget, WidgetBody, WidgetTitle } from '@/portainer/components/widget'; import { EdgeScriptForm } from '@/edge/components/EdgeScriptForm'; import { generateKey } from '@/portainer/environments/environment.service/edge'; - -import { useSettings } from '../../settings.service'; +import { useSettings } from '@/portainer/settings/queries'; import { AutoEnvCreationSettingsForm } from './AutoEnvCreationSettingsForm'; diff --git a/app/portainer/settings/edge-compute/EdgeComputeSettingsView.tsx b/app/portainer/settings/edge-compute/EdgeComputeSettingsView.tsx index 261aa282c..31f83425e 100644 --- a/app/portainer/settings/edge-compute/EdgeComputeSettingsView.tsx +++ b/app/portainer/settings/edge-compute/EdgeComputeSettingsView.tsx @@ -1,6 +1,6 @@ import { r2a } from '@/react-tools/react2angular'; -import { Settings } from '../settings.service'; +import { Settings } from '../types'; import { EdgeComputeSettings } from './EdgeComputeSettings'; import { AutomaticEdgeEnvCreation } from './AutomaticEdgeEnvCreation'; diff --git a/app/portainer/settings/edge-compute/FDOProfilesDatatable/FDOProfilesDatatable.tsx b/app/portainer/settings/edge-compute/FDOProfilesDatatable/FDOProfilesDatatable.tsx index a5499e2ff..6f8739ab5 100644 --- a/app/portainer/settings/edge-compute/FDOProfilesDatatable/FDOProfilesDatatable.tsx +++ b/app/portainer/settings/edge-compute/FDOProfilesDatatable/FDOProfilesDatatable.tsx @@ -17,11 +17,18 @@ import { TableRow, TableTitle, } from '@/portainer/components/datatables/components'; -import { FDOProfilesTableSettings } from '@/edge/devices/types'; -import { useFDOProfiles } from '@/portainer/settings/edge-compute/FDOProfilesDatatable/useFDOProfiles'; +import { + PaginationTableSettings, + SortableTableSettings, +} from '@/portainer/components/datatables/types'; +import { useFDOProfiles } from './useFDOProfiles'; import { useColumns } from './columns'; +export interface FDOProfilesTableSettings + extends SortableTableSettings, + PaginationTableSettings {} + export interface FDOProfilesDatatableProps { isFDOEnabled: boolean; } diff --git a/app/portainer/settings/queries.ts b/app/portainer/settings/queries.ts new file mode 100644 index 000000000..ec379d21c --- /dev/null +++ b/app/portainer/settings/queries.ts @@ -0,0 +1,32 @@ +import { useMutation, useQuery, useQueryClient } from 'react-query'; + +import { getSettings, updateSettings } from './settings.service'; +import { Settings } from './types'; + +export function useSettings(select?: (settings: Settings) => T) { + return useQuery(['settings'], getSettings, { + select, + meta: { + error: { + title: 'Failure', + message: 'Unable to retrieve settings', + }, + }, + }); +} + +export function useUpdateSettingsMutation() { + const queryClient = useQueryClient(); + + return useMutation(updateSettings, { + onSuccess() { + return queryClient.invalidateQueries(['settings']); + }, + meta: { + error: { + title: 'Failure', + message: 'Unable to update settings', + }, + }, + }); +} diff --git a/app/portainer/settings/settings.service.ts b/app/portainer/settings/settings.service.ts index 492acaacc..e2dd857b3 100644 --- a/app/portainer/settings/settings.service.ts +++ b/app/portainer/settings/settings.service.ts @@ -1,9 +1,9 @@ -import { useMutation, useQuery, useQueryClient } from 'react-query'; - import { PublicSettingsViewModel } from '@/portainer/models/settings'; import axios, { parseAxiosError } from '../services/axios'; +import { Settings } from './types'; + export async function publicSettings() { try { const { data } = await axios.get(buildUrl('public')); @@ -16,37 +16,6 @@ export async function publicSettings() { } } -enum AuthenticationMethod { - // AuthenticationInternal represents the internal authentication method (authentication against Portainer API) - AuthenticationInternal, - // AuthenticationLDAP represents the LDAP authentication method (authentication against a LDAP server) - AuthenticationLDAP, - // AuthenticationOAuth represents the OAuth authentication method (authentication against a authorization server) - AuthenticationOAuth, -} - -export interface Settings { - LogoURL: string; - BlackListedLabels: { name: string; value: string }[]; - AuthenticationMethod: AuthenticationMethod; - SnapshotInterval: string; - TemplatesURL: string; - EnableEdgeComputeFeatures: boolean; - UserSessionTimeout: string; - KubeconfigExpiry: string; - EnableTelemetry: boolean; - HelmRepositoryURL: string; - KubectlShellImage: string; - TrustOnFirstConnect: boolean; - EnforceEdgeID: boolean; - AgentSecret: string; - EdgePortainerUrl: string; - EdgeAgentCheckinInterval: number; - EdgePingInterval: number; - EdgeSnapshotInterval: number; - EdgeCommandInterval: number; -} - export async function getSettings() { try { const { data } = await axios.get(buildUrl()); @@ -59,7 +28,7 @@ export async function getSettings() { } } -async function updateSettings(settings: Partial) { +export async function updateSettings(settings: Partial) { try { await axios.put(buildUrl(), settings); } catch (e) { @@ -67,26 +36,6 @@ async function updateSettings(settings: Partial) { } } -export function useUpdateSettingsMutation() { - const queryClient = useQueryClient(); - - return useMutation(updateSettings, { - onSuccess() { - return queryClient.invalidateQueries(['settings']); - }, - meta: { - error: { - title: 'Failure', - message: 'Unable to update settings', - }, - }, - }); -} - -export function useSettings(select?: (settings: Settings) => T) { - return useQuery(['settings'], getSettings, { select }); -} - function buildUrl(subResource?: string, action?: string) { let url = 'settings'; if (subResource) { diff --git a/app/portainer/settings/types.ts b/app/portainer/settings/types.ts new file mode 100644 index 000000000..e593e7352 --- /dev/null +++ b/app/portainer/settings/types.ts @@ -0,0 +1,127 @@ +import { TeamId } from '../teams/types'; + +export interface FDOConfiguration { + enabled: boolean; + ownerURL: string; + ownerUsername: string; + ownerPassword: string; +} + +export interface TLSConfiguration { + TLS: boolean; + TLSSkipVerify: boolean; + TLSCACert?: string; + TLSCert?: string; + TLSKey?: string; +} + +export interface LDAPGroupSearchSettings { + GroupBaseDN: string; + GroupFilter: string; + GroupAttribute: string; +} + +export interface LDAPSearchSettings { + BaseDN: string; + Filter: string; + UserNameAttribute: string; +} + +export interface LDAPSettings { + AnonymousMode: boolean; + ReaderDN: string; + Password?: string; + URL: string; + TLSConfig: TLSConfiguration; + StartTLS: boolean; + SearchSettings: LDAPSearchSettings[]; + GroupSearchSettings: LDAPGroupSearchSettings[]; + AutoCreateUsers: boolean; +} + +export interface Pair { + name: string; + value: string; +} + +export interface OpenAMTConfiguration { + enabled: boolean; + mpsServer: string; + mpsUser: string; + mpsPassword: string; + mpsToken: string; + certFileName: string; + certFileContent: string; + certFilePassword: string; + domainName: string; +} + +export interface OAuthSettings { + ClientID: string; + ClientSecret?: string; + AccessTokenURI: string; + AuthorizationURI: string; + ResourceURI: string; + RedirectURI: string; + UserIdentifier: string; + Scopes: string; + OAuthAutoCreateUsers: boolean; + DefaultTeamID: TeamId; + SSO: boolean; + LogoutURI: string; + KubeSecretKey: string; +} + +enum AuthenticationMethod { + /** + * Internal represents the internal authentication method (authentication against Portainer API) + */ + Internal, + /** + * LDAP represents the LDAP authentication method (authentication against a LDAP server) + */ + LDAP, + /** + * OAuth represents the OAuth authentication method (authentication against a authorization server) + */ + OAuth, +} + +type Feature = string; + +export interface Settings { + LogoURL: string; + BlackListedLabels: Pair[]; + AuthenticationMethod: AuthenticationMethod; + LDAPSettings: LDAPSettings; + OAuthSettings: OAuthSettings; + openAMTConfiguration: OpenAMTConfiguration; + fdoConfiguration: FDOConfiguration; + FeatureFlagSettings: { [key: Feature]: boolean }; + SnapshotInterval: string; + TemplatesURL: string; + EnableEdgeComputeFeatures: boolean; + UserSessionTimeout: string; + KubeconfigExpiry: string; + EnableTelemetry: boolean; + HelmRepositoryURL: string; + KubectlShellImage: string; + TrustOnFirstConnect: boolean; + EnforceEdgeID: boolean; + AgentSecret: string; + EdgePortainerUrl: string; + EdgeAgentCheckinInterval: number; + EdgeCommandInterval: number; + EdgePingInterval: number; + EdgeSnapshotInterval: number; + DisplayDonationHeader: boolean; + DisplayExternalContributors: boolean; + EnableHostManagementFeatures: boolean; + AllowVolumeBrowserForRegularUsers: boolean; + AllowBindMountsForRegularUsers: boolean; + AllowPrivilegedModeForRegularUsers: boolean; + AllowHostNamespaceForRegularUsers: boolean; + AllowStackManagementForRegularUsers: boolean; + AllowDeviceMappingForRegularUsers: boolean; + AllowContainerCapabilitiesForRegularUsers: boolean; +} diff --git a/app/portainer/views/endpoints/endpoints.html b/app/portainer/views/endpoints/endpoints.html index 19619bb5b..c87dc2a3e 100644 --- a/app/portainer/views/endpoints/endpoints.html +++ b/app/portainer/views/endpoints/endpoints.html @@ -7,22 +7,7 @@ Environment management -
-
-
-
-
-
-
- - {{ state.loadingMessage }} - - -
+