mirror of https://github.com/portainer/portainer
				
				
				
			feat(edge-devices): set specific page to view [EE-2082] (#6869)
							parent
							
								
									12cddbd896
								
							
						
					
					
						commit
						b031a30f62
					
				| 
						 | 
				
			
			@ -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;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -707,7 +707,7 @@
 | 
			
		|||
                <div class="col-sm-12 form-section-title"> Resources </div>
 | 
			
		||||
                <!-- memory-reservation-input -->
 | 
			
		||||
                <div class="form-group">
 | 
			
		||||
                  <label for="memory-reservation" class="col-sm-3 col-lg-2 control-label text-left mt-20"> Memory reservation </label>
 | 
			
		||||
                  <label for="memory-reservation" class="col-sm-3 col-lg-2 control-label text-left mt-8"> Memory reservation </label>
 | 
			
		||||
                  <div class="col-sm-3">
 | 
			
		||||
                    <slider
 | 
			
		||||
                      on-change="(handleResourceChange)"
 | 
			
		||||
| 
						 | 
				
			
			@ -722,13 +722,13 @@
 | 
			
		|||
                    <input type="number" min="0" class="form-control" ng-model="formValues.MemoryReservation" id="memory-reservation" />
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <div class="col-sm-4">
 | 
			
		||||
                    <p class="small text-muted mt-7"> Memory soft limit (<b>MB</b>) </p>
 | 
			
		||||
                    <p class="small text-muted mt-2"> Memory soft limit (<b>MB</b>) </p>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <!-- !memory-reservation-input -->
 | 
			
		||||
                <!-- memory-limit-input -->
 | 
			
		||||
                <div class="form-group">
 | 
			
		||||
                  <label for="memory-limit" class="col-sm-3 col-lg-2 control-label text-left mt-20"> Memory limit </label>
 | 
			
		||||
                  <label for="memory-limit" class="col-sm-3 col-lg-2 control-label text-left mt-8"> Memory limit </label>
 | 
			
		||||
                  <div class="col-sm-3">
 | 
			
		||||
                    <slider
 | 
			
		||||
                      on-change="(handleResourceChange)"
 | 
			
		||||
| 
						 | 
				
			
			@ -749,7 +749,7 @@
 | 
			
		|||
                <!-- !memory-limit-input -->
 | 
			
		||||
                <!-- cpu-limit-input -->
 | 
			
		||||
                <div class="form-group">
 | 
			
		||||
                  <label for="cpu-limit" class="col-sm-3 col-lg-2 control-label text-left mt-20"> CPU limit </label>
 | 
			
		||||
                  <label for="cpu-limit" class="col-sm-3 col-lg-2 control-label text-left mt-8"> CPU limit </label>
 | 
			
		||||
                  <div class="col-sm-5">
 | 
			
		||||
                    <slider
 | 
			
		||||
                      on-change="(handleResourceChange)"
 | 
			
		||||
| 
						 | 
				
			
			@ -761,7 +761,7 @@
 | 
			
		|||
                      ng-if="state.sliderMaxCpu"
 | 
			
		||||
                    ></slider>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <div class="col-sm-4 mt-20">
 | 
			
		||||
                  <div class="col-sm-4 mt-8">
 | 
			
		||||
                    <p class="small text-muted"> Maximum CPU usage </p>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -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<Device> = {
 | 
			
		||||
  Header: 'Actions',
 | 
			
		||||
| 
						 | 
				
			
			@ -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<Device> = {
 | 
			
		||||
  Header: 'Power State',
 | 
			
		||||
| 
						 | 
				
			
			@ -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<Pagination>): 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<EdgeDeviceTableSettings>();
 | 
			
		||||
 | 
			
		||||
  const columns = useColumns();
 | 
			
		||||
 | 
			
		||||
  const {
 | 
			
		||||
    getTableProps,
 | 
			
		||||
    getTableBodyProps,
 | 
			
		||||
    headerGroups,
 | 
			
		||||
    rows,
 | 
			
		||||
    prepareRow,
 | 
			
		||||
    selectedFlatRows,
 | 
			
		||||
    allColumns,
 | 
			
		||||
    setHiddenColumns,
 | 
			
		||||
  } = useTable<Environment>(
 | 
			
		||||
    {
 | 
			
		||||
      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 (
 | 
			
		||||
    <div className="row">
 | 
			
		||||
      <div className="col-sm-12">
 | 
			
		||||
        <TableContainer>
 | 
			
		||||
          <TableTitle icon="fa-plug" label="Edge Devices">
 | 
			
		||||
            <TableTitleActions>
 | 
			
		||||
              <ColumnVisibilityMenu<Environment>
 | 
			
		||||
                columns={columnsToHide}
 | 
			
		||||
                onChange={handleChangeColumnsVisibility}
 | 
			
		||||
                value={settings.hiddenColumns}
 | 
			
		||||
              />
 | 
			
		||||
              <TableSettingsMenu>
 | 
			
		||||
                <EdgeDevicesDatatableSettings />
 | 
			
		||||
              </TableSettingsMenu>
 | 
			
		||||
            </TableTitleActions>
 | 
			
		||||
          </TableTitle>
 | 
			
		||||
          <TableActions>
 | 
			
		||||
            <EdgeDevicesDatatableActions
 | 
			
		||||
              selectedItems={selectedFlatRows.map((row) => row.original)}
 | 
			
		||||
              isFDOEnabled={isFdoEnabled}
 | 
			
		||||
              isOpenAMTEnabled={isOpenAmtEnabled}
 | 
			
		||||
              setLoadingMessage={setLoadingMessage}
 | 
			
		||||
              showWaitingRoomLink={showWaitingRoomLink}
 | 
			
		||||
            />
 | 
			
		||||
          </TableActions>
 | 
			
		||||
          {isOpenAmtEnabled && someDeviceHasAMTActivated && (
 | 
			
		||||
            <div className={styles.kvmTip}>
 | 
			
		||||
              <TextTip color="blue">
 | 
			
		||||
                For the KVM function to work you need to have the MPS server
 | 
			
		||||
                added to your trusted site list, browse to this{' '}
 | 
			
		||||
                <a
 | 
			
		||||
                  href={`https://${mpsServer}`}
 | 
			
		||||
                  target="_blank"
 | 
			
		||||
                  rel="noreferrer"
 | 
			
		||||
                  className="space-right"
 | 
			
		||||
                >
 | 
			
		||||
                  site
 | 
			
		||||
                </a>
 | 
			
		||||
                and add to your trusted site list
 | 
			
		||||
              </TextTip>
 | 
			
		||||
            </div>
 | 
			
		||||
          )}
 | 
			
		||||
          <SearchBar value={search} onChange={handleSearchBarChange} />
 | 
			
		||||
          <Table
 | 
			
		||||
            className={tableProps.className}
 | 
			
		||||
            role={tableProps.role}
 | 
			
		||||
            style={tableProps.style}
 | 
			
		||||
          >
 | 
			
		||||
            <thead>
 | 
			
		||||
              {headerGroups.map((headerGroup) => {
 | 
			
		||||
                const { key, className, role, style } =
 | 
			
		||||
                  headerGroup.getHeaderGroupProps();
 | 
			
		||||
                return (
 | 
			
		||||
                  <TableHeaderRow<Environment>
 | 
			
		||||
                    key={key}
 | 
			
		||||
                    className={className}
 | 
			
		||||
                    role={role}
 | 
			
		||||
                    style={style}
 | 
			
		||||
                    headers={headerGroup.headers}
 | 
			
		||||
                    onSortChange={handleSortChange}
 | 
			
		||||
                  />
 | 
			
		||||
                );
 | 
			
		||||
              })}
 | 
			
		||||
            </thead>
 | 
			
		||||
            <tbody
 | 
			
		||||
              className={tbodyProps.className}
 | 
			
		||||
              role={tbodyProps.role}
 | 
			
		||||
              style={tbodyProps.style}
 | 
			
		||||
            >
 | 
			
		||||
              <Table.Content
 | 
			
		||||
                prepareRow={prepareRow}
 | 
			
		||||
                rows={rows}
 | 
			
		||||
                renderRow={(row, { key, className, role, style }) => {
 | 
			
		||||
                  const group = groupsById[row.original.GroupId];
 | 
			
		||||
 | 
			
		||||
                  return (
 | 
			
		||||
                    <RowProvider
 | 
			
		||||
                      key={key}
 | 
			
		||||
                      isOpenAmtEnabled={isOpenAmtEnabled}
 | 
			
		||||
                      groupName={group[0]?.Name}
 | 
			
		||||
                    >
 | 
			
		||||
                      <TableRow<Environment>
 | 
			
		||||
                        cells={row.cells}
 | 
			
		||||
                        key={key}
 | 
			
		||||
                        className={className}
 | 
			
		||||
                        role={role}
 | 
			
		||||
                        style={style}
 | 
			
		||||
                      />
 | 
			
		||||
                      {row.isExpanded && (
 | 
			
		||||
                        <tr>
 | 
			
		||||
                          <td />
 | 
			
		||||
                          <td colSpan={row.cells.length - 1}>
 | 
			
		||||
                            <AMTDevicesDatatable
 | 
			
		||||
                              environmentId={row.original.Id}
 | 
			
		||||
                            />
 | 
			
		||||
                          </td>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                      )}
 | 
			
		||||
                    </RowProvider>
 | 
			
		||||
                  );
 | 
			
		||||
                }}
 | 
			
		||||
              />
 | 
			
		||||
            </tbody>
 | 
			
		||||
          </Table>
 | 
			
		||||
          <TableFooter>
 | 
			
		||||
            <SelectedRowsCount value={selectedFlatRows.length} />
 | 
			
		||||
            <PaginationControls
 | 
			
		||||
              isPageInputVisible
 | 
			
		||||
              pageLimit={pagination.pageLimit}
 | 
			
		||||
              page={pagination.page}
 | 
			
		||||
              onPageChange={(p) => gotoPage(p)}
 | 
			
		||||
              totalCount={totalCount}
 | 
			
		||||
              onPageLimitChange={handlePageSizeChange}
 | 
			
		||||
            />
 | 
			
		||||
          </TableFooter>
 | 
			
		||||
        </TableContainer>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  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 },
 | 
			
		||||
    }));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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 (
 | 
			
		||||
    <TableSettingsProvider defaults={defaultSettings} storageKey={storageKey}>
 | 
			
		||||
      <Loader storageKey={storageKey}>
 | 
			
		||||
        {({
 | 
			
		||||
          environments,
 | 
			
		||||
          pagination,
 | 
			
		||||
          totalCount,
 | 
			
		||||
          setPagination,
 | 
			
		||||
          search,
 | 
			
		||||
          setSearch,
 | 
			
		||||
        }) => (
 | 
			
		||||
          <EdgeDevicesDatatable
 | 
			
		||||
            // eslint-disable-next-line react/jsx-props-no-spreading
 | 
			
		||||
            {...props}
 | 
			
		||||
            storageKey={storageKey}
 | 
			
		||||
            dataset={environments}
 | 
			
		||||
            pagination={pagination}
 | 
			
		||||
            onChangePagination={setPagination}
 | 
			
		||||
            totalCount={totalCount}
 | 
			
		||||
            search={search}
 | 
			
		||||
            onChangeSearch={setSearch}
 | 
			
		||||
          />
 | 
			
		||||
        )}
 | 
			
		||||
      </Loader>
 | 
			
		||||
    </TableSettingsProvider>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface LoaderProps {
 | 
			
		||||
  storageKey: string;
 | 
			
		||||
  children: (options: {
 | 
			
		||||
    environments: Environment[];
 | 
			
		||||
    totalCount: number;
 | 
			
		||||
    pagination: Pagination;
 | 
			
		||||
    setPagination(value: Partial<Pagination>): void;
 | 
			
		||||
    search: string;
 | 
			
		||||
    setSearch: (value: string) => void;
 | 
			
		||||
  }) => React.ReactNode;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function Loader({ children, storageKey }: LoaderProps) {
 | 
			
		||||
  const { settings } = useTableSettings<EdgeDeviceTableSettings>();
 | 
			
		||||
  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<Pagination>) {
 | 
			
		||||
    setPagination((prev) => ({ ...prev, ...value }));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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 } =
 | 
			
		||||
| 
						 | 
				
			
			@ -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<Environment> = {
 | 
			
		||||
  Header: 'Name',
 | 
			
		||||
| 
						 | 
				
			
			@ -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 {}
 | 
			
		||||
| 
						 | 
				
			
			@ -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 (
 | 
			
		||||
    <>
 | 
			
		||||
      <PageHeader
 | 
			
		||||
        title="Edge Devices"
 | 
			
		||||
        reload
 | 
			
		||||
        breadcrumbs={[{ label: 'EdgeDevices' }]}
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      {loadingMessage ? (
 | 
			
		||||
        <ViewLoading message={loadingMessage} />
 | 
			
		||||
      ) : (
 | 
			
		||||
        <EdgeDevicesDatatableContainer
 | 
			
		||||
          setLoadingMessage={setLoadingMessage}
 | 
			
		||||
          isFdoEnabled={
 | 
			
		||||
            settings.EnableEdgeComputeFeatures &&
 | 
			
		||||
            settings.fdoConfiguration.enabled
 | 
			
		||||
          }
 | 
			
		||||
          showWaitingRoomLink={
 | 
			
		||||
            process.env.PORTAINER_EDITION === 'BE' &&
 | 
			
		||||
            settings.EnableEdgeComputeFeatures &&
 | 
			
		||||
            !settings.TrustOnFirstConnect
 | 
			
		||||
          }
 | 
			
		||||
          isOpenAmtEnabled={
 | 
			
		||||
            settings.EnableEdgeComputeFeatures &&
 | 
			
		||||
            settings.openAMTConfiguration.enabled
 | 
			
		||||
          }
 | 
			
		||||
          mpsServer={settings.openAMTConfiguration.mpsServer}
 | 
			
		||||
          groups={groupsQuery.data}
 | 
			
		||||
          storageKey="edgeDevices"
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const EdgeDevicesViewAngular = r2a(EdgeDevicesView, []);
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
export { EdgeDevicesView, EdgeDevicesViewAngular } from './EdgeDevicesView';
 | 
			
		||||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
  });
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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';
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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<void>;
 | 
			
		||||
  setLoadingMessage(message: string): void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function EdgeDevicesDatatable({
 | 
			
		||||
  storageKey,
 | 
			
		||||
  isFdoEnabled,
 | 
			
		||||
  isOpenAmtEnabled,
 | 
			
		||||
  showWaitingRoomLink,
 | 
			
		||||
  mpsServer,
 | 
			
		||||
  dataset,
 | 
			
		||||
  groups,
 | 
			
		||||
  onRefresh,
 | 
			
		||||
  setLoadingMessage,
 | 
			
		||||
}: EdgeDevicesTableProps) {
 | 
			
		||||
  const { settings, setTableSettings } =
 | 
			
		||||
    useTableSettings<EdgeDeviceTableSettings>();
 | 
			
		||||
  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<Environment>(
 | 
			
		||||
    {
 | 
			
		||||
      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 (
 | 
			
		||||
    <TableContainer>
 | 
			
		||||
      <TableTitle icon="fa-plug" label="Edge Devices">
 | 
			
		||||
        <TableTitleActions>
 | 
			
		||||
          <ColumnVisibilityMenu<Environment>
 | 
			
		||||
            columns={columnsToHide}
 | 
			
		||||
            onChange={handleChangeColumnsVisibility}
 | 
			
		||||
            value={settings.hiddenColumns}
 | 
			
		||||
          />
 | 
			
		||||
 | 
			
		||||
          <TableSettingsMenu>
 | 
			
		||||
            <EdgeDevicesDatatableSettings />
 | 
			
		||||
          </TableSettingsMenu>
 | 
			
		||||
        </TableTitleActions>
 | 
			
		||||
      </TableTitle>
 | 
			
		||||
 | 
			
		||||
      <TableActions>
 | 
			
		||||
        <EdgeDevicesDatatableActions
 | 
			
		||||
          selectedItems={selectedFlatRows.map((row) => row.original)}
 | 
			
		||||
          isFDOEnabled={isFdoEnabled}
 | 
			
		||||
          isOpenAMTEnabled={isOpenAmtEnabled}
 | 
			
		||||
          setLoadingMessage={setLoadingMessage}
 | 
			
		||||
          showWaitingRoomLink={showWaitingRoomLink}
 | 
			
		||||
        />
 | 
			
		||||
      </TableActions>
 | 
			
		||||
 | 
			
		||||
      {someDeviceHasAMTActivated && (
 | 
			
		||||
        <div className={styles.kvmTip}>
 | 
			
		||||
          <TextTip color="blue">
 | 
			
		||||
            For the KVM function to work you need to have the MPS server added
 | 
			
		||||
            to your trusted site list, browse to this{' '}
 | 
			
		||||
            <a href={`https://${mpsServer}`} target="_blank" rel="noreferrer">
 | 
			
		||||
              site
 | 
			
		||||
            </a>{' '}
 | 
			
		||||
            and add to your trusted site list
 | 
			
		||||
          </TextTip>
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      <SearchBar value={searchBarValue} onChange={handleSearchBarChange} />
 | 
			
		||||
 | 
			
		||||
      <Table
 | 
			
		||||
        className={tableProps.className}
 | 
			
		||||
        role={tableProps.role}
 | 
			
		||||
        style={tableProps.style}
 | 
			
		||||
      >
 | 
			
		||||
        <thead>
 | 
			
		||||
          {headerGroups.map((headerGroup) => {
 | 
			
		||||
            const { key, className, role, style } =
 | 
			
		||||
              headerGroup.getHeaderGroupProps();
 | 
			
		||||
 | 
			
		||||
            return (
 | 
			
		||||
              <TableHeaderRow<Environment>
 | 
			
		||||
                key={key}
 | 
			
		||||
                className={className}
 | 
			
		||||
                role={role}
 | 
			
		||||
                style={style}
 | 
			
		||||
                headers={headerGroup.headers}
 | 
			
		||||
                onSortChange={handleSortChange}
 | 
			
		||||
              />
 | 
			
		||||
            );
 | 
			
		||||
          })}
 | 
			
		||||
        </thead>
 | 
			
		||||
        <tbody
 | 
			
		||||
          className={tbodyProps.className}
 | 
			
		||||
          role={tbodyProps.role}
 | 
			
		||||
          style={tbodyProps.style}
 | 
			
		||||
        >
 | 
			
		||||
          {page.map((row) => {
 | 
			
		||||
            prepareRow(row);
 | 
			
		||||
            const { key, className, role, style } = row.getRowProps();
 | 
			
		||||
            const group = groupsById[row.original.GroupId];
 | 
			
		||||
            return (
 | 
			
		||||
              <RowProvider
 | 
			
		||||
                key={key}
 | 
			
		||||
                isOpenAmtEnabled={isOpenAmtEnabled}
 | 
			
		||||
                groupName={group[0]?.Name}
 | 
			
		||||
              >
 | 
			
		||||
                <TableRow<Environment>
 | 
			
		||||
                  cells={row.cells}
 | 
			
		||||
                  key={key}
 | 
			
		||||
                  className={className}
 | 
			
		||||
                  role={role}
 | 
			
		||||
                  style={style}
 | 
			
		||||
                />
 | 
			
		||||
                {row.isExpanded && (
 | 
			
		||||
                  <tr>
 | 
			
		||||
                    <td />
 | 
			
		||||
                    <td colSpan={row.cells.length - 1}>
 | 
			
		||||
                      <AMTDevicesDatatable environmentId={row.original.Id} />
 | 
			
		||||
                    </td>
 | 
			
		||||
                  </tr>
 | 
			
		||||
                )}
 | 
			
		||||
              </RowProvider>
 | 
			
		||||
            );
 | 
			
		||||
          })}
 | 
			
		||||
        </tbody>
 | 
			
		||||
      </Table>
 | 
			
		||||
 | 
			
		||||
      <TableFooter>
 | 
			
		||||
        <SelectedRowsCount value={selectedFlatRows.length} />
 | 
			
		||||
        <PaginationControls
 | 
			
		||||
          showAll
 | 
			
		||||
          pageLimit={pageSize}
 | 
			
		||||
          page={pageIndex + 1}
 | 
			
		||||
          onPageChange={(p) => gotoPage(p - 1)}
 | 
			
		||||
          totalCount={dataset.length}
 | 
			
		||||
          onPageLimitChange={handlePageSizeChange}
 | 
			
		||||
        />
 | 
			
		||||
      </TableFooter>
 | 
			
		||||
    </TableContainer>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  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 },
 | 
			
		||||
    }));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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 (
 | 
			
		||||
    <TableSettingsProvider defaults={defaultSettings} storageKey={storageKey}>
 | 
			
		||||
      {/* eslint-disable-next-line react/jsx-props-no-spreading */}
 | 
			
		||||
      <EdgeDevicesDatatable {...props} storageKey={storageKey} />
 | 
			
		||||
    </TableSettingsProvider>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const EdgeDevicesDatatableAngular = react2angular(
 | 
			
		||||
  EdgeDevicesDatatableContainer,
 | 
			
		||||
  [
 | 
			
		||||
    'groups',
 | 
			
		||||
    'dataset',
 | 
			
		||||
    'onRefresh',
 | 
			
		||||
    'setLoadingMessage',
 | 
			
		||||
    'isFdoEnabled',
 | 
			
		||||
    'showWaitingRoomLink',
 | 
			
		||||
    'isOpenAmtEnabled',
 | 
			
		||||
    'mpsServer',
 | 
			
		||||
  ]
 | 
			
		||||
);
 | 
			
		||||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,40 +0,0 @@
 | 
			
		|||
<rd-header>
 | 
			
		||||
  <rd-header-title title-text="Edge Devices">
 | 
			
		||||
    <a data-toggle="tooltip" title="Refresh" ui-sref="edge.devices" ui-sref-opts="{reload: true}">
 | 
			
		||||
      <i class="fa fa-sync" aria-hidden="true"></i>
 | 
			
		||||
    </a>
 | 
			
		||||
  </rd-header-title>
 | 
			
		||||
  <rd-header-content>Edge Devices</rd-header-content>
 | 
			
		||||
</rd-header>
 | 
			
		||||
 | 
			
		||||
<div
 | 
			
		||||
  class="row"
 | 
			
		||||
  style="width: 100%; height: 100%; text-align: center; display: flex; flex-direction: column; align-items: center; justify-content: center"
 | 
			
		||||
  ng-if="$ctrl.loadingMessage"
 | 
			
		||||
>
 | 
			
		||||
  <div class="sk-fold">
 | 
			
		||||
    <div class="sk-fold-cube"></div>
 | 
			
		||||
    <div class="sk-fold-cube"></div>
 | 
			
		||||
    <div class="sk-fold-cube"></div>
 | 
			
		||||
    <div class="sk-fold-cube"></div>
 | 
			
		||||
  </div>
 | 
			
		||||
  <span style="margin-top: 25px">
 | 
			
		||||
    {{ $ctrl.loadingMessage }}
 | 
			
		||||
    <i class="fa fa-cog fa-spin"></i>
 | 
			
		||||
  </span>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<div class="row" ng-if="!$ctrl.loadingMessage">
 | 
			
		||||
  <div class="col-sm-12">
 | 
			
		||||
    <edge-devices-datatable
 | 
			
		||||
      dataset="($ctrl.edgeDevices)"
 | 
			
		||||
      groups="($ctrl.groups)"
 | 
			
		||||
      is-fdo-enabled="($ctrl.isFDOEnabled)"
 | 
			
		||||
      is-open-amt-enabled="($ctrl.isOpenAMTEnabled)"
 | 
			
		||||
      show-waiting-room-link="($ctrl.showWaitingRoomLink)"
 | 
			
		||||
      mps-server="($ctrl.mpsServer)"
 | 
			
		||||
      on-refresh="($ctrl.getEnvironments)"
 | 
			
		||||
      set-loading-message="($ctrl.setLoadingMessage)"
 | 
			
		||||
    ></edge-devices-datatable>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -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();
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,8 +0,0 @@
 | 
			
		|||
import angular from 'angular';
 | 
			
		||||
 | 
			
		||||
import { EdgeDevicesViewController } from './edgeDevicesViewController';
 | 
			
		||||
 | 
			
		||||
angular.module('portainer.edge').component('edgeDevicesView', {
 | 
			
		||||
  templateUrl: './edgeDevicesView.html',
 | 
			
		||||
  controller: EdgeDevicesViewController,
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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 <ViewLoading message={message} />;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const Example: Story<Args> = Template.bind({});
 | 
			
		||||
Example.args = {
 | 
			
		||||
  message: 'Loading...',
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -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 (
 | 
			
		||||
    <div className={clsx('row', styles.root)}>
 | 
			
		||||
      <div className="sk-fold">
 | 
			
		||||
        <div className="sk-fold-cube" />
 | 
			
		||||
        <div className="sk-fold-cube" />
 | 
			
		||||
        <div className="sk-fold-cube" />
 | 
			
		||||
        <div className="sk-fold-cube" />
 | 
			
		||||
      </div>
 | 
			
		||||
      {message && (
 | 
			
		||||
        <span className={styles.message}>
 | 
			
		||||
          {message}
 | 
			
		||||
          <i className="fa fa-cog fa-spin space-left" />
 | 
			
		||||
        </span>
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const ViewLoadingAngular = r2a(ViewLoading, ['message']);
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
export { ViewLoading, ViewLoadingAngular } from './ViewLoading';
 | 
			
		||||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
interface Props {
 | 
			
		||||
  value: number;
 | 
			
		||||
  onChange(value: number): void;
 | 
			
		||||
  showAll: boolean;
 | 
			
		||||
  showAll?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function ItemsPerPageSelector({ value, onChange, showAll }: Props) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,6 +21,7 @@ export function PageButton({
 | 
			
		|||
      <button
 | 
			
		||||
        type="button"
 | 
			
		||||
        onClick={() => typeof page === 'number' && onPageChange(page)}
 | 
			
		||||
        disabled={disabled}
 | 
			
		||||
      >
 | 
			
		||||
        {children}
 | 
			
		||||
      </button>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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<Values>({
 | 
			
		||||
    initialValues: { page: '' },
 | 
			
		||||
    onSubmit: async ({ page }) => page && onChange(page),
 | 
			
		||||
    validateOnMount: true,
 | 
			
		||||
    validationSchema: () =>
 | 
			
		||||
      object({ page: number().required().max(totalPages).min(1) }),
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <form className="mx-3" onSubmit={handleSubmit}>
 | 
			
		||||
      <label className="m-0 mr-2 font-normal small" htmlFor="go-to-page-input">
 | 
			
		||||
        Go to page
 | 
			
		||||
      </label>
 | 
			
		||||
      <Input
 | 
			
		||||
        id="go-to-page-input"
 | 
			
		||||
        className="!w-32"
 | 
			
		||||
        type="number"
 | 
			
		||||
        value={values.page}
 | 
			
		||||
        max={totalPages}
 | 
			
		||||
        min={1}
 | 
			
		||||
        step={1}
 | 
			
		||||
        onChange={handleChange}
 | 
			
		||||
        onKeyPress={preventNotNumber}
 | 
			
		||||
      />
 | 
			
		||||
      <Button type="submit" disabled={!isValid}>
 | 
			
		||||
        Go
 | 
			
		||||
      </Button>
 | 
			
		||||
    </form>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  function preventNotNumber(e: KeyboardEvent<HTMLInputElement>) {
 | 
			
		||||
    if (e.key.match(/^\D$/)) {
 | 
			
		||||
      e.preventDefault();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function handleChange(e: ChangeEvent<HTMLInputElement>) {
 | 
			
		||||
    const value = parseInt(e.target.value, 10);
 | 
			
		||||
    setFieldValue('page', Number.isNaN(value) ? '' : value);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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 (
 | 
			
		||||
    <ul className="pagination">
 | 
			
		||||
      {boundaryLinks ? (
 | 
			
		||||
        <PageButton
 | 
			
		||||
          onPageChange={onPageChange}
 | 
			
		||||
          page={1}
 | 
			
		||||
          disabled={currentPage === 1}
 | 
			
		||||
        >
 | 
			
		||||
          «
 | 
			
		||||
        </PageButton>
 | 
			
		||||
      ) : null}
 | 
			
		||||
      {directionLinks ? (
 | 
			
		||||
        <PageButton
 | 
			
		||||
          onPageChange={onPageChange}
 | 
			
		||||
          page={currentPage - 1}
 | 
			
		||||
          disabled={currentPage === 1}
 | 
			
		||||
        >
 | 
			
		||||
          ‹
 | 
			
		||||
        </PageButton>
 | 
			
		||||
      ) : null}
 | 
			
		||||
      {pages.map((pageNumber, index) => (
 | 
			
		||||
        <PageButton
 | 
			
		||||
          onPageChange={onPageChange}
 | 
			
		||||
          page={pageNumber}
 | 
			
		||||
          disabled={pageNumber === '...'}
 | 
			
		||||
          active={currentPage === pageNumber}
 | 
			
		||||
          key={index}
 | 
			
		||||
        >
 | 
			
		||||
          {pageNumber}
 | 
			
		||||
        </PageButton>
 | 
			
		||||
      ))}
 | 
			
		||||
    <>
 | 
			
		||||
      {isInputVisible && (
 | 
			
		||||
        <PageInput
 | 
			
		||||
          onChange={(page) => onPageChange(page)}
 | 
			
		||||
          totalPages={Math.ceil(totalCount / itemsPerPage)}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
      <ul className="pagination">
 | 
			
		||||
        {boundaryLinks ? (
 | 
			
		||||
          <PageButton
 | 
			
		||||
            onPageChange={onPageChange}
 | 
			
		||||
            page={1}
 | 
			
		||||
            disabled={currentPage === 1}
 | 
			
		||||
          >
 | 
			
		||||
            «
 | 
			
		||||
          </PageButton>
 | 
			
		||||
        ) : null}
 | 
			
		||||
        {directionLinks ? (
 | 
			
		||||
          <PageButton
 | 
			
		||||
            onPageChange={onPageChange}
 | 
			
		||||
            page={currentPage - 1}
 | 
			
		||||
            disabled={currentPage === 1}
 | 
			
		||||
          >
 | 
			
		||||
            ‹
 | 
			
		||||
          </PageButton>
 | 
			
		||||
        ) : null}
 | 
			
		||||
        {pages.map((pageNumber, index) => (
 | 
			
		||||
          <PageButton
 | 
			
		||||
            onPageChange={onPageChange}
 | 
			
		||||
            page={pageNumber}
 | 
			
		||||
            disabled={pageNumber === '...'}
 | 
			
		||||
            active={currentPage === pageNumber}
 | 
			
		||||
            key={index}
 | 
			
		||||
          >
 | 
			
		||||
            {pageNumber}
 | 
			
		||||
          </PageButton>
 | 
			
		||||
        ))}
 | 
			
		||||
 | 
			
		||||
      {directionLinks ? (
 | 
			
		||||
        <PageButton
 | 
			
		||||
          onPageChange={onPageChange}
 | 
			
		||||
          page={currentPage + 1}
 | 
			
		||||
          disabled={currentPage === last}
 | 
			
		||||
        >
 | 
			
		||||
          ›
 | 
			
		||||
        </PageButton>
 | 
			
		||||
      ) : null}
 | 
			
		||||
      {boundaryLinks ? (
 | 
			
		||||
        <PageButton
 | 
			
		||||
          disabled={currentPage === last}
 | 
			
		||||
          onPageChange={onPageChange}
 | 
			
		||||
          page={last}
 | 
			
		||||
        >
 | 
			
		||||
          »
 | 
			
		||||
        </PageButton>
 | 
			
		||||
      ) : null}
 | 
			
		||||
    </ul>
 | 
			
		||||
        {directionLinks ? (
 | 
			
		||||
          <PageButton
 | 
			
		||||
            onPageChange={onPageChange}
 | 
			
		||||
            page={currentPage + 1}
 | 
			
		||||
            disabled={currentPage === last}
 | 
			
		||||
          >
 | 
			
		||||
            ›
 | 
			
		||||
          </PageButton>
 | 
			
		||||
        ) : null}
 | 
			
		||||
        {boundaryLinks ? (
 | 
			
		||||
          <PageButton
 | 
			
		||||
            disabled={currentPage === last}
 | 
			
		||||
            onPageChange={onPageChange}
 | 
			
		||||
            page={last}
 | 
			
		||||
          >
 | 
			
		||||
            »
 | 
			
		||||
          </PageButton>
 | 
			
		||||
        ) : null}
 | 
			
		||||
      </ul>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 (
 | 
			
		||||
    <div className="paginationControls">
 | 
			
		||||
      <form className="form-inline">
 | 
			
		||||
      <div className="form-inline flex">
 | 
			
		||||
        <ItemsPerPageSelector
 | 
			
		||||
          value={pageLimit}
 | 
			
		||||
          onChange={handlePageLimitChange}
 | 
			
		||||
          showAll={showAll}
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
        {pageLimit !== 0 && (
 | 
			
		||||
          <PageSelector
 | 
			
		||||
            maxSize={5}
 | 
			
		||||
| 
						 | 
				
			
			@ -33,9 +36,10 @@ export function PaginationControls({
 | 
			
		|||
            currentPage={page}
 | 
			
		||||
            itemsPerPage={pageLimit}
 | 
			
		||||
            totalCount={totalCount}
 | 
			
		||||
            isInputVisible={isPageInputVisible}
 | 
			
		||||
          />
 | 
			
		||||
        )}
 | 
			
		||||
      </form>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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';
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,32 @@
 | 
			
		|||
import { useMutation, useQuery, useQueryClient } from 'react-query';
 | 
			
		||||
 | 
			
		||||
import { getSettings, updateSettings } from './settings.service';
 | 
			
		||||
import { Settings } from './types';
 | 
			
		||||
 | 
			
		||||
export function useSettings<T = Settings>(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',
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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<Settings>(buildUrl());
 | 
			
		||||
| 
						 | 
				
			
			@ -59,7 +28,7 @@ export async function getSettings() {
 | 
			
		|||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function updateSettings(settings: Partial<Settings>) {
 | 
			
		||||
export async function updateSettings(settings: Partial<Settings>) {
 | 
			
		||||
  try {
 | 
			
		||||
    await axios.put(buildUrl(), settings);
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
| 
						 | 
				
			
			@ -67,26 +36,6 @@ async function updateSettings(settings: Partial<Settings>) {
 | 
			
		|||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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<T = Settings>(select?: (settings: Settings) => T) {
 | 
			
		||||
  return useQuery(['settings'], getSettings, { select });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function buildUrl(subResource?: string, action?: string) {
 | 
			
		||||
  let url = 'settings';
 | 
			
		||||
  if (subResource) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -7,22 +7,7 @@
 | 
			
		|||
  <rd-header-content>Environment management</rd-header-content>
 | 
			
		||||
</rd-header>
 | 
			
		||||
 | 
			
		||||
<div
 | 
			
		||||
  class="row"
 | 
			
		||||
  style="width: 100%; height: 100%; text-align: center; display: flex; flex-direction: column; align-items: center; justify-content: center"
 | 
			
		||||
  ng-if="state.loadingMessage"
 | 
			
		||||
>
 | 
			
		||||
  <div class="sk-fold">
 | 
			
		||||
    <div class="sk-fold-cube"></div>
 | 
			
		||||
    <div class="sk-fold-cube"></div>
 | 
			
		||||
    <div class="sk-fold-cube"></div>
 | 
			
		||||
    <div class="sk-fold-cube"></div>
 | 
			
		||||
  </div>
 | 
			
		||||
  <span style="margin-top: 25px">
 | 
			
		||||
    {{ state.loadingMessage }}
 | 
			
		||||
    <i class="fa fa-cog fa-spin"></i>
 | 
			
		||||
  </span>
 | 
			
		||||
</div>
 | 
			
		||||
<view-loading ng-if="state.loadingMessage" message="state.loadingMessage"></view-loading>
 | 
			
		||||
 | 
			
		||||
<div class="row" ng-if="!state.loadingMessage">
 | 
			
		||||
  <div class="col-sm-12">
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue