mirror of
https://github.com/portainer/portainer.git
synced 2025-07-25 08:19:40 +02:00
refactor(portainer): move to react [EE-3350] (#7915)
This commit is contained in:
parent
30e23ea5b4
commit
78dcba614d
192 changed files with 200 additions and 211 deletions
|
@ -1,7 +1,7 @@
|
|||
import { Meta } from '@storybook/react';
|
||||
|
||||
import { Edition, FeatureId } from '@/portainer/feature-flags/enums';
|
||||
import { init as initFeatureService } from '@/portainer/feature-flags/feature-flags.service';
|
||||
import { Edition, FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||
import { init as initFeatureService } from '@/react/portainer/feature-flags/feature-flags.service';
|
||||
|
||||
import { BEFeatureIndicator, Props } from './BEFeatureIndicator';
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import { Briefcase } from 'react-feather';
|
|||
|
||||
import './BEFeatureIndicator.css';
|
||||
|
||||
import { FeatureId } from '@/portainer/feature-flags/enums';
|
||||
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||
|
||||
import { getFeatureDetails } from './utils';
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { FeatureId } from '@/portainer/feature-flags/enums';
|
||||
import { isLimitedToBE } from '@/portainer/feature-flags/feature-flags.service';
|
||||
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||
import { isLimitedToBE } from '@/react/portainer/feature-flags/feature-flags.service';
|
||||
|
||||
const BE_URL = 'https://www.portainer.io/business-upsell?from=';
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { Meta } from '@storybook/react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { init as initFeatureService } from '@/portainer/feature-flags/feature-flags.service';
|
||||
import { Edition, FeatureId } from '@/portainer/feature-flags/enums';
|
||||
import { init as initFeatureService } from '@/react/portainer/feature-flags/feature-flags.service';
|
||||
import { Edition, FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||
|
||||
import { BoxSelector } from './BoxSelector';
|
||||
import { BoxSelectorOption } from './types';
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Meta } from '@storybook/react';
|
||||
|
||||
import { init as initFeatureService } from '@/portainer/feature-flags/feature-flags.service';
|
||||
import { Edition, FeatureId } from '@/portainer/feature-flags/enums';
|
||||
import { init as initFeatureService } from '@/react/portainer/feature-flags/feature-flags.service';
|
||||
import { Edition, FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||
|
||||
import { BoxSelectorItem } from './BoxSelectorItem';
|
||||
import { BoxSelectorOption } from './types';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import clsx from 'clsx';
|
||||
|
||||
import { isLimitedToBE } from '@/portainer/feature-flags/feature-flags.service';
|
||||
import { isLimitedToBE } from '@/react/portainer/feature-flags/feature-flags.service';
|
||||
import { Icon } from '@/react/components/Icon';
|
||||
|
||||
import './BoxSelectorItem.css';
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import ReactTooltip from 'react-tooltip';
|
||||
import { HelpCircle } from 'react-feather';
|
||||
|
||||
import { FeatureId } from '@/portainer/feature-flags/enums';
|
||||
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||
|
||||
import { getFeatureDetails } from '@@/BEFeatureIndicator/utils';
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { FeatureId } from '@/portainer/feature-flags/enums';
|
||||
import type { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||
|
||||
import { IconProps } from '@@/Icon';
|
||||
|
||||
|
|
44
app/react/components/EdgeIndicator.test.tsx
Normal file
44
app/react/components/EdgeIndicator.test.tsx
Normal file
|
@ -0,0 +1,44 @@
|
|||
import { createMockEnvironment } from '@/react-tools/test-mocks';
|
||||
import { renderWithQueryClient } from '@/react-tools/test-utils';
|
||||
import { rest, server } from '@/setup-tests/server';
|
||||
|
||||
import { EdgeIndicator } from './EdgeIndicator';
|
||||
|
||||
test('when edge id is not set, should show unassociated label', async () => {
|
||||
const { queryByLabelText } = await renderComponent();
|
||||
|
||||
const unassociatedLabel = queryByLabelText('unassociated');
|
||||
|
||||
expect(unassociatedLabel).toBeVisible();
|
||||
});
|
||||
|
||||
test('given edge id and last checkin is set, should show heartbeat', async () => {
|
||||
const { queryByLabelText } = await renderComponent('id', 1);
|
||||
|
||||
expect(queryByLabelText('edge-heartbeat')).toBeVisible();
|
||||
expect(queryByLabelText('edge-last-checkin')).toBeVisible();
|
||||
});
|
||||
|
||||
async function renderComponent(
|
||||
edgeId = '',
|
||||
lastCheckInDate = 0,
|
||||
checkInInterval = 0,
|
||||
queryDate = 0
|
||||
) {
|
||||
server.use(rest.get('/api/settings', (req, res, ctx) => res(ctx.json({}))));
|
||||
|
||||
const environment = createMockEnvironment();
|
||||
|
||||
environment.EdgeID = edgeId;
|
||||
environment.LastCheckInDate = lastCheckInDate;
|
||||
environment.EdgeCheckinInterval = checkInInterval;
|
||||
environment.QueryDate = queryDate;
|
||||
|
||||
const queries = renderWithQueryClient(
|
||||
<EdgeIndicator environment={environment} showLastCheckInDate />
|
||||
);
|
||||
|
||||
await expect(queries.findByRole('status')).resolves.toBeVisible();
|
||||
|
||||
return queries;
|
||||
}
|
114
app/react/components/EdgeIndicator.tsx
Normal file
114
app/react/components/EdgeIndicator.tsx
Normal file
|
@ -0,0 +1,114 @@
|
|||
import clsx from 'clsx';
|
||||
|
||||
import { isoDateFromTimestamp } from '@/portainer/filters/filters';
|
||||
import { Environment } from '@/react/portainer/environments/types';
|
||||
import { usePublicSettings } from '@/react/portainer/settings/queries';
|
||||
import { PublicSettingsViewModel } from '@/portainer/models/settings';
|
||||
|
||||
interface Props {
|
||||
showLastCheckInDate?: boolean;
|
||||
environment: Environment;
|
||||
}
|
||||
|
||||
export function EdgeIndicator({
|
||||
environment,
|
||||
|
||||
showLastCheckInDate = false,
|
||||
}: Props) {
|
||||
const associated = !!environment.EdgeID;
|
||||
|
||||
const isValid = useHasHeartbeat(environment, associated);
|
||||
|
||||
if (isValid === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!associated) {
|
||||
return (
|
||||
<span role="status" aria-label="edge-status">
|
||||
<span className="label label-default" aria-label="unassociated">
|
||||
<s>associated</s>
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span role="status" aria-label="edge-status">
|
||||
<span
|
||||
className={clsx('label', {
|
||||
'label-danger': !isValid,
|
||||
'label-success': isValid,
|
||||
})}
|
||||
aria-label="edge-heartbeat"
|
||||
>
|
||||
heartbeat
|
||||
</span>
|
||||
|
||||
{showLastCheckInDate && !!environment.LastCheckInDate && (
|
||||
<span
|
||||
className="space-left small text-muted"
|
||||
aria-label="edge-last-checkin"
|
||||
>
|
||||
{isoDateFromTimestamp(environment.LastCheckInDate)}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function useHasHeartbeat(environment: Environment, associated: boolean) {
|
||||
const settingsQuery = usePublicSettings({ enabled: associated });
|
||||
|
||||
if (!associated) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { LastCheckInDate, QueryDate } = environment;
|
||||
|
||||
const settings = settingsQuery.data;
|
||||
|
||||
if (!settings) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const checkInInterval = getCheckinInterval(environment, settings);
|
||||
|
||||
if (checkInInterval && QueryDate && LastCheckInDate) {
|
||||
return QueryDate - LastCheckInDate <= checkInInterval * 2 + 20;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function getCheckinInterval(
|
||||
environment: Environment,
|
||||
settings: PublicSettingsViewModel
|
||||
) {
|
||||
const asyncMode = environment.Edge.AsyncMode;
|
||||
|
||||
if (asyncMode) {
|
||||
const intervals = [
|
||||
environment.Edge.PingInterval > 0
|
||||
? environment.Edge.PingInterval
|
||||
: settings.Edge.PingInterval,
|
||||
environment.Edge.SnapshotInterval > 0
|
||||
? environment.Edge.SnapshotInterval
|
||||
: settings.Edge.SnapshotInterval,
|
||||
environment.Edge.CommandInterval > 0
|
||||
? environment.Edge.CommandInterval
|
||||
: settings.Edge.CommandInterval,
|
||||
].filter((n) => n > 0);
|
||||
|
||||
return intervals.length > 0 ? Math.min(...intervals) : 60;
|
||||
}
|
||||
|
||||
if (
|
||||
!environment.EdgeCheckinInterval ||
|
||||
environment.EdgeCheckinInterval === 0
|
||||
) {
|
||||
return settings.Edge.CheckinInterval;
|
||||
}
|
||||
|
||||
return environment.EdgeCheckinInterval;
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import { Meta, Story } from '@storybook/react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { UserContext } from '@/portainer/hooks/useUser';
|
||||
import { UserContext } from '@/react/hooks/useUser';
|
||||
import { UserViewModel } from '@/portainer/models/user';
|
||||
|
||||
import { HeaderContainer } from './HeaderContainer';
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { UserContext } from '@/portainer/hooks/useUser';
|
||||
import { UserContext } from '@/react/hooks/useUser';
|
||||
import { UserViewModel } from '@/portainer/models/user';
|
||||
import { render } from '@/react-tools/test-utils';
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ import { useEffect, useState } from 'react';
|
|||
import { useStore } from 'zustand';
|
||||
|
||||
import { AutomationTestingProps } from '@/types';
|
||||
import { useUser } from '@/portainer/hooks/useUser';
|
||||
import { useUser } from '@/react/hooks/useUser';
|
||||
import { ToastNotification } from '@/react/portainer/notifications/types';
|
||||
|
||||
import { Icon } from '@@/Icon';
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Meta, Story } from '@storybook/react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { UserContext } from '@/portainer/hooks/useUser';
|
||||
import { UserContext } from '@/react/hooks/useUser';
|
||||
import { UserViewModel } from '@/portainer/models/user';
|
||||
|
||||
import { PageHeader } from './PageHeader';
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { UserContext } from '@/portainer/hooks/useUser';
|
||||
import { UserContext } from '@/react/hooks/useUser';
|
||||
import { UserViewModel } from '@/portainer/models/user';
|
||||
import { render } from '@/react-tools/test-utils';
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ import clsx from 'clsx';
|
|||
import { User, ChevronDown } from 'react-feather';
|
||||
|
||||
import { AutomationTestingProps } from '@/types';
|
||||
import { useUser } from '@/portainer/hooks/useUser';
|
||||
import { useUser } from '@/react/hooks/useUser';
|
||||
|
||||
import styles from './HeaderTitle.module.css';
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Search } from 'react-feather';
|
||||
|
||||
import { useLocalStorage } from '@/portainer/hooks/useLocalStorage';
|
||||
import { useLocalStorage } from '@/react/hooks/useLocalStorage';
|
||||
|
||||
interface Props {
|
||||
value: string;
|
||||
|
|
|
@ -2,7 +2,7 @@ import { Search } from 'react-feather';
|
|||
import { useEffect, useMemo, useState } from 'react';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { useLocalStorage } from '@/portainer/hooks/useLocalStorage';
|
||||
import { useLocalStorage } from '@/react/hooks/useLocalStorage';
|
||||
import { AutomationTestingProps } from '@/types';
|
||||
|
||||
interface Props extends AutomationTestingProps {
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
.sort-by-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.sort-by-element {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.sort-button {
|
||||
background-color: var(--bg-sortbutton-color);
|
||||
color: var(--grey-6);
|
||||
border: 1px solid var(--border-sortbutton);
|
||||
display: inline-block;
|
||||
padding: 8px 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
:global(:root[theme='dark']) .sort-button {
|
||||
color: var(--white-color);
|
||||
}
|
||||
|
||||
:global(:root[theme='highcontrast']) .sort-button {
|
||||
color: var(--white-color);
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { Filter } from '@/portainer/home/types';
|
||||
|
||||
import { Select } from '@@/form-components/ReactSelect';
|
||||
|
||||
import styles from './SortbySelector.module.css';
|
||||
|
||||
interface Props {
|
||||
filterOptions: Filter[];
|
||||
onChange: (filterOptions: Filter) => void;
|
||||
onDescending: () => void;
|
||||
placeHolder: string;
|
||||
sortByDescending: boolean;
|
||||
sortByButton: boolean;
|
||||
value?: Filter;
|
||||
}
|
||||
|
||||
export function SortbySelector({
|
||||
filterOptions,
|
||||
onChange,
|
||||
onDescending,
|
||||
placeHolder,
|
||||
sortByDescending,
|
||||
sortByButton,
|
||||
value,
|
||||
}: Props) {
|
||||
const upIcon = 'fa fa-sort-alpha-up';
|
||||
const downIcon = 'fa fa-sort-alpha-down';
|
||||
const [iconStyle, setIconStyle] = useState(downIcon);
|
||||
|
||||
useEffect(() => {
|
||||
if (sortByDescending) {
|
||||
setIconStyle(upIcon);
|
||||
} else {
|
||||
setIconStyle(downIcon);
|
||||
}
|
||||
}, [sortByDescending]);
|
||||
|
||||
return (
|
||||
<div className={styles.sortByContainer}>
|
||||
<div className={styles.sortByElement}>
|
||||
<Select
|
||||
placeholder={placeHolder}
|
||||
options={filterOptions}
|
||||
onChange={(option) => onChange(option as Filter)}
|
||||
isClearable
|
||||
value={value}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.sortByElement}>
|
||||
<button
|
||||
className={styles.sortButton}
|
||||
type="button"
|
||||
disabled={!sortByButton}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onDescending();
|
||||
}}
|
||||
>
|
||||
<i className={iconStyle} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -8,7 +8,7 @@ import {
|
|||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { useLocalStorage } from '@/portainer/hooks/useLocalStorage';
|
||||
import { useLocalStorage } from '@/react/hooks/useLocalStorage';
|
||||
|
||||
interface TableSettingsContextInterface<T> {
|
||||
settings: T;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import clsx from 'clsx';
|
||||
|
||||
import { isLimitedToBE } from '@/portainer/feature-flags/feature-flags.service';
|
||||
import { FeatureId } from '@/portainer/feature-flags/enums';
|
||||
import { isLimitedToBE } from '@/react/portainer/feature-flags/feature-flags.service';
|
||||
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||
|
||||
import { BEFeatureIndicator } from '@@/BEFeatureIndicator';
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import clsx from 'clsx';
|
||||
|
||||
import { FeatureId } from '@/portainer/feature-flags/enums';
|
||||
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||
|
||||
import { Tooltip } from '@@/Tip/Tooltip';
|
||||
|
||||
|
|
24
app/react/components/form-components/yup-file-validation.ts
Normal file
24
app/react/components/form-components/yup-file-validation.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { mixed } from 'yup';
|
||||
import { MixedSchema } from 'yup/lib/mixed';
|
||||
|
||||
type FileSchema = MixedSchema<File | undefined>;
|
||||
|
||||
export function file(): FileSchema {
|
||||
return mixed();
|
||||
}
|
||||
|
||||
export function withFileSize(fileValidation: FileSchema, maxSize: number) {
|
||||
return fileValidation.test(
|
||||
'fileSize',
|
||||
'Selected file is too big.',
|
||||
validateFileSize
|
||||
);
|
||||
|
||||
function validateFileSize(file?: File) {
|
||||
if (!file) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return file.size <= maxSize;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue