1
0
Fork 0
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:
Chaim Lev-Ari 2022-11-13 10:10:18 +02:00 committed by GitHub
parent 30e23ea5b4
commit 78dcba614d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
192 changed files with 200 additions and 211 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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