Browse Source

feat(images): add screen to view custom and managed nodes

master
jamaljsr 5 years ago
parent
commit
0852e47fda
  1. 12
      src/components/common/NavMenu.spec.tsx
  2. 31
      src/components/common/NavMenu.tsx
  3. 103
      src/components/nodes/CustomNodesTable.tsx
  4. 86
      src/components/nodes/ManagedNodesTable.tsx
  5. 49
      src/components/nodes/NodesView.tsx
  6. 3
      src/components/nodes/index.ts
  7. 3
      src/components/routing/Routes.tsx
  8. 1
      src/components/routing/index.ts
  9. 12
      src/i18n/locales/en-US.json
  10. BIN
      src/resources/lnd.png
  11. 27
      src/store/models/app.ts
  12. 26
      src/types/index.ts
  13. 5
      src/utils/constants.ts

12
src/components/common/NavMenu.spec.tsx

@ -1,7 +1,7 @@
import React from 'react';
import { fireEvent } from '@testing-library/dom';
import { renderWithProviders } from 'utils/tests';
import { NETWORK_NEW } from 'components/routing';
import { NETWORK_NEW, NODES_VIEW } from 'components/routing';
import NavMenu from './NavMenu';
describe('DetailsList Component', () => {
@ -10,8 +10,16 @@ describe('DetailsList Component', () => {
};
it('should navigate to /network when create menu item clicked', () => {
const { getByText, history } = renderComponent();
const { getByText, getByLabelText, history } = renderComponent();
fireEvent.click(getByLabelText('menu'));
fireEvent.click(getByText('Create Network'));
expect(history.location.pathname).toEqual(NETWORK_NEW);
});
it('should navigate to /nodes when manage nodes item clicked', () => {
const { getByText, getByLabelText, history } = renderComponent();
fireEvent.click(getByLabelText('menu'));
fireEvent.click(getByText('Manage Nodes'));
expect(history.location.pathname).toEqual(NODES_VIEW);
});
});

31
src/components/common/NavMenu.tsx

@ -1,17 +1,16 @@
import React from 'react';
import { PlusOutlined } from '@ant-design/icons';
import { HddOutlined, MenuOutlined, PlusOutlined } from '@ant-design/icons';
import styled from '@emotion/styled';
import { Menu } from 'antd';
import { Dropdown, Menu } from 'antd';
import { usePrefixedTranslation } from 'hooks';
import { useStoreActions } from 'store';
import { NETWORK_NEW } from 'components/routing';
import { NETWORK_NEW, NODES_VIEW } from 'components/routing';
const Styled = {
Menu: styled.div`
float: right;
margin-top: 9px;
`,
PlusIcon: styled(PlusOutlined)`
MenuIcon: styled(MenuOutlined)`
font-size: 1.2rem;
color: #fff;
`,
@ -20,15 +19,23 @@ const Styled = {
const NavMenu: React.FC = () => {
const { l } = usePrefixedTranslation('cmps.common.NavMenu');
const { navigateTo } = useStoreActions(s => s.app);
const menu = (
<Menu theme="dark">
<Menu.Item onClick={() => navigateTo(NETWORK_NEW)}>
<PlusOutlined />
{l('createNetwork')}
</Menu.Item>
<Menu.Item onClick={() => navigateTo(NODES_VIEW)}>
<HddOutlined />
{l('manageNodes')}
</Menu.Item>
</Menu>
);
return (
<Styled.Menu>
<Menu theme="dark" mode="horizontal" selectable={false}>
<Menu.Item onClick={() => navigateTo(NETWORK_NEW)}>
<Styled.PlusIcon />
{l('createNetwork')}
</Menu.Item>
</Menu>
<Dropdown overlay={menu} trigger={['click']}>
<Styled.MenuIcon />
</Dropdown>
</Styled.Menu>
);
};

103
src/components/nodes/CustomNodesTable.tsx

@ -0,0 +1,103 @@
import React from 'react';
import { DeleteOutlined, FormOutlined } from '@ant-design/icons';
import styled from '@emotion/styled';
import { Button, Table } from 'antd';
import { usePrefixedTranslation } from 'hooks';
import { CustomNode } from 'types';
import { dockerConfigs } from 'utils/constants';
import { getPolarPlatform } from 'utils/system';
const Styled = {
Table: styled(Table)`
margin: 20px 0;
`,
Logo: styled.img`
width: 24px;
height: 24px;
margin-right: 10px;
`,
DeleteButton: styled(Button)`
color: #a61d24;
&:hover {
color: #800f19;
}
`,
};
interface NodeInfo {
id: string;
name: string;
dockerImage: string;
logo: string;
command: string;
}
interface Props {
nodes: CustomNode[];
}
const CustomNodesTable: React.FC<Props> = ({ nodes }) => {
const { l } = usePrefixedTranslation('cmps.nodes.CustomNodesTable');
const currPlatform = getPolarPlatform();
const handleEdit = (node: NodeInfo) => {
console.warn(node);
};
const handleDelete = (node: NodeInfo) => {
console.warn(node);
};
if (!nodes.length) {
return null;
}
const customNodes: NodeInfo[] = [];
nodes.forEach(({ id, implementation, dockerImage, command }) => {
const { name, logo, platforms } = dockerConfigs[implementation];
if (!platforms.includes(currPlatform)) return;
customNodes.push({ id, name, dockerImage, logo, command });
});
return (
<Styled.Table
dataSource={customNodes}
title={() => l('title')}
pagination={false}
rowKey="id"
emptyText={<>There are no custom images.</>}
>
<Table.Column
title={l('implementation')}
dataIndex="name"
key="name"
render={(name: string, node: NodeInfo) => (
<span key="name">
<Styled.Logo src={node.logo} />
{name}
</span>
)}
/>
<Table.Column title={l('dockerImage')} dataIndex="dockerImage" key="imageName" />
<Table.Column
title=""
width={200}
align="right"
render={(_, node: NodeInfo) => (
<span key="edit">
<Button type="link" icon={<FormOutlined />} onClick={() => handleEdit(node)}>
{l('edit')}
</Button>
<Styled.DeleteButton
type="link"
icon={<DeleteOutlined />}
onClick={() => handleDelete(node)}
></Styled.DeleteButton>
</span>
)}
/>
</Styled.Table>
);
};
export default CustomNodesTable;

86
src/components/nodes/ManagedNodesTable.tsx

@ -0,0 +1,86 @@
import React from 'react';
import { FormOutlined } from '@ant-design/icons';
import styled from '@emotion/styled';
import { Button, Table } from 'antd';
import { usePrefixedTranslation } from 'hooks';
import { ManagedNode } from 'types';
import { dockerConfigs } from 'utils/constants';
import { getPolarPlatform } from 'utils/system';
const Styled = {
Table: styled(Table)`
margin: 20px 0;
`,
Logo: styled.img`
width: 24px;
height: 24px;
margin-right: 10px;
`,
};
interface NodeInfo {
index: number;
name: string;
imageName: string;
logo: string;
version: string;
command: string;
}
interface Props {
nodes: ManagedNode[];
}
const ManagedNodesTable: React.FC<Props> = ({ nodes }) => {
const { l } = usePrefixedTranslation('cmps.nodes.ManagedNodesTable');
const currPlatform = getPolarPlatform();
const handleCustomize = (node: NodeInfo) => {
console.warn(node);
};
const managedNodes: NodeInfo[] = [];
nodes.forEach(({ implementation, version, command }, index) => {
const { name, imageName, logo, platforms } = dockerConfigs[implementation];
if (!platforms.includes(currPlatform)) return;
managedNodes.push({ index, name, imageName, logo, version, command });
});
return (
<Styled.Table
dataSource={managedNodes}
title={() => l('title')}
pagination={false}
rowKey="index"
>
<Table.Column
title={l('implementation')}
dataIndex="name"
render={(name: string, node: NodeInfo) => (
<span key="name">
<Styled.Logo src={node.logo} />
{name}
</span>
)}
/>
<Table.Column title={l('dockerImage')} dataIndex="imageName" />
<Table.Column title={l('version')} dataIndex="version" />
<Table.Column
title=""
width={150}
align="right"
render={(_, node: NodeInfo) => (
<Button
type="link"
icon={<FormOutlined />}
onClick={() => handleCustomize(node)}
>
{l('customize')}
</Button>
)}
/>
</Styled.Table>
);
};
export default ManagedNodesTable;

49
src/components/nodes/NodesView.tsx

@ -0,0 +1,49 @@
import React, { useEffect } from 'react';
import { info } from 'electron-log';
import { PlusOutlined } from '@ant-design/icons';
import styled from '@emotion/styled';
import { Button, PageHeader } from 'antd';
import { usePrefixedTranslation } from 'hooks';
import { useTheme } from 'hooks/useTheme';
import { useStoreActions, useStoreState } from 'store';
import { ThemeColors } from 'theme/colors';
import { HOME } from 'components/routing';
import { CustomNodesTable, ManagedNodesTable } from './';
const Styled = {
PageHeader: styled(PageHeader)<{ colors: ThemeColors['pageHeader'] }>`
border: 1px solid ${props => props.colors.border};
border-radius: 2px;
background-color: ${props => props.colors.background};
margin-bottom: 10px;
flex: 0;
`,
};
const NodesView: React.FC = () => {
useEffect(() => info('Rendering NodesView component'), []);
const { l } = usePrefixedTranslation('cmps.nodes.NodesView');
const theme = useTheme();
const { managedNodes, settings } = useStoreState(s => s.app);
const { navigateTo } = useStoreActions(s => s.app);
return (
<>
<Styled.PageHeader
title={l('title')}
colors={theme.pageHeader}
onBack={() => navigateTo(HOME)}
extra={
<Button type="primary" icon={<PlusOutlined />}>
{l('addBtn')}
</Button>
}
/>
<CustomNodesTable nodes={settings.nodes.custom} />
<ManagedNodesTable nodes={managedNodes} />
</>
);
};
export default NodesView;

3
src/components/nodes/index.ts

@ -0,0 +1,3 @@
export { default as CustomNodesTable } from './CustomNodesTable';
export { default as ManagedNodesTable } from './ManagedNodesTable';
export { default as NodesView } from './NodesView';

3
src/components/routing/Routes.tsx

@ -3,10 +3,12 @@ import { Route, Switch } from 'react-router';
import { Home } from 'components/home';
import { AppLayout } from 'components/layouts';
import { NetworkView, NewNetwork } from 'components/network';
import { NodesView } from 'components/nodes';
import {
HOME,
NETWORK_NEW,
NETWORK_VIEW,
NODES_VIEW,
Switch as AnimatedSwitch,
TERMINAL,
} from 'components/routing';
@ -21,6 +23,7 @@ const Routes: React.FC = () => (
<Route path={HOME} exact component={Home} />
<Route path={NETWORK_NEW} exact component={NewNetwork} />
<Route path={NETWORK_VIEW(':id')} component={NetworkView} />
<Route path={NODES_VIEW} component={NodesView} />
</AnimatedSwitch>
</AppLayout>
</Route>

1
src/components/routing/index.ts

@ -4,5 +4,6 @@ export { default as Routes } from './Routes';
export const HOME = '/';
export const NETWORK_NEW = '/network';
export const NETWORK_VIEW = (id: number | string) => `/network/${id}`;
export const NODES_VIEW = '/nodes';
export const TERMINAL = (type: number | string, name: number | string) =>
`/terminal/${type}/${name}`;

12
src/i18n/locales/en-US.json

@ -8,6 +8,7 @@
"cmps.common.form.LightningNodeSelect.balance": "Balance",
"cmps.common.CopyIcon.message": "Copied {{label}} to clipboard",
"cmps.common.NavMenu.createNetwork": "Create Network",
"cmps.common.NavMenu.manageNodes": "Manage Nodes",
"cmps.common.OpenTerminalButton.title": "Terminal",
"cmps.common.OpenTerminalButton.btn": "Launch",
"cmps.common.OpenTerminalButton.info": "Run '{{cmd}}' commands directly on the node",
@ -251,6 +252,17 @@
"cmps.network.NewNetwork.clightningWindows": "Not supported on Windows yet.",
"cmps.network.NewNetwork.bitcoindNodesLabel": "How many bitcoind nodes?",
"cmps.network.NewNetwork.btnCreate": "Create",
"cmps.nodes.CustomNodesTable.title": "Custom Nodes",
"cmps.nodes.CustomNodesTable.implementation": "Implementation",
"cmps.nodes.CustomNodesTable.dockerImage": "Docker Image",
"cmps.nodes.CustomNodesTable.edit": "Edit",
"cmps.nodes.ManagedNodesTable.title": "Nodes Managed by Polar",
"cmps.nodes.ManagedNodesTable.implementation": "Implementation",
"cmps.nodes.ManagedNodesTable.dockerImage": "Docker Image",
"cmps.nodes.ManagedNodesTable.version": "Version",
"cmps.nodes.ManagedNodesTable.customize": "Customize",
"cmps.nodes.NodesView.title": "Customize Node Docker Images",
"cmps.nodes.NodesView.addBtn": "Add a Custom Node",
"cmps.terminal.DockerTerminal.connected": "Connected to the {{type}} docker container {{name}} ...",
"cmps.terminal.DockerTerminal.connectErr": "Unable to connect to terminal",
"cmps.terminal.DockerTerminal.nodeTypeErr": "Invalid node type '{{type}}'",

BIN
src/resources/lnd.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 44 KiB

27
src/store/models/app.ts

@ -3,13 +3,15 @@ import { shell } from 'electron';
import { notification } from 'antd';
import { ArgsProps } from 'antd/lib/notification';
import { push } from 'connected-react-router';
import { Action, action, Thunk, thunk } from 'easy-peasy';
import { Action, action, Computed, computed, Thunk, thunk } from 'easy-peasy';
import { ipcChannels } from 'shared';
import { NodeImplementation } from 'shared/types';
import {
AppSettings,
DockerRepoState,
DockerRepoUpdates,
DockerVersions,
ManagedNode,
StoreInjections,
} from 'types';
import { defaultRepoState } from 'utils/constants';
@ -29,6 +31,7 @@ export interface AppModel {
// images that have been pulled/downloaded from Docker Hub
dockerImages: string[];
// all images that are available on Docker Hub
managedNodes: Computed<AppModel, ManagedNode[]>;
dockerRepoState: DockerRepoState;
setInitialized: Action<AppModel, boolean>;
setSettings: Action<AppModel, Partial<AppSettings>>;
@ -63,10 +66,32 @@ const appModel: AppModel = {
lang: getI18n().language,
theme: 'dark',
showAllNodeVersions: false,
nodes: {
managed: [],
custom: [],
},
},
dockerVersions: { docker: '', compose: '' },
dockerImages: [],
dockerRepoState: defaultRepoState,
// computed properties
managedNodes: computed(state => {
// the list of managed nodes should be computed to merge the user-defined
// commands with the hard-coded nodes
const nodes: ManagedNode[] = [];
const { managed } = state.settings.nodes;
Object.entries(state.dockerRepoState.images).forEach(([type, entry]) => {
entry.versions.forEach(version => {
const m = managed.find(n => n.implementation === type && n.version === version);
nodes.push({
implementation: type as NodeImplementation,
version,
command: m ? m.command : '',
});
});
});
return nodes;
}),
// reducer actions (mutations allowed thx to immer)
setInitialized: action((state, initialized) => {
state.initialized = initialized;

26
src/types/index.ts

@ -22,10 +22,35 @@ export interface Network {
};
}
/**
* Managed nodes are hard-coded with docker images pushed to the
* Docker Hub polarlightning repo
*/
export interface ManagedNode {
implementation: NodeImplementation;
version: string;
command: string;
}
/**
* Custom nodes are created by the user using docker images that only
* exist locally on the user's machine
*/
export interface CustomNode {
id: string;
implementation: NodeImplementation;
dockerImage: string;
command: string;
}
export interface AppSettings {
lang: string;
theme: 'light' | 'dark';
showAllNodeVersions: boolean;
nodes: {
managed: ManagedNode[];
custom: CustomNode[];
};
}
export interface SettingsInjection {
@ -40,6 +65,7 @@ export interface DockerVersions {
export interface DockerConfig {
name: string;
imageName: string;
logo: string;
platforms: PolarPlatform[];
volumeDirName: string;

5
src/utils/constants.ts

@ -69,12 +69,14 @@ export const bitcoinCredentials = {
export const dockerConfigs: Record<NodeImplementation, DockerConfig> = {
LND: {
name: 'LND',
imageName: 'polarlightning/lnd',
logo: lndLogo,
platforms: ['mac', 'linux', 'windows'],
volumeDirName: 'lnd',
},
'c-lightning': {
name: 'c-lightning',
imageName: 'polarlightning/clightning',
logo: clightningLogo,
platforms: ['mac', 'linux'],
volumeDirName: 'c-lightning',
@ -83,18 +85,21 @@ export const dockerConfigs: Record<NodeImplementation, DockerConfig> = {
},
eclair: {
name: 'Eclair',
imageName: '',
logo: '',
platforms: ['mac', 'linux', 'windows'],
volumeDirName: 'eclair',
},
bitcoind: {
name: 'Bitcoin Core',
imageName: 'polarlightning/bitcoind',
logo: bitcoindLogo,
platforms: ['mac', 'linux', 'windows'],
volumeDirName: 'bitcoind',
},
btcd: {
name: 'btcd',
imageName: '',
logo: '',
platforms: ['mac', 'linux', 'windows'],
volumeDirName: 'btcd',

Loading…
Cancel
Save