mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-01-03 14:57:22 +03:00
Migrated to TS all servers helpers
This commit is contained in:
parent
8cc0695ee9
commit
aee4c2d02f
14 changed files with 129 additions and 137 deletions
|
@ -3,10 +3,10 @@ module.exports = {
|
|||
collectCoverageFrom: [
|
||||
'src/**/*.{js,jsx,ts,tsx}',
|
||||
'!src/registerServiceWorker.js',
|
||||
'!src/index.js',
|
||||
'!src/reducers/index.js',
|
||||
'!src/**/provideServices.js',
|
||||
'!src/container/*.js',
|
||||
'!src/index.ts',
|
||||
'!src/reducers/index.ts',
|
||||
'!src/**/provideServices.ts',
|
||||
'!src/container/*.ts',
|
||||
],
|
||||
resolver: 'jest-pnp-resolver',
|
||||
setupFiles: [
|
||||
|
|
10
shlink-web-client.d.ts
vendored
10
shlink-web-client.d.ts
vendored
|
@ -1,7 +1,5 @@
|
|||
export declare global {
|
||||
declare module '*.png'
|
||||
|
||||
interface Window {
|
||||
__REDUX_DEVTOOLS_EXTENSION_COMPOSE__?: Function;
|
||||
}
|
||||
declare module 'event-source-polyfill' {
|
||||
export const EventSourcePolyfill: any;
|
||||
}
|
||||
|
||||
declare module '*.png'
|
||||
|
|
|
@ -4,7 +4,7 @@ import { save, load, RLSOptions } from 'redux-localstorage-simple';
|
|||
import reducers from '../reducers';
|
||||
|
||||
const isProduction = process.env.NODE_ENV !== 'production';
|
||||
const composeEnhancers: Function = !isProduction && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
||||
const composeEnhancers: Function = !isProduction && (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
||||
|
||||
const localStorageConfig: RLSOptions = {
|
||||
states: [ 'settings', 'servers' ],
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
import { useEffect } from 'react';
|
||||
import { EventSourcePolyfill as EventSource } from 'event-source-polyfill';
|
||||
|
||||
export const bindToMercureTopic = (mercureInfo, topic, onMessage, onTokenExpired) => () => {
|
||||
const { mercureHubUrl, token, loading, error } = mercureInfo;
|
||||
|
||||
if (loading || error) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const hubUrl = new URL(mercureHubUrl);
|
||||
|
||||
hubUrl.searchParams.append('topic', topic);
|
||||
const es = new EventSource(hubUrl, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
es.onmessage = ({ data }) => onMessage(JSON.parse(data));
|
||||
es.onerror = ({ status }) => status === 401 && onTokenExpired();
|
||||
|
||||
return () => es.close();
|
||||
};
|
||||
|
||||
export const useMercureTopicBinding = (mercureInfo, topic, onMessage, onTokenExpired) => {
|
||||
useEffect(bindToMercureTopic(mercureInfo, topic, onMessage, onTokenExpired), [ mercureInfo ]);
|
||||
};
|
34
src/mercure/helpers/index.ts
Normal file
34
src/mercure/helpers/index.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { useEffect } from 'react';
|
||||
import { EventSourcePolyfill as EventSource } from 'event-source-polyfill';
|
||||
import { MercureInfo } from '../reducers/mercureInfo';
|
||||
|
||||
export const bindToMercureTopic = <T>(mercureInfo: MercureInfo, topic: string, onMessage: (message: T) => void, onTokenExpired: Function) => () => { // eslint-disable-line max-len
|
||||
const { mercureHubUrl, token, loading, error } = mercureInfo;
|
||||
|
||||
if (loading || error || !mercureHubUrl) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const hubUrl = new URL(mercureHubUrl);
|
||||
|
||||
hubUrl.searchParams.append('topic', topic);
|
||||
const es = new EventSource(hubUrl, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
es.onmessage = ({ data }: { data: string }) => onMessage(JSON.parse(data) as T);
|
||||
es.onerror = ({ status }: { status: number }) => status === 401 && onTokenExpired();
|
||||
|
||||
return () => es.close();
|
||||
};
|
||||
|
||||
export const useMercureTopicBinding = <T>(
|
||||
mercureInfo: MercureInfo,
|
||||
topic: string,
|
||||
onMessage: (message: T) => void,
|
||||
onTokenExpired: Function,
|
||||
) => {
|
||||
useEffect(bindToMercureTopic(mercureInfo, topic, onMessage, onTokenExpired), [ mercureInfo ]);
|
||||
};
|
|
@ -1,30 +0,0 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { serverType } from '../prop-types';
|
||||
import { versionMatch } from '../../utils/helpers/version';
|
||||
|
||||
const propTypes = {
|
||||
minVersion: PropTypes.string,
|
||||
maxVersion: PropTypes.string,
|
||||
selectedServer: serverType,
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
const ForServerVersion = ({ minVersion, maxVersion, selectedServer, children }) => {
|
||||
if (!selectedServer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { version } = selectedServer;
|
||||
const matchesVersion = versionMatch(version, { maxVersion, minVersion });
|
||||
|
||||
if (!matchesVersion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <React.Fragment>{children}</React.Fragment>;
|
||||
};
|
||||
|
||||
ForServerVersion.propTypes = propTypes;
|
||||
|
||||
export default ForServerVersion;
|
24
src/servers/helpers/ForServerVersion.tsx
Normal file
24
src/servers/helpers/ForServerVersion.tsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
import React, { FC } from 'react';
|
||||
import { versionMatch, Versions } from '../../utils/helpers/version';
|
||||
import { isReachableServer, SelectedServer } from '../data';
|
||||
|
||||
interface ForServerVersionProps extends Versions {
|
||||
selectedServer: SelectedServer;
|
||||
}
|
||||
|
||||
const ForServerVersion: FC<ForServerVersionProps> = ({ minVersion, maxVersion, selectedServer, children }) => {
|
||||
if (!isReachableServer(selectedServer)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { version } = selectedServer;
|
||||
const matchesVersion = versionMatch(version, { maxVersion, minVersion });
|
||||
|
||||
if (!matchesVersion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <React.Fragment>{children}</React.Fragment>;
|
||||
};
|
||||
|
||||
export default ForServerVersion;
|
|
@ -1,19 +1,14 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { FC, useEffect, useState } from 'react';
|
||||
import { HorizontalFormGroup } from '../../utils/HorizontalFormGroup';
|
||||
import { handleEventPreventingDefault } from '../../utils/utils';
|
||||
import { ServerData } from '../data';
|
||||
|
||||
const propTypes = {
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
initialValues: PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
url: PropTypes.string.isRequired,
|
||||
apiKey: PropTypes.string.isRequired,
|
||||
}),
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
interface ServerFormProps {
|
||||
onSubmit: (server: ServerData) => void;
|
||||
initialValues?: ServerData;
|
||||
}
|
||||
|
||||
export const ServerForm = ({ onSubmit, initialValues, children }) => {
|
||||
export const ServerForm: FC<ServerFormProps> = ({ onSubmit, initialValues, children }) => {
|
||||
const [ name, setName ] = useState('');
|
||||
const [ url, setUrl ] = useState('');
|
||||
const [ apiKey, setApiKey ] = useState('');
|
||||
|
@ -35,5 +30,3 @@ export const ServerForm = ({ onSubmit, initialValues, children }) => {
|
|||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
ServerForm.propTypes = propTypes;
|
|
@ -1,35 +0,0 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Message from '../../utils/Message';
|
||||
import { serverType } from '../prop-types';
|
||||
|
||||
const propTypes = {
|
||||
selectServer: PropTypes.func,
|
||||
selectedServer: serverType,
|
||||
match: PropTypes.object,
|
||||
};
|
||||
|
||||
export const withSelectedServer = (WrappedComponent, ServerError) => {
|
||||
const Component = (props) => {
|
||||
const { selectServer, selectedServer, match } = props;
|
||||
const { params: { serverId } } = match;
|
||||
|
||||
useEffect(() => {
|
||||
selectServer(serverId);
|
||||
}, [ serverId ]);
|
||||
|
||||
if (!selectedServer) {
|
||||
return <Message loading />;
|
||||
}
|
||||
|
||||
if (selectedServer.serverNotFound) {
|
||||
return <ServerError />;
|
||||
}
|
||||
|
||||
return <WrappedComponent {...props} />;
|
||||
};
|
||||
|
||||
Component.propTypes = propTypes;
|
||||
|
||||
return Component;
|
||||
};
|
29
src/servers/helpers/withSelectedServer.tsx
Normal file
29
src/servers/helpers/withSelectedServer.tsx
Normal file
|
@ -0,0 +1,29 @@
|
|||
import React, { FC, useEffect } from 'react';
|
||||
import { RouteChildrenProps } from 'react-router';
|
||||
import Message from '../../utils/Message';
|
||||
import { isReachableServer, SelectedServer } from '../data';
|
||||
|
||||
interface WithSelectedServerProps extends RouteChildrenProps<{ serverId: string }> {
|
||||
selectServer: (serverId: string) => void;
|
||||
selectedServer: SelectedServer;
|
||||
}
|
||||
|
||||
export const withSelectedServer = (WrappedComponent: FC<WithSelectedServerProps>, ServerError: FC) => (
|
||||
props: WithSelectedServerProps,
|
||||
) => {
|
||||
const { selectServer, selectedServer, match } = props;
|
||||
|
||||
useEffect(() => {
|
||||
match?.params?.serverId && selectServer(match?.params.serverId);
|
||||
}, [ match?.params.serverId ]);
|
||||
|
||||
if (!selectedServer) {
|
||||
return <Message loading />;
|
||||
}
|
||||
|
||||
if (!isReachableServer(selectedServer)) {
|
||||
return <ServerError />;
|
||||
}
|
||||
|
||||
return <WrappedComponent {...props} />;
|
||||
};
|
|
@ -1,5 +1,8 @@
|
|||
import { EventSourcePolyfill as EventSource } from 'event-source-polyfill';
|
||||
import { Mock } from 'ts-mockery';
|
||||
import { identity } from 'ramda';
|
||||
import { bindToMercureTopic } from '../../../src/mercure/helpers';
|
||||
import { MercureInfo } from '../../../src/mercure/reducers/mercureInfo';
|
||||
|
||||
jest.mock('event-source-polyfill');
|
||||
|
||||
|
@ -11,11 +14,13 @@ describe('helpers', () => {
|
|||
const onTokenExpired = jest.fn();
|
||||
|
||||
it.each([
|
||||
[{ loading: true, error: false }],
|
||||
[{ loading: false, error: true }],
|
||||
[{ loading: true, error: true }],
|
||||
])('does not bind an EventSource when loading or error', (mercureInfo) => {
|
||||
bindToMercureTopic(mercureInfo)();
|
||||
[ Mock.of<MercureInfo>({ loading: true, error: false, mercureHubUrl: 'foo' }) ],
|
||||
[ Mock.of<MercureInfo>({ loading: false, error: true, mercureHubUrl: 'foo' }) ],
|
||||
[ Mock.of<MercureInfo>({ loading: true, error: true, mercureHubUrl: 'foo' }) ],
|
||||
[ Mock.of<MercureInfo>({ loading: false, error: false, mercureHubUrl: undefined }) ],
|
||||
[ Mock.of<MercureInfo>({ loading: true, error: true, mercureHubUrl: undefined }) ],
|
||||
])('does not bind an EventSource when loading, error or no hub URL', (mercureInfo) => {
|
||||
bindToMercureTopic(mercureInfo, '', identity, identity)();
|
||||
|
||||
expect(EventSource).not.toHaveBeenCalled();
|
||||
expect(onMessage).not.toHaveBeenCalled();
|
||||
|
@ -50,7 +55,7 @@ describe('helpers', () => {
|
|||
expect(onMessage).toHaveBeenCalledWith({ foo: 'bar' });
|
||||
expect(onTokenExpired).toHaveBeenCalled();
|
||||
|
||||
callback();
|
||||
callback?.();
|
||||
expect(es.close).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -16,6 +16,8 @@ describe('<EditServer />', () => {
|
|||
name: 'name',
|
||||
url: 'url',
|
||||
apiKey: 'apiKey',
|
||||
printableVersion: 'v1.2.0',
|
||||
version: '1.2.0'
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { mount, ReactWrapper } from 'enzyme';
|
||||
import { Mock } from 'ts-mockery';
|
||||
import ForServerVersion from '../../../src/servers/helpers/ForServerVersion';
|
||||
import { ReachableServer, SelectedServer } from '../../../src/servers/data';
|
||||
|
||||
describe('<ForServerVersion />', () => {
|
||||
let wrapped;
|
||||
let wrapped: ReactWrapper;
|
||||
|
||||
const renderComponent = (minVersion, maxVersion, selectedServer) => {
|
||||
const renderComponent = (selectedServer: SelectedServer, minVersion?: string, maxVersion?: string) => {
|
||||
wrapped = mount(
|
||||
<ForServerVersion minVersion={minVersion} maxVersion={maxVersion} selectedServer={selectedServer}>
|
||||
<span>Hello</span>
|
||||
|
@ -15,10 +17,10 @@ describe('<ForServerVersion />', () => {
|
|||
return wrapped;
|
||||
};
|
||||
|
||||
afterEach(() => wrapped && wrapped.unmount());
|
||||
afterEach(() => wrapped?.unmount());
|
||||
|
||||
it('does not render children when current server is empty', () => {
|
||||
const wrapped = renderComponent('1');
|
||||
const wrapped = renderComponent(null, '1');
|
||||
|
||||
expect(wrapped.html()).toBeNull();
|
||||
});
|
||||
|
@ -28,7 +30,7 @@ describe('<ForServerVersion />', () => {
|
|||
[ undefined, '1.8.0', '1.8.3' ],
|
||||
[ '1.7.0', '1.8.0', '1.8.3' ],
|
||||
])('does not render children when current version does not match requirements', (min, max, version) => {
|
||||
const wrapped = renderComponent(min, max, { version });
|
||||
const wrapped = renderComponent(Mock.of<ReachableServer>({ version, printableVersion: version }), min, max);
|
||||
|
||||
expect(wrapped.html()).toBeNull();
|
||||
});
|
||||
|
@ -40,7 +42,7 @@ describe('<ForServerVersion />', () => {
|
|||
[ undefined, '1.8.0', '1.7.1' ],
|
||||
[ '1.7.0', '1.8.0', '1.7.3' ],
|
||||
])('renders children when current version matches requirements', (min, max, version) => {
|
||||
const wrapped = renderComponent(min, max, { version });
|
||||
const wrapped = renderComponent(Mock.of<ReachableServer>({ version, printableVersion: version }), min, max);
|
||||
|
||||
expect(wrapped.html()).toContain('<span>Hello</span>');
|
||||
});
|
|
@ -1,20 +1,18 @@
|
|||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { shallow, ShallowWrapper } from 'enzyme';
|
||||
import { ServerForm } from '../../../src/servers/helpers/ServerForm';
|
||||
import { HorizontalFormGroup } from '../../../src/utils/HorizontalFormGroup';
|
||||
|
||||
describe('<ServerForm />', () => {
|
||||
let wrapper;
|
||||
let wrapper: ShallowWrapper;
|
||||
const onSubmit = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(<ServerForm onSubmit={onSubmit}><span>Something</span></ServerForm>);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
wrapper && wrapper.unmount();
|
||||
});
|
||||
afterEach(() => wrapper?.unmount());
|
||||
afterEach(jest.resetAllMocks);
|
||||
|
||||
it('renders components', () => {
|
||||
expect(wrapper.find(HorizontalFormGroup)).toHaveLength(3);
|
Loading…
Reference in a new issue