1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-09 07:45:22 +02:00

feat(edge-devices): set specific page to view [EE-2082] (#6869)

This commit is contained in:
Chaim Lev-Ari 2022-05-23 10:57:22 +03:00 committed by GitHub
parent 12cddbd896
commit b031a30f62
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 892 additions and 639 deletions

View file

@ -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)

View file

@ -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;
}

View file

@ -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...',
};

View file

@ -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']);

View file

@ -0,0 +1 @@
export { ViewLoading, ViewLoadingAngular } from './ViewLoading';

View file

@ -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)

View file

@ -1,7 +1,7 @@
interface Props {
value: number;
onChange(value: number): void;
showAll: boolean;
showAll?: boolean;
}
export function ItemsPerPageSelector({ value, onChange, showAll }: Props) {

View file

@ -21,6 +21,7 @@ export function PageButton({
<button
type="button"
onClick={() => typeof page === 'number' && onPageChange(page)}
disabled={disabled}
>
{children}
</button>

View file

@ -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);
}
}

View file

@ -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}
>
&laquo;
</PageButton>
) : null}
{directionLinks ? (
<PageButton
onPageChange={onPageChange}
page={currentPage - 1}
disabled={currentPage === 1}
>
&lsaquo;
</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}
>
&laquo;
</PageButton>
) : null}
{directionLinks ? (
<PageButton
onPageChange={onPageChange}
page={currentPage - 1}
disabled={currentPage === 1}
>
&lsaquo;
</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}
>
&rsaquo;
</PageButton>
) : null}
{boundaryLinks ? (
<PageButton
disabled={currentPage === last}
onPageChange={onPageChange}
page={last}
>
&raquo;
</PageButton>
) : null}
</ul>
{directionLinks ? (
<PageButton
onPageChange={onPageChange}
page={currentPage + 1}
disabled={currentPage === last}
>
&rsaquo;
</PageButton>
) : null}
{boundaryLinks ? (
<PageButton
disabled={currentPage === last}
onPageChange={onPageChange}
page={last}
>
&raquo;
</PageButton>
) : null}
</ul>
</>
);
}

View file

@ -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>
);

View file

@ -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;
}

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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;
}

View file

@ -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',
},
},
});
}

View file

@ -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) {

View file

@ -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;
}

View file

@ -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">