8000 Add plugin dashboard · corner4world/rclone-webui-react@83525bf · GitHub
[go: up one dir, main page]

Skip to content

Commit 83525bf

Browse files
committed
Add plugin dashboard
1 parent 0cf5135 commit 83525bf

File tree

11 files changed

+298
-8
lines changed

11 files changed

+298
-8
lines changed

src/_nav.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ export default {
2222
},
2323
{
2424
name: 'Plugins',
25-
url: '/pluginsDashboard',
26-
icon: 'icon-plugins'
25+
url: '/pluginDashboard',
26+
icon: 'fa fa-plug'
2727
},
2828
{
2929
name: 'Mounts',

src/actions/pluginActions.js

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import axiosInstance from "../utils/API/API";
22
import urls from "../utils/API/endpoint";
3-
import {ADD_TEST_PLUGIN, GET_TEST_PLUGINS, REQUEST_ERROR, REQUEST_SUCCESS} from "./types";
3+
import {ADD_TEST_PLUGIN, GET_TEST_PLUGINS, LOAD_PLUGINS, REQUEST_ERROR, REQUEST_SUCCESS} from "./types";
4+
import {toast} from "react-toastify";
45

56
/**
67
* Load test plugins
@@ -36,3 +37,32 @@ export const addTestPlugin = (pluginName, pluginUrl) => (dispatch) => {
3637
})
3738
})
3839
}
40+
41+
export const getPlugins = () => (dispatch) => {
42+
axiosInstance.post("pluginsctl/getPlugins").then(res => {
43+
dispatch({
44+
type: LOAD_PLUGINS,
45+
status: REQUEST_SUCCESS,
46+
payload: res.data
47+
})
48+
},
49+
(error) => {
50+
dispatch({
51+
type: LOAD_PLUGINS,
52+
status: REQUEST_ERROR,
53+
payload: error
54+
})
55+
}
56+
)
57+
58+
}
59+
60+
export const addPlugin = (pluginURL) => dispatch => {
61+
axiosInstance.post("pluginsctl/addPlugin", {url: pluginURL}).then(res => {
62+
toast.info(`Plugin ${pluginURL} added successfully`);
63+
// reload plugins database
64+
dispatch(getPlugins())
65+
}, (error) => {
66+
toast.error(`Error adding plugin. Please try again: ${error.data}`);
67+
})
68+
}

src/actions/types.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export const REMOVE_MOUNT = "REMOVE_MOUNT";
4343
export const CREATE_MOUNT = "CREATE_MOUNT";
4444

4545
export const ADD_TEST_PLUGIN = "ADD_TEST_PLUGIN";
46+
export const LOAD_PLUGINS = "LOAD_PLUGINS";
4647

4748
export const REQUEST_ERROR = 'ERROR';
4849
export const REQUEST_SUCCESS = 'SUCCESS';

src/reducers/pluginsReducer.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import {ADD_TEST_PLUGIN, GET_TEST_PLUGINS, REQUEST_ERROR, REQUEST_SUCCESS} from "../actions/types";
1+
import {ADD_TEST_PLUGIN, GET_TEST_PLUGINS, LOAD_PLUGINS, REQUEST_ERROR, REQUEST_SUCCESS} from "../actions/types";
22

33
const initialState = {
44
loadedTestPlugins: {},
5+
loadedPlugins: {},
56
error: ""
67
};
78

@@ -29,6 +30,21 @@ export default function (state = initialState, action) {
2930
break;
3031
case ADD_TEST_PLUGIN:
3132
return state;
33+
case LOAD_PLUGINS:
34+
if (action.status === REQUEST_SUCCESS) {
35+
return {
36+
...state,
37+
loadedPlugins: action.payload.loadedPlugins,
38+
error: ""
39+
}
40+
} else if (action.status === REQUEST_ERROR) {
41+
return {
42+
...state,
43+
loadedPlugins: {},
44+
error: action.payload
45+
}
46+
}
47+
return state;
3248
default:
3349
return state;
3450
}

src/routes.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const ShowConfig = React.lazy(() => import('./views/RemoteManagement/ShowConfig'
66
const RemoteExplorerLayout = React.lazy(() => import("./views/Explorer/RemoteExplorerLayout"));
77
const Login = React.lazy(() => import("./views/Pages/Login"));
88
const RCloneDashboard = React.lazy(() => import("./views/RCloneDashboard"));
9+
const PluginDashboard = React.lazy(() => import("./views/PluginDashboard"));
910
const MountDashboard = React.lazy(() => import("./views/MountDashboard"));
1011

1112
// https://github.com/ReactTraining/react-router/tree/master/packages/react-router-config
@@ -20,6 +21,7 @@ const routes = [
2021
{path: '/remoteExplorer/:remoteName/:remotePath', exact: true, name: 'Explorer', component: RemoteExplorerLayout},
2122
{path: '/remoteExplorer', name: 'Explorer', component: RemoteExplorerLayout},
2223
{path: '/rcloneBackend', name: 'Rclone Backend', component: RCloneDashboard},
24+
{path: '/pluginDashboard', name: 'Plugins', component: PluginDashboard},
2325
{path: '/mountDashboard', name: 'Mount Dashboard', component: MountDashboard},
2426

2527
];

src/scss/_custom.scss

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -195,15 +195,22 @@ body{
195195
font-family: Nunito, sans-serif;
196196
font-weight: bold;
197197
}
198+
198199
.nav-link.active {
199200
background-color: #ffffff;
200-
color: rgb(63,121,173);
201+
color: rgb(63, 121, 173);
201202
}
202-
.nav-link.active > .nav-icon :hover{
203-
color: rgb(63,121,173);
203+
204+
.nav-link.active .nav-icon :hover {
205+
color: rgb(63, 121, 173);
204206
}
207+
208+
.nav-link.active .nav-icon :hover {
209+
color: rgb(63, 121, 173);
210+
}
211+
205212
.nav-link {
206-
color: rgb(114,114,114);
213+
color: rgb(114, 114, 114);
207214
padding-top: 15px;
208215
padding-bottom: 15px;
209216
border-top: 1px solid rgba(232, 239, 247, 1);

src/utils/Tools.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line chang 57AE e
@@ -182,6 +182,11 @@ export function validateDriveName(name) {
182182
return baseValidator(regex, name);
183183
}
184184

185+
export function validateURL(url) {
186+
const regex = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$/g;
187+
return baseValidator(regex, url);
188+
}
189+
185190
/**
186191
* Opens the specified URL in a new tab and focus on it.
187192
* @param url {string} URL to be opened.

src/views/PluginDashboard/PluginDashboard.js

B41A
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import React from 'react';
2+
import {connect} from "react-redux";
3+
import {
4+
Button,
5+
Card,
6+
CardBody,
7+
CardFooter,
8+
CardHeader,
9+
Col,
10+
FormFeedback,
11+
FormGroup,
12+
Input,
13+
Label,
14+
Row,
15+
Table
16+
} from "reactstrap";
17+
import * as PropTypes from 'prop-types';
18+
import {addPlugin, getPlugins} from "../../actions/pluginActions";
19+
import PluginRowEntries from "./PluginRowEntries";
20+
import {validateURL} from "../../utils/Tools";
21+
22+
// function MountRows({remotes, refreshHandle}) {
23+
//
24+
// let returnMap = [];
25+
// let curKey = 1;
26+
// for (const [key, value] of Object.entries(remotes)) {
27+
// returnMap.push((<ConfigRow sequenceNumber={curKey} key={key} remoteName={key} remote={value}
28+
// refreshHandle={refreshHandle}/>));
29+
// curKey++;
30+
// }
31+
// return returnMap;
32+
// }
33+
34+
class PluginDashboard extends React.Component {
35+
36+
constructor(props, context) {
37+
super(props, context);
38+
this.state = {
39+
pluginDownloadURL: "",
40+
showNewPluginCard: false,
41+
}
42+
}
43+
44+
componentDidMount() {
45+
const {getPlugins} = this.props;
46+
getPlugins();
47+
}
48+
49+
addPluginHandler = (e) => {
50+
e.stopPropagation();
51+
const {addPlugin} = this.props;
52+
const {pluginDownloadURL} = this.state;
53+
addPlugin(pluginDownloadURL);
54+
}
55+
56+
changePluginURL = (e) => {
57+
const pluginDownloadURL = e.target.value;
58+
this.setState({pluginDownloadURL})
59+
}
60+
61+
toggleShowNewPluginCard = () => {
62+
this.setState((state) => ({showNewPluginCard: !state.showNewPluginCard}))
63+
}
64+
65+
addButtonStatus = () => {
66+
const {pluginDownloadURL} = this.state;
67+
return !pluginDownloadURL || this.state.pluginDownloadURL === "" || !validateURL(pluginDownloadURL);
68+
}
69+
70+
render() {
71+
const {showNewPluginCard, pluginDownloadURL} = this.state;
72+
const {loadedPlugins} = this.props;
73+
return (
74+
<div data-test="pluginDashboardComponent">
75+
<Row style={{display: showNewPluginCard ? "block" : "none"}}>
76+
<Card
77+
className={"col-12"}>
78+
<CardHeader>
79+
Add Plugin
80+
</CardHeader>
81+
<CardBody>
82+
<FormGroup row>
83+
<Label for={"mountPoint"} sm={5}>Plugin URL</Label>
84+
<Col sm={7}>
85+
<Input type={"text"} value={pluginDownloadURL}
86+
name={"mountPoint"}
87+
id={"mountPoint"} onChange={this.changePluginURL} required={true}
88+
>
89+
</Input>
90+
<FormFeedback/>
91+
92+
</Col>
93+
</FormGroup>
94+
</CardBody>
95+
<CardFooter>
96+
<div className="clear-fix float-right">
97+
<Button color="primary" disabled={this.addButtonStatus()}
98+
onClick={this.addPluginHandler}>Verify and add</Button>
99+
</div>
100+
<div className="clear-fix float-right mr-2">
101+
<Button color="danger" onClick={this.toggleShowNewPluginCard}>Cancel</Button>
102+
</div>
103+
</CardFooter>
104+
</Card>
105+
</Row>
106+
107+
<Row>
108+
{!showNewPluginCard && <Col lg={8} className={"mb-4"}>
109+
<Button color={"primary"} className={"float-left"} onClick={this.toggleShowNewPluginCard}>
110+
Add new Plugin
111+
</Button>
112+
</Col>}
113+
<Col lg={4}>
114+
115+
</Col>
116+
117+
</Row>
118+
<Table responsive className="table-striped">
119+
<thead>
120+
<tr>
121+
<th>Author/ Name</th>
122+
<th>Description</th>
123+
<th>Actions</th>
124+
</tr>
125+
</thead>
126+
<tbody>
127+
<PluginRowEntries loadedPlugins={loadedPlugins}/>
128+
</tbody>
129+
</Table>
130+
</div>);
131+
}
132+
}
133+
134+
const mapStateToProps = state => ({
135+
loadedPlugins: state.plugins.loadedPlugins,
136+
});
137+
138+
PluginDashboard.propTypes = {
139+
loadedPlugins: PropTypes.object.isRequired,
140+
141+
getPlugins: PropTypes.func.isRequired,
142+
143+
addPlugin: PropTypes.func.isRequired,
144+
};
145+
146+
export default connect(mapStateToProps, {getPlugins, addPlugin})(PluginDashboard);
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import React from "react";
2+
import {shallow} from "enzyme";
3+
import {findByTestAttr, testStore} from "../../../Utils";
4+
import PluginDashboard from "./PluginDashboard";
5+
import toJson from "enzyme-to-json";
6+
7+
8+
const setUp = (intialState = {}, props = {}) => {
9+
const store = testStore(intialState);
10+
11+
const component = shallow(<PluginDashboard {...props} store={store}/>);
12+
return component.childAt(0).dive();
13+
};
14+
15+
16+
describe('Plugin Dashboard', function () {
17+
18+
describe('renders', function () {
19+
let wrapper;
20+
beforeEach(() => {
21+
const initialState = {
22+
status: {
23+
checkStatus: true
24+
}
25+
};
26+
27+
const props = {};
28+
wrapper = setUp(initialState, props)
29+
});
30+
31+
it('should render without crashing', function () {
32+
const component = findByTestAttr(wrapper, "pluginDashboardComponent");
33+
expect(component).toHaveLength(1);
34+
});
35+
36+
it('should match snapshot', function () {
37+
expect(toJson(wrapper)).toMatchSnapshot()
38+
});
39+
});
40+
41+
});
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import React from "react";
2+
import * as PropTypes from "prop-types";
3+
import {Button} from "reactstrap";
4+
5+
const deactivatePlugin = (item) => {
6+
7+
}
8+
9+
const removePlugin = (item) => {
10+
11+
}
12+
13+
function PluginRowEntries({loadedPlugins}) {
14+
15+
16+
let entries = [];
17+
for (const [key, value] of Object.entries(loadedPlugins)) {
18+
entries.push(
19+
<tr key={key}>
20+
<td>{value.name}</td>
21+
<td>{value.description}</td>
22+
<td>
23+
<Button color="primary">Deactivate</Button>
24+
<Button color={"danger"} className="ml-2">Delete</Button>
25+
</td>
26+
</tr>
27+
)
28+
}
29+
return entries;
30+
}
31+
32+
PluginRowEntries.propTypes = {
33+
loadedPlugins: PropTypes.object.isRequired,
34+
}
35+
36+
export default PluginRowEntries;

0 commit comments

Comments
 (0)
0