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: [
|
collectCoverageFrom: [
|
||||||
'src/**/*.{js,jsx,ts,tsx}',
|
'src/**/*.{js,jsx,ts,tsx}',
|
||||||
'!src/registerServiceWorker.js',
|
'!src/registerServiceWorker.js',
|
||||||
'!src/index.js',
|
'!src/index.ts',
|
||||||
'!src/reducers/index.js',
|
'!src/reducers/index.ts',
|
||||||
'!src/**/provideServices.js',
|
'!src/**/provideServices.ts',
|
||||||
'!src/container/*.js',
|
'!src/container/*.ts',
|
||||||
],
|
],
|
||||||
resolver: 'jest-pnp-resolver',
|
resolver: 'jest-pnp-resolver',
|
||||||
setupFiles: [
|
setupFiles: [
|
||||||
|
|
10
shlink-web-client.d.ts
vendored
10
shlink-web-client.d.ts
vendored
|
@ -1,7 +1,5 @@
|
||||||
export declare global {
|
declare module 'event-source-polyfill' {
|
||||||
declare module '*.png'
|
export const EventSourcePolyfill: any;
|
||||||
|
|
||||||
interface Window {
|
|
||||||
__REDUX_DEVTOOLS_EXTENSION_COMPOSE__?: Function;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module '*.png'
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { save, load, RLSOptions } from 'redux-localstorage-simple';
|
||||||
import reducers from '../reducers';
|
import reducers from '../reducers';
|
||||||
|
|
||||||
const isProduction = process.env.NODE_ENV !== 'production';
|
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 = {
|
const localStorageConfig: RLSOptions = {
|
||||||
states: [ 'settings', 'servers' ],
|
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 React, { FC, useEffect, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { HorizontalFormGroup } from '../../utils/HorizontalFormGroup';
|
import { HorizontalFormGroup } from '../../utils/HorizontalFormGroup';
|
||||||
import { handleEventPreventingDefault } from '../../utils/utils';
|
import { handleEventPreventingDefault } from '../../utils/utils';
|
||||||
|
import { ServerData } from '../data';
|
||||||
|
|
||||||
const propTypes = {
|
interface ServerFormProps {
|
||||||
onSubmit: PropTypes.func.isRequired,
|
onSubmit: (server: ServerData) => void;
|
||||||
initialValues: PropTypes.shape({
|
initialValues?: ServerData;
|
||||||
name: PropTypes.string.isRequired,
|
}
|
||||||
url: PropTypes.string.isRequired,
|
|
||||||
apiKey: PropTypes.string.isRequired,
|
|
||||||
}),
|
|
||||||
children: PropTypes.node.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ServerForm = ({ onSubmit, initialValues, children }) => {
|
export const ServerForm: FC<ServerFormProps> = ({ onSubmit, initialValues, children }) => {
|
||||||
const [ name, setName ] = useState('');
|
const [ name, setName ] = useState('');
|
||||||
const [ url, setUrl ] = useState('');
|
const [ url, setUrl ] = useState('');
|
||||||
const [ apiKey, setApiKey ] = useState('');
|
const [ apiKey, setApiKey ] = useState('');
|
||||||
|
@ -35,5 +30,3 @@ export const ServerForm = ({ onSubmit, initialValues, children }) => {
|
||||||
</form>
|
</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 { EventSourcePolyfill as EventSource } from 'event-source-polyfill';
|
||||||
|
import { Mock } from 'ts-mockery';
|
||||||
|
import { identity } from 'ramda';
|
||||||
import { bindToMercureTopic } from '../../../src/mercure/helpers';
|
import { bindToMercureTopic } from '../../../src/mercure/helpers';
|
||||||
|
import { MercureInfo } from '../../../src/mercure/reducers/mercureInfo';
|
||||||
|
|
||||||
jest.mock('event-source-polyfill');
|
jest.mock('event-source-polyfill');
|
||||||
|
|
||||||
|
@ -11,11 +14,13 @@ describe('helpers', () => {
|
||||||
const onTokenExpired = jest.fn();
|
const onTokenExpired = jest.fn();
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
[{ loading: true, error: false }],
|
[ Mock.of<MercureInfo>({ loading: true, error: false, mercureHubUrl: 'foo' }) ],
|
||||||
[{ loading: false, error: true }],
|
[ Mock.of<MercureInfo>({ loading: false, error: true, mercureHubUrl: 'foo' }) ],
|
||||||
[{ loading: true, error: true }],
|
[ Mock.of<MercureInfo>({ loading: true, error: true, mercureHubUrl: 'foo' }) ],
|
||||||
])('does not bind an EventSource when loading or error', (mercureInfo) => {
|
[ Mock.of<MercureInfo>({ loading: false, error: false, mercureHubUrl: undefined }) ],
|
||||||
bindToMercureTopic(mercureInfo)();
|
[ 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(EventSource).not.toHaveBeenCalled();
|
||||||
expect(onMessage).not.toHaveBeenCalled();
|
expect(onMessage).not.toHaveBeenCalled();
|
||||||
|
@ -50,7 +55,7 @@ describe('helpers', () => {
|
||||||
expect(onMessage).toHaveBeenCalledWith({ foo: 'bar' });
|
expect(onMessage).toHaveBeenCalledWith({ foo: 'bar' });
|
||||||
expect(onTokenExpired).toHaveBeenCalled();
|
expect(onTokenExpired).toHaveBeenCalled();
|
||||||
|
|
||||||
callback();
|
callback?.();
|
||||||
expect(es.close).toHaveBeenCalled();
|
expect(es.close).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
|
@ -16,6 +16,8 @@ describe('<EditServer />', () => {
|
||||||
name: 'name',
|
name: 'name',
|
||||||
url: 'url',
|
url: 'url',
|
||||||
apiKey: 'apiKey',
|
apiKey: 'apiKey',
|
||||||
|
printableVersion: 'v1.2.0',
|
||||||
|
version: '1.2.0'
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import React from 'react';
|
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 ForServerVersion from '../../../src/servers/helpers/ForServerVersion';
|
||||||
|
import { ReachableServer, SelectedServer } from '../../../src/servers/data';
|
||||||
|
|
||||||
describe('<ForServerVersion />', () => {
|
describe('<ForServerVersion />', () => {
|
||||||
let wrapped;
|
let wrapped: ReactWrapper;
|
||||||
|
|
||||||
const renderComponent = (minVersion, maxVersion, selectedServer) => {
|
const renderComponent = (selectedServer: SelectedServer, minVersion?: string, maxVersion?: string) => {
|
||||||
wrapped = mount(
|
wrapped = mount(
|
||||||
<ForServerVersion minVersion={minVersion} maxVersion={maxVersion} selectedServer={selectedServer}>
|
<ForServerVersion minVersion={minVersion} maxVersion={maxVersion} selectedServer={selectedServer}>
|
||||||
<span>Hello</span>
|
<span>Hello</span>
|
||||||
|
@ -15,10 +17,10 @@ describe('<ForServerVersion />', () => {
|
||||||
return wrapped;
|
return wrapped;
|
||||||
};
|
};
|
||||||
|
|
||||||
afterEach(() => wrapped && wrapped.unmount());
|
afterEach(() => wrapped?.unmount());
|
||||||
|
|
||||||
it('does not render children when current server is empty', () => {
|
it('does not render children when current server is empty', () => {
|
||||||
const wrapped = renderComponent('1');
|
const wrapped = renderComponent(null, '1');
|
||||||
|
|
||||||
expect(wrapped.html()).toBeNull();
|
expect(wrapped.html()).toBeNull();
|
||||||
});
|
});
|
||||||
|
@ -28,7 +30,7 @@ describe('<ForServerVersion />', () => {
|
||||||
[ undefined, '1.8.0', '1.8.3' ],
|
[ undefined, '1.8.0', '1.8.3' ],
|
||||||
[ '1.7.0', '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) => {
|
])('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();
|
expect(wrapped.html()).toBeNull();
|
||||||
});
|
});
|
||||||
|
@ -40,7 +42,7 @@ describe('<ForServerVersion />', () => {
|
||||||
[ undefined, '1.8.0', '1.7.1' ],
|
[ undefined, '1.8.0', '1.7.1' ],
|
||||||
[ '1.7.0', '1.8.0', '1.7.3' ],
|
[ '1.7.0', '1.8.0', '1.7.3' ],
|
||||||
])('renders children when current version matches requirements', (min, max, version) => {
|
])('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>');
|
expect(wrapped.html()).toContain('<span>Hello</span>');
|
||||||
});
|
});
|
|
@ -1,20 +1,18 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
import { ServerForm } from '../../../src/servers/helpers/ServerForm';
|
import { ServerForm } from '../../../src/servers/helpers/ServerForm';
|
||||||
import { HorizontalFormGroup } from '../../../src/utils/HorizontalFormGroup';
|
import { HorizontalFormGroup } from '../../../src/utils/HorizontalFormGroup';
|
||||||
|
|
||||||
describe('<ServerForm />', () => {
|
describe('<ServerForm />', () => {
|
||||||
let wrapper;
|
let wrapper: ShallowWrapper;
|
||||||
const onSubmit = jest.fn();
|
const onSubmit = jest.fn();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
wrapper = shallow(<ServerForm onSubmit={onSubmit}><span>Something</span></ServerForm>);
|
wrapper = shallow(<ServerForm onSubmit={onSubmit}><span>Something</span></ServerForm>);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => wrapper?.unmount());
|
||||||
jest.resetAllMocks();
|
afterEach(jest.resetAllMocks);
|
||||||
wrapper && wrapper.unmount();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders components', () => {
|
it('renders components', () => {
|
||||||
expect(wrapper.find(HorizontalFormGroup)).toHaveLength(3);
|
expect(wrapper.find(HorizontalFormGroup)).toHaveLength(3);
|
Loading…
Reference in a new issue