parent
30c963a5f0
commit
61a1c93f0f
89 changed files with 15859 additions and 2 deletions
3
client/.babelrc
Normal file
3
client/.babelrc
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"presets": ["env", "react", "stage-0", "stage-2"]
|
||||
}
|
145
client/components/composite/auth_pane.jsx
Normal file
145
client/components/composite/auth_pane.jsx
Normal file
|
@ -0,0 +1,145 @@
|
|||
import React from "react";
|
||||
import { Button } from "../control/button";
|
||||
import { Input } from "../control/input";
|
||||
|
||||
import { config } from "../../config";
|
||||
import { getIcon } from "../display/icon";
|
||||
import { makePostBody } from "../../libs/utils";
|
||||
import { styleButtonLabel } from "./info_bar";
|
||||
|
||||
export const classLogin = "auth-pane-login";
|
||||
export const classLogout = "auth-pane-logout";
|
||||
const IconSignIn = getIcon("signIn");
|
||||
const IconSignOut = getIcon("signOut");
|
||||
const IconAngRight = getIcon("angRight");
|
||||
|
||||
export class AuthPane extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
adminId: "",
|
||||
adminPwd: ""
|
||||
};
|
||||
}
|
||||
|
||||
onLogin = e => {
|
||||
e.preventDefault();
|
||||
this.props.onLogin(
|
||||
this.props.serverAddr,
|
||||
this.state.adminId,
|
||||
this.state.adminPwd
|
||||
);
|
||||
};
|
||||
|
||||
onLogout = () => {
|
||||
this.props.onLogout(this.props.serverAddr);
|
||||
};
|
||||
|
||||
onChangeAdminId = adminId => {
|
||||
this.setState({ adminId });
|
||||
};
|
||||
|
||||
onChangeAdminPwd = adminPwd => {
|
||||
this.setState({ adminPwd });
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.props.isLogin) {
|
||||
return (
|
||||
<span className={classLogout} style={this.props.styleContainer}>
|
||||
<Button
|
||||
onClick={this.onLogout}
|
||||
icon={<IconSignOut size={config.rootSize} />}
|
||||
label={"Logout"}
|
||||
styleLabel={styleButtonLabel}
|
||||
styleDefault={{ color: "#666" }}
|
||||
styleContainer={{ backgroundColor: "#ccc" }}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
if (this.props.compact) {
|
||||
return (
|
||||
<form
|
||||
onSubmit={this.onLogin}
|
||||
className={classLogin}
|
||||
style={this.props.styleContainer}
|
||||
>
|
||||
<Input
|
||||
placeholder="user name"
|
||||
type="text"
|
||||
onChange={this.onChangeAdminId}
|
||||
value={this.state.adminId}
|
||||
styleContainer={{ margin: "0.5rem" }}
|
||||
icon={<IconAngRight size={config.rootSize} />}
|
||||
/>
|
||||
<Input
|
||||
placeholder="password"
|
||||
type="password"
|
||||
onChange={this.onChangeAdminPwd}
|
||||
value={this.state.adminPwd}
|
||||
styleContainer={{ margin: "0.5rem" }}
|
||||
icon={<IconAngRight size={config.rootSize} />}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
icon={<IconSignIn size={config.rootSize} />}
|
||||
label={"login"}
|
||||
styleLabel={styleButtonLabel}
|
||||
styleDefault={{ color: "#fff" }}
|
||||
styleContainer={{
|
||||
backgroundColor: "#2c3e50",
|
||||
marginLeft: "0.5rem"
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<form
|
||||
onSubmit={this.onLogin}
|
||||
className={classLogin}
|
||||
style={this.props.styleContainer}
|
||||
>
|
||||
<Input
|
||||
placeholder="user name"
|
||||
type="text"
|
||||
onChange={this.onChangeAdminId}
|
||||
value={this.state.adminId}
|
||||
icon={<IconAngRight size={config.rootSize} />}
|
||||
/>
|
||||
<Input
|
||||
placeholder="password"
|
||||
type="password"
|
||||
onChange={this.onChangeAdminPwd}
|
||||
value={this.state.adminPwd}
|
||||
styleContainer={{ marginLeft: "0.5rem" }}
|
||||
icon={<IconAngRight size={config.rootSize} />}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
icon={<IconSignIn size={config.rootSize} />}
|
||||
label={"login"}
|
||||
styleLabel={styleButtonLabel}
|
||||
styleDefault={{ color: "#fff" }}
|
||||
styleContainer={{
|
||||
backgroundColor: "#2c3e50",
|
||||
marginLeft: "0.5rem"
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AuthPane.defaultProps = {
|
||||
onLogin: () => console.error("undefined"),
|
||||
onLogout: () => console.error("undefined"),
|
||||
compact: false,
|
||||
isLogin: false,
|
||||
serverAddr: "",
|
||||
styleContainer: {},
|
||||
styleStr: ""
|
||||
};
|
249
client/components/composite/file_box.jsx
Normal file
249
client/components/composite/file_box.jsx
Normal file
|
@ -0,0 +1,249 @@
|
|||
import React from "react";
|
||||
import { FileBoxDetail } from "./file_box_detail";
|
||||
import { Button } from "../control/button";
|
||||
|
||||
import { config } from "../../config";
|
||||
import { getIcon, getIconColor } from "../display/icon";
|
||||
import { getFileExt } from "../../libs/file_type";
|
||||
import { del, publishId, shadowId, setDownLimit } from "../../libs/api_share";
|
||||
|
||||
const msgUploadOk = "Uploading is stopped and file is deleted";
|
||||
const msgUploadNok = "Fail to delete file";
|
||||
|
||||
const styleLeft = {
|
||||
float: "left",
|
||||
padding: "1rem 0 1rem 1rem"
|
||||
};
|
||||
|
||||
const styleRight = {
|
||||
float: "right",
|
||||
textAlign: "right",
|
||||
padding: "0rem"
|
||||
};
|
||||
|
||||
const clear = <div style={{ clear: "both" }} />;
|
||||
|
||||
const iconDesStyle = {
|
||||
display: "inline-block",
|
||||
fontSize: "0.875rem",
|
||||
lineHeight: "1rem",
|
||||
marginBottom: "0.25rem",
|
||||
maxWidth: "12rem",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
textDecoration: "none",
|
||||
verticalAlign: "middle",
|
||||
whiteSpace: "nowrap"
|
||||
};
|
||||
|
||||
const descStyle = {
|
||||
fontSize: "0.75rem",
|
||||
padding: "0.75rem"
|
||||
};
|
||||
|
||||
const otherStyle = `
|
||||
.main-pane {
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
transition: background-color 0.1s;
|
||||
}
|
||||
.main-pane:hover {
|
||||
background-color: rgba(255, 255, 255, 0.85);
|
||||
transition: background-color 0.1s;
|
||||
}
|
||||
|
||||
.show-detail {
|
||||
opacity: 1;
|
||||
height: auto;
|
||||
transition: opacity 0.15s, height 0.5s;
|
||||
}
|
||||
|
||||
.hide-detail {
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
transition: opacity 0.15s, height 0.5s;
|
||||
}
|
||||
|
||||
.main-pane a {
|
||||
color: #333;
|
||||
transition: color 1s;
|
||||
}
|
||||
.main-pane a:hover {
|
||||
color: #3498db;
|
||||
transition: color 1s;
|
||||
}
|
||||
`;
|
||||
|
||||
const IconMore = getIcon("bars");
|
||||
const IconTimesCir = getIcon("timesCir");
|
||||
const styleIconTimesCir = {
|
||||
color: getIconColor("timesCir")
|
||||
};
|
||||
|
||||
const iconMoreStyleStr = `
|
||||
.file-box-more {
|
||||
color: #333;
|
||||
background-color: #fff;
|
||||
transition: color 0.4s, background-color 0.4s;
|
||||
}
|
||||
|
||||
.file-box-more:hover {
|
||||
color: #000;
|
||||
background-color: #ccc;
|
||||
transition: color 0.4s, background-color 0.4s;
|
||||
}
|
||||
`;
|
||||
|
||||
let styleFileBox = {
|
||||
textAlign: "left",
|
||||
margin: "1px 0px",
|
||||
fontSize: "0.75rem"
|
||||
};
|
||||
|
||||
const styleButtonContainer = {
|
||||
width: "1rem",
|
||||
height: "1rem",
|
||||
padding: "1.5rem 1rem"
|
||||
};
|
||||
|
||||
const styleButtonIcon = {
|
||||
lineHeight: "1rem",
|
||||
height: "1rem",
|
||||
margin: "0"
|
||||
};
|
||||
|
||||
export class FileBox extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
onToggleDetail = () => {
|
||||
this.props.onToggleDetail(this.props.id);
|
||||
};
|
||||
|
||||
onDelete = () => {
|
||||
del(this.props.id).then(ok => {
|
||||
if (ok) {
|
||||
this.props.onOk(msgUploadOk);
|
||||
this.props.onRefresh();
|
||||
} else {
|
||||
this.props.onError(msgUploadNok);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const ext = getFileExt(this.props.name);
|
||||
const IconFile = getIcon(ext);
|
||||
const IconSpinner = getIcon("spinner");
|
||||
|
||||
const styleIcon = {
|
||||
color: this.props.isLoading ? "#34495e" : getIconColor(ext)
|
||||
};
|
||||
|
||||
styleFileBox = {
|
||||
...styleFileBox,
|
||||
width: this.props.width
|
||||
};
|
||||
|
||||
const fileIcon = this.props.isLoading ? (
|
||||
<IconSpinner
|
||||
size={config.rootSize * 2}
|
||||
style={styleIcon}
|
||||
className="anm-rotate"
|
||||
/>
|
||||
) : (
|
||||
<IconFile size={config.rootSize * 2} style={styleIcon} />
|
||||
);
|
||||
|
||||
const opIcon = this.props.isLoading ? (
|
||||
<Button
|
||||
icon={<IconTimesCir size={config.rootSize} style={styleIconTimesCir} />}
|
||||
label=""
|
||||
styleContainer={styleButtonContainer}
|
||||
styleIcon={styleButtonIcon}
|
||||
onClick={this.onDelete}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
icon={<IconMore size={config.rootSize} />}
|
||||
className={"file-box-more"}
|
||||
label=""
|
||||
styleContainer={styleButtonContainer}
|
||||
styleIcon={styleButtonIcon}
|
||||
styleStr={iconMoreStyleStr}
|
||||
onClick={this.onToggleDetail}
|
||||
/>
|
||||
);
|
||||
|
||||
const downloadLink = (
|
||||
<a href={this.props.href} style={iconDesStyle} target="_blank">
|
||||
{this.props.name}
|
||||
</a>
|
||||
);
|
||||
|
||||
const classDetailPane =
|
||||
this.props.showDetailId === this.props.id &&
|
||||
this.props.uploadState === "done"
|
||||
? "show-detail"
|
||||
: "hide-detail";
|
||||
|
||||
return (
|
||||
<div key={this.props.id} style={styleFileBox} className="file-box">
|
||||
<div className="main-pane">
|
||||
<div style={styleLeft}>{fileIcon}</div>
|
||||
<div style={styleLeft}>
|
||||
{downloadLink}
|
||||
<div
|
||||
style={{
|
||||
color: "#999",
|
||||
lineHeight: "0.75rem",
|
||||
height: "0.75rem"
|
||||
}}
|
||||
>{`${this.props.size} ${this.props.modTime}`}</div>
|
||||
</div>
|
||||
<div style={styleRight}>{opIcon}</div>
|
||||
{clear}
|
||||
<style>{otherStyle}</style>
|
||||
</div>
|
||||
|
||||
<div style={{ position: "relative" }}>
|
||||
<FileBoxDetail
|
||||
id={this.props.id}
|
||||
name={this.props.name}
|
||||
size={this.props.size}
|
||||
modTime={this.props.modTime}
|
||||
href={this.props.href}
|
||||
downLimit={this.props.downLimit}
|
||||
width={this.props.width}
|
||||
className={classDetailPane}
|
||||
onRefresh={this.props.onRefresh}
|
||||
onError={this.props.onError}
|
||||
onOk={this.props.onOk}
|
||||
onDel={del}
|
||||
onPublishId={publishId}
|
||||
onShadowId={shadowId}
|
||||
onSetDownLimit={setDownLimit}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FileBox.defaultProps = {
|
||||
id: "",
|
||||
name: "",
|
||||
isLoading: false,
|
||||
modTime: "unknown",
|
||||
uploadState: "",
|
||||
href: "",
|
||||
width: "320px",
|
||||
showDetailId: "",
|
||||
downLimit: -3,
|
||||
size: "unknown",
|
||||
onToggleDetail: () => console.error("undefined"),
|
||||
onRefresh: () => console.error("undefined"),
|
||||
onError: () => console.error("undefined"),
|
||||
onOk: () => console.error("undefined")
|
||||
};
|
230
client/components/composite/file_box_detail.jsx
Normal file
230
client/components/composite/file_box_detail.jsx
Normal file
|
@ -0,0 +1,230 @@
|
|||
import React from "react";
|
||||
import { Button } from "../control/button";
|
||||
import { Input } from "../control/input";
|
||||
|
||||
export const classDelBtn = "file-box-pane-btn-del";
|
||||
export const classDelYes = "del-no";
|
||||
export const classDelNo = "del-yes";
|
||||
|
||||
let styleDetailPane = {
|
||||
color: "#666",
|
||||
backgroundColor: "#fff",
|
||||
position: "absolute",
|
||||
marginBottom: "5rem",
|
||||
zIndex: "10"
|
||||
};
|
||||
|
||||
const styleDetailContainer = {
|
||||
padding: "1em",
|
||||
borderBottom: "solid 1rem #ccc"
|
||||
};
|
||||
|
||||
const styleDetailHeader = {
|
||||
color: "#999",
|
||||
fontSize: "0.75rem",
|
||||
fontWeight: "bold",
|
||||
margin: "1.5rem 0 0.5rem 0",
|
||||
padding: 0,
|
||||
textTransform: "uppercase"
|
||||
};
|
||||
|
||||
const styleDesc = {
|
||||
overflow: "hidden",
|
||||
whiteSpace: "nowrap",
|
||||
textOverflow: "ellipsis",
|
||||
lineHeight: "1.5rem",
|
||||
fontSize: "0.875rem"
|
||||
};
|
||||
|
||||
export class FileBoxDetail extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
downLimit: this.props.downLimit,
|
||||
showDelComfirm: false
|
||||
};
|
||||
|
||||
styleDetailPane = {
|
||||
...styleDetailPane,
|
||||
width: this.props.width
|
||||
};
|
||||
}
|
||||
|
||||
onResetLink = () => {
|
||||
return this.props.onPublishId(this.props.id).then(resettedId => {
|
||||
if (resettedId == null) {
|
||||
this.props.onError("Resetting link failed");
|
||||
} else {
|
||||
this.props.onOk("Link is reset");
|
||||
this.props.onRefresh();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onShadowLink = () => {
|
||||
return this.props.onShadowId(this.props.id).then(shadowId => {
|
||||
if (shadowId == null) {
|
||||
this.props.onError("Shadowing link failed");
|
||||
} else {
|
||||
this.props.onOk("Link is shadowed");
|
||||
this.props.onRefresh();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onSetDownLimit = newValue => {
|
||||
this.setState({ downLimit: newValue });
|
||||
};
|
||||
|
||||
onComfirmDel = () => {
|
||||
this.setState({ showDelComfirm: true });
|
||||
};
|
||||
|
||||
onCancelDel = () => {
|
||||
this.setState({ showDelComfirm: false });
|
||||
};
|
||||
|
||||
onUpdateDownLimit = () => {
|
||||
return this.props
|
||||
.onSetDownLimit(this.props.id, this.state.downLimit)
|
||||
.then(ok => {
|
||||
if (ok) {
|
||||
this.props.onOk("Download limit updated");
|
||||
this.props.onRefresh();
|
||||
} else {
|
||||
this.props.onError("Setting download limit failed");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onDelete = () => {
|
||||
return this.props.onDel(this.props.id).then(ok => {
|
||||
if (ok) {
|
||||
this.props.onOk("File deleted");
|
||||
this.props.onRefresh();
|
||||
} else {
|
||||
this.props.onError("Fail to delete file");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const delComfirmButtons = (
|
||||
<div>
|
||||
<Button
|
||||
className={classDelYes}
|
||||
label={"DON'T delete"}
|
||||
styleContainer={{ backgroundColor: "#2c3e50", marginTop: "0.25rem" }}
|
||||
styleDefault={{ color: "#fff" }}
|
||||
onClick={this.onCancelDel}
|
||||
/>
|
||||
<Button
|
||||
className={classDelNo}
|
||||
label={"DELETE it"}
|
||||
styleContainer={{ backgroundColor: "#e74c3c", marginTop: "0.25rem" }}
|
||||
styleDefault={{ color: "#fff" }}
|
||||
onClick={this.onDelete}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={styleDetailPane} className={this.props.className}>
|
||||
<div style={styleDetailContainer}>
|
||||
<div>
|
||||
<h4 style={styleDetailHeader}>File Information</h4>
|
||||
<div>
|
||||
<div style={styleDesc}>
|
||||
<b>Name</b> {this.props.name}
|
||||
</div>
|
||||
<div style={styleDesc}>
|
||||
<b>Size</b> {this.props.size}
|
||||
</div>
|
||||
<div style={styleDesc}>
|
||||
<b>Time</b> {this.props.modTime}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4 style={styleDetailHeader}>Download Link</h4>
|
||||
<Input
|
||||
type="text"
|
||||
value={`${window.location.protocol}//${window.location.host}${
|
||||
this.props.href
|
||||
}`}
|
||||
style={{ marginBottom: "0.5rem" }}
|
||||
/>
|
||||
{/* <Button label={"Copy"} onClick={this.onCopyLink} /> */}
|
||||
<br />
|
||||
<Button
|
||||
label={"Reset"}
|
||||
onClick={this.onResetLink}
|
||||
styleContainer={{
|
||||
backgroundColor: "#ccc",
|
||||
marginRight: "0.5rem",
|
||||
marginTop: "0.25rem"
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
label={"Regenerate"}
|
||||
onClick={this.onShadowLink}
|
||||
styleContainer={{ backgroundColor: "#ccc", marginTop: "0.25rem" }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h4 style={styleDetailHeader}>
|
||||
Download Limit (-1 means unlimited)
|
||||
</h4>
|
||||
<Input
|
||||
type="text"
|
||||
value={this.state.downLimit}
|
||||
onChange={this.onSetDownLimit}
|
||||
style={{ marginBottom: "0.5rem" }}
|
||||
/>
|
||||
<br />
|
||||
<Button
|
||||
label={"Update"}
|
||||
styleContainer={{ backgroundColor: "#ccc", marginTop: "0.25rem" }}
|
||||
onClick={this.onUpdateDownLimit}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h4 style={styleDetailHeader}>Delete</h4>
|
||||
{this.state.showDelComfirm ? (
|
||||
delComfirmButtons
|
||||
) : (
|
||||
<Button
|
||||
className={classDelBtn}
|
||||
label={"Delete"}
|
||||
styleContainer={{
|
||||
backgroundColor: "#e74c3c",
|
||||
marginTop: "0.25rem"
|
||||
}}
|
||||
styleDefault={{ color: "#fff" }}
|
||||
onClick={this.onComfirmDel}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FileBoxDetail.defaultProps = {
|
||||
id: "n/a",
|
||||
name: "n/a",
|
||||
size: "n/a",
|
||||
modTime: 0,
|
||||
href: "n/a",
|
||||
downLimit: -3,
|
||||
width: -1,
|
||||
className: "",
|
||||
onRefresh: () => console.error("undefined"),
|
||||
onError: () => console.error("undefined"),
|
||||
onOk: () => console.error("undefined"),
|
||||
onDel: () => console.error("undefined"),
|
||||
onPublishId: () => console.error("undefined"),
|
||||
onShadowId: () => console.error("undefined"),
|
||||
onSetDownLimit: () => console.error("undefined")
|
||||
};
|
154
client/components/composite/file_pane.jsx
Normal file
154
client/components/composite/file_pane.jsx
Normal file
|
@ -0,0 +1,154 @@
|
|||
import axios from "axios";
|
||||
import byteSize from "byte-size";
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import throttle from "lodash.throttle";
|
||||
import { Grids } from "../../components/layout/grids";
|
||||
import { Uploader } from "../../components/composite/uploader";
|
||||
import { FileBox } from "./file_box";
|
||||
import { TimeGrids } from "./time_grids";
|
||||
|
||||
import { config } from "../../config";
|
||||
|
||||
const msgSynced = "Synced";
|
||||
const msgSyncFailed = "Syncing failed";
|
||||
const interval = 250;
|
||||
|
||||
export class FilePane extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
infos: [],
|
||||
showDetailId: -1
|
||||
};
|
||||
this.onRefresh = throttle(this.onRefreshImp, interval);
|
||||
this.onUpdateProgress = throttle(this.onUpdateProgressImp, interval);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
return this.onRefreshImp();
|
||||
}
|
||||
|
||||
onRefreshImp = () => {
|
||||
return this.props
|
||||
.onList()
|
||||
.then(infos => {
|
||||
if (infos != null) {
|
||||
this.setState({ infos });
|
||||
this.props.onOk(msgSynced);
|
||||
} else {
|
||||
this.props.onError(msgSyncFailed);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
this.props.onError(msgSyncFailed);
|
||||
});
|
||||
};
|
||||
|
||||
onUpdateProgressImp = (shareId, progress) => {
|
||||
const updatedInfos = this.state.infos.map(shareInfo => {
|
||||
return shareInfo.Id === shareId ? { ...shareInfo, progress } : shareInfo;
|
||||
});
|
||||
|
||||
this.setState({ infos: updatedInfos });
|
||||
};
|
||||
|
||||
onToggleDetail = id => {
|
||||
this.setState({
|
||||
showDetailId: this.state.showDetailId === id ? -1 : id
|
||||
});
|
||||
};
|
||||
|
||||
getByteSize = size => {
|
||||
const sizeObj = byteSize(size);
|
||||
return `${sizeObj.value} ${sizeObj.unit}`;
|
||||
};
|
||||
|
||||
getInfos = filterName => {
|
||||
const filteredInfos = this.state.infos.filter(shareInfo => {
|
||||
return shareInfo.PathLocal.includes(filterName);
|
||||
});
|
||||
|
||||
return filteredInfos.map(shareInfo => {
|
||||
const isLoading = shareInfo.State === "uploading";
|
||||
const timestamp = shareInfo.ModTime / 1000000;
|
||||
const modTime = new Date(timestamp).toLocaleString();
|
||||
const href = `${config.serverAddr}/download?shareid=${shareInfo.Id}`;
|
||||
const progress = isNaN(shareInfo.progress) ? 0 : shareInfo.progress;
|
||||
const name = isLoading
|
||||
? `${Math.floor(progress * 100)}% ${shareInfo.PathLocal}`
|
||||
: shareInfo.PathLocal;
|
||||
|
||||
return {
|
||||
key: shareInfo.Id,
|
||||
timestamp,
|
||||
component: (
|
||||
<FileBox
|
||||
key={shareInfo.Id}
|
||||
id={shareInfo.Id}
|
||||
name={name}
|
||||
size={this.getByteSize(shareInfo.Uploaded)}
|
||||
uploadState={shareInfo.State}
|
||||
isLoading={isLoading}
|
||||
modTime={modTime}
|
||||
href={href}
|
||||
downLimit={shareInfo.DownLimit}
|
||||
width={`${this.props.colWidth}rem`}
|
||||
onRefresh={this.onRefresh}
|
||||
onOk={this.props.onOk}
|
||||
onError={this.props.onError}
|
||||
showDetailId={this.state.showDetailId}
|
||||
onToggleDetail={this.onToggleDetail}
|
||||
/>
|
||||
)
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const styleUploaderContainer = {
|
||||
width: `${this.props.colWidth}rem`,
|
||||
margin: "auto"
|
||||
};
|
||||
|
||||
const containerStyle = {
|
||||
width: this.props.width,
|
||||
margin: "auto",
|
||||
marginTop: "0",
|
||||
marginBottom: "10rem"
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="file-pane" style={containerStyle}>
|
||||
<TimeGrids
|
||||
items={this.getInfos(this.props.filterName)}
|
||||
styleContainer={{
|
||||
width:
|
||||
this.props.width === "auto"
|
||||
? config.rootSize * config.colWidth
|
||||
: this.props.width,
|
||||
margin: "auto"
|
||||
}}
|
||||
/>
|
||||
<div style={styleUploaderContainer}>
|
||||
<Uploader
|
||||
onRefresh={this.onRefresh}
|
||||
onUpdateProgress={this.onUpdateProgress}
|
||||
onOk={this.props.onOk}
|
||||
onError={this.props.onError}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FilePane.defaultProps = {
|
||||
width: "100%",
|
||||
colWidth: 20,
|
||||
filterName: "",
|
||||
onList: () => console.error("undefined"),
|
||||
onOk: () => console.error("undefined"),
|
||||
onError: () => console.error("undefined")
|
||||
};
|
250
client/components/composite/info_bar.jsx
Normal file
250
client/components/composite/info_bar.jsx
Normal file
|
@ -0,0 +1,250 @@
|
|||
import React from "react";
|
||||
|
||||
import { Button } from "../control/button";
|
||||
import { Input } from "../control/input";
|
||||
import { getIcon, getIconColor } from "../display/icon";
|
||||
import { AuthPane } from "./auth_pane";
|
||||
import { rootSize } from "../../config";
|
||||
|
||||
let styleInfoBar = {
|
||||
textAlign: "left",
|
||||
color: "#999",
|
||||
marginBottom: "1rem",
|
||||
margin: "auto"
|
||||
};
|
||||
|
||||
const styleContainer = {
|
||||
padding: "0.5rem",
|
||||
backgroundColor: "rgba(255, 255, 255, 0.5)"
|
||||
};
|
||||
|
||||
const styleLeft = {
|
||||
float: "left",
|
||||
width: "50%",
|
||||
heigth: "2rem"
|
||||
};
|
||||
|
||||
const styleRight = {
|
||||
float: "right",
|
||||
width: "50%",
|
||||
textAlign: "right",
|
||||
heigth: "2rem"
|
||||
};
|
||||
|
||||
const styleButtonLabel = {
|
||||
verticalAlign: "middle"
|
||||
};
|
||||
|
||||
const IconPlusCir = getIcon("pluscir");
|
||||
const IconSearch = getIcon("search");
|
||||
const clear = <div style={{ clear: "both" }} />;
|
||||
|
||||
export class InfoBar extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
filterFileName: "",
|
||||
fold: this.props.compact
|
||||
};
|
||||
}
|
||||
|
||||
onLogin = (serverAddr, adminId, adminPwd) => {
|
||||
this.props.onLogin(serverAddr, adminId, adminPwd);
|
||||
};
|
||||
|
||||
onLogout = serverAddr => {
|
||||
this.props.onLogout(serverAddr);
|
||||
};
|
||||
|
||||
onSearch = value => {
|
||||
// TODO: need debounce
|
||||
this.props.onSearch(value);
|
||||
this.setState({ filterFileName: value });
|
||||
};
|
||||
|
||||
onAddLocalFiles = () => {
|
||||
return this.props.onAddLocalFiles().then(ok => {
|
||||
if (ok) {
|
||||
// TODO: need to add refresh
|
||||
this.props.onOk("Local files are added, please refresh.");
|
||||
} else {
|
||||
this.props.onError("Fail to add local files");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onToggle = () => {
|
||||
this.setState({ fold: !this.state.fold });
|
||||
};
|
||||
|
||||
render() {
|
||||
styleInfoBar = { ...styleInfoBar, width: this.props.width };
|
||||
|
||||
if (this.props.compact) {
|
||||
const IconMore = getIcon("bars");
|
||||
|
||||
const menuIcon = (
|
||||
<div style={{ backgroundColor: "rgba(255, 255, 255, 0.5)" }}>
|
||||
<div>
|
||||
<div style={{ float: "right" }}>
|
||||
<Button
|
||||
onClick={this.onToggle}
|
||||
label={""}
|
||||
// styleLabel={styleButtonLabel}
|
||||
styleContainer={{
|
||||
height: "2.5rem",
|
||||
width: "2.5rem",
|
||||
backgroundColor: "rgba(255, 255, 255, 0.2)",
|
||||
margin: "0.5rem"
|
||||
}}
|
||||
styleDefault={{ height: "auto" }}
|
||||
styleIcon={{
|
||||
lineHeight: "1rem",
|
||||
height: "1rem",
|
||||
margin: "0.75rem",
|
||||
display: "inline-block"
|
||||
}}
|
||||
icon={<IconMore size={rootSize} style={{ color: "#fff" }} />}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ float: "right" }}>
|
||||
<Input
|
||||
onChange={this.onSearch}
|
||||
placeholder="Search..."
|
||||
type="text"
|
||||
value={this.state.filterFileName}
|
||||
styleContainer={{
|
||||
backgroundColor: "rgba(255, 255, 255, 0.5)",
|
||||
margin: "0.5rem",
|
||||
textAlign: "left"
|
||||
}}
|
||||
icon={<IconSearch size={16} />}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ clear: "both" }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const menuList = !this.state.fold ? (
|
||||
<div style={styleContainer}>
|
||||
<div>
|
||||
<AuthPane
|
||||
onLogin={this.onLogin}
|
||||
onLogout={this.onLogout}
|
||||
isLogin={this.props.isLogin}
|
||||
serverAddr={this.props.serverAddr}
|
||||
compact={this.props.compact}
|
||||
/>
|
||||
<Button
|
||||
onClick={this.onAddLocalFiles}
|
||||
label={"Scan Files"}
|
||||
styleLabel={styleButtonLabel}
|
||||
styleContainer={{
|
||||
backgroundColor: "#2ecc71",
|
||||
marginLeft: "0.5rem"
|
||||
}}
|
||||
styleDefault={{ color: "#fff" }}
|
||||
icon={<IconPlusCir size={16} style={{ color: "#fff" }} />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<span />
|
||||
);
|
||||
|
||||
const menu = (
|
||||
<div>
|
||||
{menuIcon}
|
||||
{menuList}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className="info-bar"
|
||||
style={{ ...styleInfoBar, textAlign: "right" }}
|
||||
>
|
||||
{this.props.isLogin ? (
|
||||
menu
|
||||
) : (
|
||||
<AuthPane
|
||||
onLogin={this.onLogin}
|
||||
onLogout={this.onLogout}
|
||||
isLogin={this.props.isLogin}
|
||||
serverAddr={this.props.serverAddr}
|
||||
styleContainer={{ textAlign: "left" }}
|
||||
compact={this.props.compact}
|
||||
/>
|
||||
)}
|
||||
<div>{this.props.children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const visitorPane = (
|
||||
<AuthPane
|
||||
onLogin={this.onLogin}
|
||||
onLogout={this.onLogout}
|
||||
isLogin={this.props.isLogin}
|
||||
serverAddr={this.props.serverAddr}
|
||||
compact={this.props.compact}
|
||||
/>
|
||||
);
|
||||
|
||||
const memberPane = (
|
||||
<div>
|
||||
<div style={styleLeft}>
|
||||
<AuthPane
|
||||
onLogin={this.onLogin}
|
||||
onLogout={this.onLogout}
|
||||
isLogin={this.props.isLogin}
|
||||
serverAddr={this.props.serverAddr}
|
||||
/>
|
||||
<Button
|
||||
onClick={this.onAddLocalFiles}
|
||||
label={"Scan Files"}
|
||||
styleLabel={styleButtonLabel}
|
||||
styleContainer={{
|
||||
backgroundColor: "#2ecc71",
|
||||
marginLeft: "0.5rem"
|
||||
}}
|
||||
styleDefault={{ color: "#fff" }}
|
||||
icon={<IconPlusCir size={16} style={{ color: "#fff" }} />}
|
||||
/>
|
||||
</div>
|
||||
<div style={styleRight}>
|
||||
<Input
|
||||
onChange={this.onSearch}
|
||||
placeholder="Search..."
|
||||
type="text"
|
||||
value={this.state.filterFileName}
|
||||
styleContainer={{ backgroundColor: "rgba(255, 255, 255, 0.5)" }}
|
||||
icon={<IconSearch size={16} />}
|
||||
/>
|
||||
</div>
|
||||
{clear}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="info-bar" style={styleInfoBar}>
|
||||
<div style={styleContainer}>
|
||||
{this.props.isLogin ? memberPane : visitorPane}
|
||||
</div>
|
||||
<div>{this.props.children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
InfoBar.defaultProps = {
|
||||
compact: false,
|
||||
width: "-1",
|
||||
isLogin: false,
|
||||
serverAddr: "",
|
||||
onLogin: () => console.error("undefined"),
|
||||
onLogout: () => console.error("undefined"),
|
||||
onAddLocalFiles: () => console.error("undefined"),
|
||||
onSearch: () => console.error("undefined"),
|
||||
onOk: () => console.error("undefined"),
|
||||
onError: () => console.error("undefined")
|
||||
};
|
214
client/components/composite/log.jsx
Normal file
214
client/components/composite/log.jsx
Normal file
|
@ -0,0 +1,214 @@
|
|||
import React from "react";
|
||||
import { getIcon, getIconColor } from "../display/icon";
|
||||
|
||||
const statusNull = "null";
|
||||
const statusInfo = "info";
|
||||
const statusWarn = "warn";
|
||||
const statusError = "error";
|
||||
const statusOk = "ok";
|
||||
const statusStart = "start";
|
||||
const statusEnd = "end";
|
||||
|
||||
const IconInfo = getIcon("infoCir");
|
||||
const IconWarn = getIcon("exTri");
|
||||
const IconError = getIcon("timesCir");
|
||||
const IconOk = getIcon("checkCir");
|
||||
const IconStart = getIcon("refresh");
|
||||
|
||||
const colorInfo = getIconColor("infoCir");
|
||||
const colorWarn = getIconColor("exTri");
|
||||
const colorError = getIconColor("timesCir");
|
||||
const colorOk = getIconColor("checkCir");
|
||||
const colorStart = getIconColor("refresh");
|
||||
|
||||
const classFadeIn = "log-fade-in";
|
||||
const classHidden = "log-hidden";
|
||||
const styleStr = `
|
||||
.log .${classFadeIn} {
|
||||
opacity: 1;
|
||||
margin-left: 0.5rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
transition: opacity 0.3s, margin-left 0.3s, padding 0.3s;
|
||||
}
|
||||
|
||||
.log .${classHidden} {
|
||||
opacity: 0;
|
||||
margin-left: 0rem;
|
||||
padding: 0;
|
||||
transition: opacity 0.3s, margin-left 0.3s, padding 0.3s;
|
||||
}
|
||||
|
||||
.log a {
|
||||
color: #2980b9;
|
||||
transition: color 0.3s;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.log a:hover {
|
||||
color: #3498db;
|
||||
transition: color 0.3s;
|
||||
text-decoration: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const wait = 5000;
|
||||
const logSlotLen = 2;
|
||||
const getEmptyLog = () => ({
|
||||
className: classHidden,
|
||||
msg: "",
|
||||
status: statusNull
|
||||
});
|
||||
|
||||
const getLogIcon = status => {
|
||||
switch (status) {
|
||||
case statusInfo:
|
||||
return (
|
||||
<IconInfo
|
||||
size={16}
|
||||
style={{ marginRight: "0.25rem", color: colorInfo }}
|
||||
/>
|
||||
);
|
||||
case statusWarn:
|
||||
return (
|
||||
<IconWarn
|
||||
size={16}
|
||||
style={{ marginRight: "0.25rem", color: colorWarn }}
|
||||
/>
|
||||
);
|
||||
case statusError:
|
||||
return (
|
||||
<IconError
|
||||
size={16}
|
||||
style={{ marginRight: "0.25rem", color: colorError }}
|
||||
/>
|
||||
);
|
||||
case statusOk:
|
||||
return (
|
||||
<IconOk size={16} style={{ marginRight: "0.25rem", color: colorOk }} />
|
||||
);
|
||||
case statusStart:
|
||||
return (
|
||||
<IconStart
|
||||
size={16}
|
||||
className={"anm-rotate"}
|
||||
style={{ marginRight: "0.25rem", color: colorStart }}
|
||||
/>
|
||||
);
|
||||
case statusEnd:
|
||||
return (
|
||||
<IconOk size={16} style={{ marginRight: "0.25rem", color: colorOk }} />
|
||||
);
|
||||
default:
|
||||
return <span />;
|
||||
}
|
||||
};
|
||||
|
||||
export class Log extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
logs: Array(logSlotLen).fill(getEmptyLog())
|
||||
};
|
||||
this.id = 0;
|
||||
}
|
||||
|
||||
genId = () => {
|
||||
return this.id++ % logSlotLen;
|
||||
};
|
||||
|
||||
addLog = (status, msg) => {
|
||||
const id = this.genId();
|
||||
const nextLogs = [
|
||||
...this.state.logs.slice(0, id),
|
||||
{
|
||||
className: classFadeIn,
|
||||
msg,
|
||||
status
|
||||
},
|
||||
...this.state.logs.slice(id + 1)
|
||||
];
|
||||
|
||||
this.setState({ logs: nextLogs });
|
||||
this.delayClearLog(id);
|
||||
return id;
|
||||
};
|
||||
|
||||
delayClearLog = idToDel => {
|
||||
setTimeout(this.clearLog, wait, idToDel);
|
||||
};
|
||||
|
||||
clearLog = idToDel => {
|
||||
// TODO: there may be race condition here
|
||||
const nextLogs = [
|
||||
...this.state.logs.slice(0, idToDel),
|
||||
getEmptyLog(),
|
||||
...this.state.logs.slice(idToDel + 1)
|
||||
];
|
||||
this.setState({ logs: nextLogs });
|
||||
};
|
||||
|
||||
info = msg => {
|
||||
this.addLog(statusInfo, msg);
|
||||
};
|
||||
|
||||
warn = msg => {
|
||||
this.addLog(statusWarn, msg);
|
||||
};
|
||||
|
||||
error = msg => {
|
||||
this.addLog(statusError, msg);
|
||||
};
|
||||
|
||||
ok = msg => {
|
||||
this.addLog(statusOk, msg);
|
||||
};
|
||||
|
||||
start = msg => {
|
||||
const id = this.genId();
|
||||
const nextLogs = [
|
||||
...this.state.logs.slice(0, id),
|
||||
{
|
||||
className: classFadeIn,
|
||||
msg,
|
||||
status: statusStart
|
||||
},
|
||||
...this.state.logs.slice(id + 1)
|
||||
];
|
||||
|
||||
this.setState({ logs: nextLogs });
|
||||
return id;
|
||||
};
|
||||
|
||||
end = (startId, msg) => {
|
||||
// remove start log
|
||||
this.clearLog(startId);
|
||||
this.addLog(statusEnd, msg);
|
||||
};
|
||||
|
||||
render() {
|
||||
const logList = Object.keys(this.state.logs).map(logId => {
|
||||
return (
|
||||
<span
|
||||
key={logId}
|
||||
style={this.props.styleLog}
|
||||
className={this.state.logs[logId].className}
|
||||
>
|
||||
{getLogIcon(this.state.logs[logId].status)}
|
||||
{this.state.logs[logId].msg}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<span className={"log"} style={this.props.style}>
|
||||
{logList}
|
||||
<style>{styleStr}</style>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Log.defaultProps = {
|
||||
style: {},
|
||||
styleLog: {}
|
||||
};
|
32
client/components/composite/tests/auth_pane.test.jsx
Normal file
32
client/components/composite/tests/auth_pane.test.jsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
import React from "react";
|
||||
import { AuthPane, classLogin, classLogout } from "../auth_pane";
|
||||
|
||||
describe("AuthPane", () => {
|
||||
test("AuthPane should show login pane if isLogin === true, or show logout pane", () => {
|
||||
const tests = [
|
||||
{
|
||||
input: {
|
||||
onLogin: jest.fn,
|
||||
onLogout: jest.fn,
|
||||
isLogin: false,
|
||||
serverAddr: ""
|
||||
},
|
||||
output: classLogin
|
||||
},
|
||||
{
|
||||
input: {
|
||||
onLogin: jest.fn,
|
||||
onLogout: jest.fn,
|
||||
isLogin: true,
|
||||
serverAddr: ""
|
||||
},
|
||||
output: classLogout
|
||||
}
|
||||
];
|
||||
|
||||
tests.forEach(testCase => {
|
||||
const pane = new AuthPane(testCase.input);
|
||||
expect(pane.render().props.className).toBe(testCase.output);
|
||||
});
|
||||
});
|
||||
});
|
297
client/components/composite/tests/file_box_detail.test.jsx
Normal file
297
client/components/composite/tests/file_box_detail.test.jsx
Normal file
|
@ -0,0 +1,297 @@
|
|||
jest.mock("../../../libs/api_share");
|
||||
import React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import { FileBoxDetail, classDelYes, classDelNo } from "../file_box_detail";
|
||||
import { execFuncs, getDesc, verifyCalls } from "../../../tests/test_helper";
|
||||
import valueEqual from "value-equal";
|
||||
import {
|
||||
del,
|
||||
publishId,
|
||||
shadowId,
|
||||
setDownLimit
|
||||
} from "../../../libs/api_share";
|
||||
|
||||
describe("FileBoxDetail", () => {
|
||||
test("FileBoxDetail should show delete button by default, toggle using onComfirmDel and onCancelDel", () => {
|
||||
const box = mount(<FileBoxDetail />);
|
||||
expect(box.instance().state.showDelComfirm).toBe(false);
|
||||
box.instance().onComfirmDel();
|
||||
expect(box.instance().state.showDelComfirm).toBe(true);
|
||||
box.instance().onCancelDel();
|
||||
expect(box.instance().state.showDelComfirm).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("FileBoxDetail", () => {
|
||||
const tests = [
|
||||
{
|
||||
init: {
|
||||
id: "0",
|
||||
name: "filename",
|
||||
size: "1B",
|
||||
modTime: 0,
|
||||
href: "href",
|
||||
downLimit: -1
|
||||
},
|
||||
execs: [
|
||||
{
|
||||
func: "onSetDownLimit",
|
||||
args: [3]
|
||||
}
|
||||
],
|
||||
state: {
|
||||
downLimit: 3,
|
||||
showDelComfirm: false
|
||||
}
|
||||
},
|
||||
{
|
||||
init: {
|
||||
id: "0",
|
||||
name: "filename",
|
||||
size: "1B",
|
||||
modTime: 0,
|
||||
href: "href",
|
||||
downLimit: -1
|
||||
},
|
||||
execs: [
|
||||
{
|
||||
func: "onComfirmDel",
|
||||
args: []
|
||||
}
|
||||
],
|
||||
state: {
|
||||
downLimit: -1,
|
||||
showDelComfirm: true
|
||||
}
|
||||
},
|
||||
{
|
||||
init: {
|
||||
id: "0",
|
||||
name: "filename",
|
||||
size: "1B",
|
||||
modTime: 0,
|
||||
href: "href",
|
||||
downLimit: -1
|
||||
},
|
||||
execs: [
|
||||
{
|
||||
func: "onComfirmDel",
|
||||
args: []
|
||||
},
|
||||
{
|
||||
func: "onCancelDel",
|
||||
args: []
|
||||
}
|
||||
],
|
||||
state: {
|
||||
downLimit: -1,
|
||||
showDelComfirm: false
|
||||
}
|
||||
},
|
||||
{
|
||||
init: {
|
||||
id: "0",
|
||||
name: "filename",
|
||||
size: "1B",
|
||||
modTime: 0,
|
||||
href: "href",
|
||||
downLimit: -1
|
||||
},
|
||||
execs: [
|
||||
{
|
||||
func: "onResetLink",
|
||||
args: []
|
||||
}
|
||||
],
|
||||
state: {
|
||||
downLimit: -1,
|
||||
showDelComfirm: false
|
||||
},
|
||||
calls: [
|
||||
{
|
||||
func: "onPublishId",
|
||||
count: 1
|
||||
},
|
||||
{
|
||||
func: "onOk",
|
||||
count: 1
|
||||
},
|
||||
{
|
||||
func: "onRefresh",
|
||||
count: 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
init: {
|
||||
id: "0",
|
||||
name: "filename",
|
||||
size: "1B",
|
||||
modTime: 0,
|
||||
href: "href",
|
||||
downLimit: -1
|
||||
},
|
||||
execs: [
|
||||
{
|
||||
func: "onShadowLink",
|
||||
args: []
|
||||
}
|
||||
],
|
||||
state: {
|
||||
downLimit: -1,
|
||||
showDelComfirm: false
|
||||
},
|
||||
calls: [
|
||||
{
|
||||
func: "onShadowId",
|
||||
count: 1
|
||||
},
|
||||
{
|
||||
func: "onOk",
|
||||
count: 1
|
||||
},
|
||||
{
|
||||
func: "onRefresh",
|
||||
count: 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
init: {
|
||||
id: "0",
|
||||
name: "filename",
|
||||
size: "1B",
|
||||
modTime: 0,
|
||||
href: "href",
|
||||
downLimit: -1
|
||||
},
|
||||
execs: [
|
||||
{
|
||||
func: "onUpdateDownLimit",
|
||||
args: []
|
||||
}
|
||||
],
|
||||
state: {
|
||||
downLimit: -1,
|
||||
showDelComfirm: false
|
||||
},
|
||||
calls: [
|
||||
{
|
||||
func: "onSetDownLimit",
|
||||
count: 1
|
||||
},
|
||||
{
|
||||
func: "onOk",
|
||||
count: 1
|
||||
},
|
||||
{
|
||||
func: "onRefresh",
|
||||
count: 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
init: {
|
||||
id: "0",
|
||||
name: "filename",
|
||||
size: "1B",
|
||||
modTime: 0,
|
||||
href: "href",
|
||||
downLimit: -1
|
||||
},
|
||||
execs: [
|
||||
{
|
||||
func: "onDelete",
|
||||
args: []
|
||||
}
|
||||
],
|
||||
state: {
|
||||
downLimit: -1,
|
||||
showDelComfirm: false
|
||||
},
|
||||
calls: [
|
||||
{
|
||||
func: "onDel",
|
||||
count: 1
|
||||
},
|
||||
{
|
||||
func: "onOk",
|
||||
count: 1
|
||||
},
|
||||
{
|
||||
func: "onRefresh",
|
||||
count: 1
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
tests.forEach(testCase => {
|
||||
test(getDesc("FileBoxDetail", testCase), () => {
|
||||
const stubs = {
|
||||
onOk: jest.fn(),
|
||||
onError: jest.fn(),
|
||||
onRefresh: jest.fn(),
|
||||
onDel: jest.fn(),
|
||||
onPublishId: jest.fn(),
|
||||
onShadowId: jest.fn(),
|
||||
onSetDownLimit: jest.fn()
|
||||
};
|
||||
|
||||
const stubWraps = {
|
||||
onDel: () => {
|
||||
stubs.onDel();
|
||||
return del();
|
||||
},
|
||||
onPublishId: () => {
|
||||
stubs.onPublishId();
|
||||
return publishId();
|
||||
},
|
||||
onShadowId: () => {
|
||||
stubs.onShadowId();
|
||||
return shadowId();
|
||||
},
|
||||
onSetDownLimit: () => {
|
||||
stubs.onSetDownLimit();
|
||||
return setDownLimit();
|
||||
}
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const pane = mount(
|
||||
<FileBoxDetail
|
||||
id={testCase.init.id}
|
||||
name={testCase.init.name}
|
||||
size={testCase.init.size}
|
||||
modTime={testCase.init.modTime}
|
||||
href={testCase.init.href}
|
||||
downLimit={testCase.init.downLimit}
|
||||
onRefresh={stubs.onRefresh}
|
||||
onOk={stubs.onOk}
|
||||
onError={stubs.onError}
|
||||
onDel={stubWraps.onDel}
|
||||
onPublishId={stubWraps.onPublishId}
|
||||
onShadowId={stubWraps.onShadowId}
|
||||
onSetDownLimit={stubWraps.onSetDownLimit}
|
||||
/>
|
||||
);
|
||||
|
||||
execFuncs(pane.instance(), testCase.execs).then(() => {
|
||||
pane.update();
|
||||
if (!valueEqual(pane.instance().state, testCase.state)) {
|
||||
return reject("FileBoxDetail: state not identical");
|
||||
}
|
||||
|
||||
if (testCase.calls != null) {
|
||||
const err = verifyCalls(testCase.calls, stubs);
|
||||
if (err != null) {
|
||||
return reject("FileBoxDetail: state not identical");
|
||||
}
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
144
client/components/composite/tests/file_pane.test.jsx
Normal file
144
client/components/composite/tests/file_pane.test.jsx
Normal file
|
@ -0,0 +1,144 @@
|
|||
jest.mock("../../../libs/api_share");
|
||||
import React from "react";
|
||||
import { FilePane } from "../file_pane";
|
||||
import { mount } from "enzyme";
|
||||
import * as mockApiShare from "../../../libs/api_share";
|
||||
import { execFuncs, getDesc, verifyCalls } from "../../../tests/test_helper";
|
||||
import valueEqual from "value-equal";
|
||||
|
||||
describe("FilePane", () => {
|
||||
const tests = [
|
||||
{
|
||||
init: {
|
||||
list: [{ Id: 0, PathLocal: "" }]
|
||||
},
|
||||
execs: [
|
||||
{
|
||||
func: "componentWillMount",
|
||||
args: []
|
||||
}
|
||||
],
|
||||
state: {
|
||||
infos: [{ Id: 0, PathLocal: "" }],
|
||||
showDetailId: -1
|
||||
},
|
||||
calls: [
|
||||
{
|
||||
func: "onList",
|
||||
count: 2 // because componentWillMount will be callled twice
|
||||
},
|
||||
{
|
||||
func: "onOk",
|
||||
count: 2 // because componentWillMount will be callled twice
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
init: {
|
||||
list: [{ Id: 0, PathLocal: "" }, { Id: 1, PathLocal: "" }]
|
||||
},
|
||||
execs: [
|
||||
{
|
||||
func: "componentWillMount",
|
||||
args: []
|
||||
},
|
||||
{
|
||||
func: "onUpdateProgressImp",
|
||||
args: [0, "100%"]
|
||||
}
|
||||
],
|
||||
state: {
|
||||
infos: [
|
||||
{ Id: 0, PathLocal: "", progress: "100%" },
|
||||
{ Id: 1, PathLocal: "" }
|
||||
],
|
||||
showDetailId: -1
|
||||
}
|
||||
},
|
||||
{
|
||||
init: {
|
||||
list: []
|
||||
},
|
||||
execs: [
|
||||
{
|
||||
func: "componentWillMount",
|
||||
args: []
|
||||
},
|
||||
{
|
||||
func: "onToggleDetail",
|
||||
args: [0]
|
||||
}
|
||||
],
|
||||
state: {
|
||||
infos: [],
|
||||
showDetailId: 0
|
||||
}
|
||||
},
|
||||
{
|
||||
init: {
|
||||
list: []
|
||||
},
|
||||
execs: [
|
||||
{
|
||||
func: "onToggleDetail",
|
||||
args: [0]
|
||||
},
|
||||
{
|
||||
func: "onToggleDetail",
|
||||
args: [0]
|
||||
}
|
||||
],
|
||||
state: {
|
||||
infos: [],
|
||||
showDetailId: -1
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
tests.forEach(testCase => {
|
||||
test(getDesc("FilePane", testCase), () => {
|
||||
// mock list()
|
||||
mockApiShare.__truncInfos();
|
||||
mockApiShare.__addInfos(testCase.init.list);
|
||||
|
||||
const stubs = {
|
||||
onList: jest.fn(),
|
||||
onOk: jest.fn(),
|
||||
onError: jest.fn()
|
||||
};
|
||||
|
||||
const stubWraps = {
|
||||
onListWrap: () => {
|
||||
stubs.onList();
|
||||
return mockApiShare.list();
|
||||
}
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const pane = mount(
|
||||
<FilePane
|
||||
onList={stubWraps.onListWrap}
|
||||
onOk={stubs.onOk}
|
||||
onError={stubs.onError}
|
||||
/>
|
||||
);
|
||||
|
||||
execFuncs(pane.instance(), testCase.execs).then(() => {
|
||||
pane.update();
|
||||
if (!valueEqual(pane.instance().state, testCase.state)) {
|
||||
return reject("FilePane: state not identical");
|
||||
}
|
||||
|
||||
if (testCase.calls != null) {
|
||||
const err = verifyCalls(testCase.calls, stubs);
|
||||
if (err != null) {
|
||||
return reject(err);
|
||||
}
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
136
client/components/composite/tests/info_bar.test.jsx
Normal file
136
client/components/composite/tests/info_bar.test.jsx
Normal file
|
@ -0,0 +1,136 @@
|
|||
jest.mock("../../../libs/api_share");
|
||||
jest.mock("../../../libs/api_auth");
|
||||
import React from "react";
|
||||
import { InfoBar } from "../info_bar";
|
||||
import { mount } from "enzyme";
|
||||
import * as mockApiShare from "../../../libs/api_share";
|
||||
import { execFuncs, getDesc, verifyCalls } from "../../../tests/test_helper";
|
||||
import valueEqual from "value-equal";
|
||||
|
||||
describe("InfoBar", () => {
|
||||
const tests = [
|
||||
{
|
||||
execs: [
|
||||
{
|
||||
func: "onSearch",
|
||||
args: ["searchFileName"]
|
||||
}
|
||||
],
|
||||
state: {
|
||||
filterFileName: "searchFileName",
|
||||
fold: false
|
||||
},
|
||||
calls: [
|
||||
{
|
||||
func: "onSearch",
|
||||
count: 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
execs: [
|
||||
{
|
||||
func: "onLogin",
|
||||
args: ["serverAddr", "adminId", "adminPwd"]
|
||||
}
|
||||
],
|
||||
state: {
|
||||
filterFileName: "",
|
||||
fold: false
|
||||
},
|
||||
calls: [
|
||||
{
|
||||
func: "onLogin",
|
||||
count: 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
execs: [
|
||||
{
|
||||
func: "onLogout",
|
||||
args: ["serverAddr"]
|
||||
}
|
||||
],
|
||||
state: {
|
||||
filterFileName: "",
|
||||
fold: false
|
||||
},
|
||||
calls: [
|
||||
{
|
||||
func: "onLogout",
|
||||
count: 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
execs: [
|
||||
{
|
||||
func: "onAddLocalFiles",
|
||||
args: []
|
||||
}
|
||||
],
|
||||
state: {
|
||||
filterFileName: "",
|
||||
fold: false
|
||||
},
|
||||
calls: [
|
||||
{
|
||||
func: "onAddLocalFiles",
|
||||
count: 1
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
tests.forEach(testCase => {
|
||||
test(getDesc("InfoBar", testCase), () => {
|
||||
const stubs = {
|
||||
onLogin: jest.fn(),
|
||||
onLogout: jest.fn(),
|
||||
onAddLocalFiles: jest.fn(),
|
||||
onSearch: jest.fn(),
|
||||
onOk: jest.fn(),
|
||||
onError: jest.fn()
|
||||
};
|
||||
|
||||
const onAddLocalFilesWrap = () => {
|
||||
stubs.onAddLocalFiles();
|
||||
return Promise.resolve(true);
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const infoBar = mount(
|
||||
<InfoBar
|
||||
width="100%"
|
||||
isLogin={false}
|
||||
serverAddr=""
|
||||
onLogin={stubs.onLogin}
|
||||
onLogout={stubs.onLogout}
|
||||
onAddLocalFiles={onAddLocalFilesWrap}
|
||||
onSearch={stubs.onSearch}
|
||||
onOk={stubs.onOk}
|
||||
onError={stubs.onError}
|
||||
/>
|
||||
);
|
||||
|
||||
execFuncs(infoBar.instance(), testCase.execs)
|
||||
.then(() => {
|
||||
infoBar.update();
|
||||
|
||||
if (!valueEqual(infoBar.instance().state, testCase.state)) {
|
||||
return reject("state not identical");
|
||||
}
|
||||
if (testCase.calls != null) {
|
||||
const err = verifyCalls(testCase.calls, stubs);
|
||||
if (err !== null) {
|
||||
return reject(err);
|
||||
}
|
||||
}
|
||||
resolve();
|
||||
})
|
||||
.catch(err => console.error(err));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
51
client/components/composite/tests/uploader.test.jsx
Normal file
51
client/components/composite/tests/uploader.test.jsx
Normal file
|
@ -0,0 +1,51 @@
|
|||
import React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import { checkQueueCycle, Uploader } from "../uploader";
|
||||
|
||||
const testTimeout = 4000;
|
||||
|
||||
describe("Uploader", () => {
|
||||
test(
|
||||
"Uploader will upload files in uploadQueue by interval",
|
||||
() => {
|
||||
// TODO: could be refactored using timer mocks
|
||||
// https://facebook.github.io/jest/docs/en/timer-mocks.html
|
||||
const tests = [
|
||||
{
|
||||
input: { target: { files: ["task1", "task2", "task3"] } },
|
||||
uploadCalled: 3
|
||||
}
|
||||
];
|
||||
|
||||
let promises = [];
|
||||
|
||||
const uploader = mount(<Uploader />);
|
||||
tests.forEach(testCase => {
|
||||
// mock
|
||||
const uploadSpy = jest.fn();
|
||||
const uploadStub = () => {
|
||||
uploadSpy();
|
||||
return Promise.resolve();
|
||||
};
|
||||
uploader.instance().upload = uploadStub;
|
||||
uploader.update();
|
||||
|
||||
// upload and verify
|
||||
uploader.instance().onUpload(testCase.input);
|
||||
const wait = testCase.input.target.files.length * 1000 + 100;
|
||||
const promise = new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
expect(uploader.instance().state.uploadQueue.length).toBe(0);
|
||||
expect(uploadSpy.mock.calls.length).toBe(testCase.uploadCalled);
|
||||
resolve();
|
||||
}, wait);
|
||||
});
|
||||
|
||||
promises = [...promises, promise];
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
},
|
||||
testTimeout
|
||||
);
|
||||
});
|
69
client/components/composite/time_grids.jsx
Normal file
69
client/components/composite/time_grids.jsx
Normal file
|
@ -0,0 +1,69 @@
|
|||
import React from "react";
|
||||
import { config } from "../../config";
|
||||
import { Grids } from "../layout/grids";
|
||||
|
||||
const styleTitle = {
|
||||
color: "#fff",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.4)",
|
||||
display: "inline-block",
|
||||
padding: "0.5rem 1rem",
|
||||
fontSize: "1rem",
|
||||
margin: "2rem 0 0.5rem 0",
|
||||
lineHeight: "1rem",
|
||||
height: "1rem"
|
||||
};
|
||||
|
||||
export class TimeGrids extends React.PureComponent {
|
||||
render() {
|
||||
const groups = new Map();
|
||||
|
||||
this.props.items.forEach(item => {
|
||||
const date = new Date(item.timestamp);
|
||||
const key = `${date.getFullYear()}-${date.getMonth() +
|
||||
1}-${date.getDate()}`;
|
||||
|
||||
if (groups.has(key)) {
|
||||
groups.set(key, [...groups.get(key), item]);
|
||||
} else {
|
||||
groups.set(key, [item]);
|
||||
}
|
||||
});
|
||||
|
||||
var timeGrids = [];
|
||||
groups.forEach((gridGroup, groupKey) => {
|
||||
const year = parseInt(groupKey.split("-")[0]);
|
||||
const month = parseInt(groupKey.split("-")[1]);
|
||||
const date = parseInt(groupKey.split("-")[2]);
|
||||
|
||||
const sortedGroup = gridGroup.sort((item1, item2) => {
|
||||
return item2.timestamp - item1.timestamp;
|
||||
});
|
||||
|
||||
timeGrids = [
|
||||
...timeGrids,
|
||||
<div key={year * 365 + month * 30 + date}>
|
||||
<div style={styleTitle}>
|
||||
<span>{groupKey}</span>
|
||||
</div>
|
||||
<Grids nodes={sortedGroup} />
|
||||
</div>
|
||||
];
|
||||
});
|
||||
|
||||
const sortedGroups = timeGrids.sort((group1, group2) => {
|
||||
return group2.key - group1.key;
|
||||
});
|
||||
return <div style={this.props.styleContainer}>{sortedGroups}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
TimeGrids.defaultProps = {
|
||||
items: [
|
||||
{
|
||||
key: "",
|
||||
timestamp: -1,
|
||||
component: <span>no grid found</span>
|
||||
}
|
||||
],
|
||||
styleContainer: {}
|
||||
};
|
215
client/components/composite/uploader.jsx
Normal file
215
client/components/composite/uploader.jsx
Normal file
|
@ -0,0 +1,215 @@
|
|||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
import { config } from "../../config";
|
||||
import { Button } from "../control/button";
|
||||
import { getIcon } from "../display/icon";
|
||||
import { FileUploader } from "../../libs/api_upload";
|
||||
|
||||
const msgFileNotFound = "File not found";
|
||||
const msgFileUploadOk = "is uploaded";
|
||||
const msgChromeLink = "https://www.google.com/chrome/";
|
||||
const msgFirefoxLink = "https://www.mozilla.org/";
|
||||
|
||||
export const checkQueueCycle = 1000;
|
||||
|
||||
const IconPlus = getIcon("cirUp");
|
||||
const IconThiList = getIcon("thList");
|
||||
|
||||
const styleContainer = {
|
||||
position: "fixed",
|
||||
bottom: "0.5rem",
|
||||
margin: "auto",
|
||||
zIndex: 1
|
||||
};
|
||||
|
||||
const styleButtonContainer = {
|
||||
backgroundColor: "#2ecc71",
|
||||
width: "20rem",
|
||||
height: "auto",
|
||||
textAlign: "center"
|
||||
};
|
||||
|
||||
const styleDefault = {
|
||||
color: "#fff"
|
||||
};
|
||||
|
||||
const styleLabel = {
|
||||
display: "inline-block",
|
||||
verticalAlign: "middle",
|
||||
marginLeft: "0.5rem"
|
||||
};
|
||||
|
||||
const styleUploadQueue = {
|
||||
backgroundColor: "#000",
|
||||
opacity: 0.85,
|
||||
color: "#fff",
|
||||
fontSize: "0.75rem",
|
||||
lineHeight: "1.25rem"
|
||||
};
|
||||
|
||||
const styleUploadItem = {
|
||||
width: "18rem",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
padding: "0.5rem 1rem"
|
||||
};
|
||||
|
||||
const styleUnsupported = {
|
||||
backgroundColor: "#e74c3c",
|
||||
color: "#fff",
|
||||
overflow: "hidden",
|
||||
padding: "0.5rem 1rem",
|
||||
width: "18rem",
|
||||
textAlign: "center"
|
||||
};
|
||||
|
||||
const styleStr = `
|
||||
a {
|
||||
color: white;
|
||||
margin: auto 0.5rem auto 0.5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export class Uploader extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
uploadQueue: [],
|
||||
uploadValue: ""
|
||||
};
|
||||
|
||||
this.input = undefined;
|
||||
this.assignInput = input => {
|
||||
this.input = ReactDOM.findDOMNode(input);
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// will polling uploadQueue like a worker
|
||||
this.checkQueue();
|
||||
}
|
||||
|
||||
checkQueue = () => {
|
||||
// TODO: using web worker to avoid lagging UI
|
||||
if (this.state.uploadQueue.length > 0) {
|
||||
this.upload(this.state.uploadQueue[0]).then(() => {
|
||||
this.setState({ uploadQueue: this.state.uploadQueue.slice(1) });
|
||||
setTimeout(this.checkQueue, checkQueueCycle);
|
||||
});
|
||||
} else {
|
||||
setTimeout(this.checkQueue, checkQueueCycle);
|
||||
}
|
||||
};
|
||||
|
||||
upload = file => {
|
||||
const fileUploader = new FileUploader(
|
||||
this.onStart,
|
||||
this.onProgress,
|
||||
this.onFinish,
|
||||
this.onError
|
||||
);
|
||||
|
||||
return fileUploader.uploadFile(file);
|
||||
};
|
||||
|
||||
onStart = () => {
|
||||
this.props.onRefresh();
|
||||
};
|
||||
|
||||
onProgress = (shareId, progress) => {
|
||||
this.props.onUpdateProgress(shareId, progress);
|
||||
};
|
||||
|
||||
onFinish = () => {
|
||||
this.props.onRefresh();
|
||||
};
|
||||
|
||||
onError = err => {
|
||||
this.props.onError(err);
|
||||
};
|
||||
|
||||
onUpload = event => {
|
||||
if (event.target.files == null || event.target.files.length === 0) {
|
||||
this.props.onError(msgFileNotFound);
|
||||
this.setState({ uploadValue: "" });
|
||||
} else {
|
||||
this.setState({
|
||||
uploadQueue: [...this.state.uploadQueue, ...event.target.files],
|
||||
uploadValue: ""
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onChooseFile = () => {
|
||||
this.input.click();
|
||||
};
|
||||
|
||||
render() {
|
||||
if (
|
||||
window.FormData == null ||
|
||||
window.FileReader == null ||
|
||||
window.Blob == null
|
||||
) {
|
||||
return (
|
||||
<div style={{ ...styleUnsupported, ...styleContainer }}>
|
||||
Unsupported Browser. Try
|
||||
<a href={msgFirefoxLink} target="_blank">
|
||||
Firefox
|
||||
</a>
|
||||
or
|
||||
<a href={msgChromeLink} target="_blank">
|
||||
Chrome
|
||||
</a>
|
||||
<style>{styleStr}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const hiddenInput = (
|
||||
<input
|
||||
type="file"
|
||||
onChange={this.onUpload}
|
||||
style={{ display: "none" }}
|
||||
ref={this.assignInput}
|
||||
multiple={true}
|
||||
value={this.state.uploadValue}
|
||||
/>
|
||||
);
|
||||
|
||||
const uploadQueue = this.state.uploadQueue.map(file => {
|
||||
return (
|
||||
<div key={file.name} style={styleUploadItem}>
|
||||
<IconThiList
|
||||
size={config.rootSize * 0.75}
|
||||
style={{ marginRight: "0.5rem" }}
|
||||
/>
|
||||
{file.name}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="uploader" style={styleContainer}>
|
||||
<div style={styleUploadQueue}>{uploadQueue}</div>
|
||||
<Button
|
||||
onClick={this.onChooseFile}
|
||||
label="UPLOAD"
|
||||
icon={<IconPlus size={config.rootSize} style={styleDefault} />}
|
||||
styleDefault={styleDefault}
|
||||
styleContainer={styleButtonContainer}
|
||||
styleLabel={styleLabel}
|
||||
/>
|
||||
{hiddenInput}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Uploader.defaultProps = {
|
||||
onRefresh: () => console.error("undefined"),
|
||||
onUpdateProgress: () => console.error("undefined"),
|
||||
onOk: () => console.error("undefined"),
|
||||
onError: () => console.error("undefined")
|
||||
};
|
102
client/components/control/button.jsx
Normal file
102
client/components/control/button.jsx
Normal file
|
@ -0,0 +1,102 @@
|
|||
import React from "react";
|
||||
|
||||
const buttonClassName = "btn";
|
||||
|
||||
const styleContainer = {
|
||||
display: "inline-block",
|
||||
height: "2.5rem"
|
||||
};
|
||||
|
||||
const styleIcon = {
|
||||
lineHeight: "2.5rem",
|
||||
height: "2.5rem",
|
||||
margin: "0 -0.25rem 0 0.5rem"
|
||||
};
|
||||
|
||||
const styleBase = {
|
||||
background: "transparent",
|
||||
lineHeight: "2.5rem",
|
||||
fontSize: "0.875rem",
|
||||
border: "none",
|
||||
outline: "none",
|
||||
padding: "0 0.75rem",
|
||||
textAlign: "center"
|
||||
};
|
||||
|
||||
const styleDefault = {
|
||||
...styleBase
|
||||
};
|
||||
|
||||
const styleStr = `
|
||||
.${buttonClassName}:hover {
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.25s;
|
||||
}
|
||||
|
||||
.${buttonClassName}:active {
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.25s;
|
||||
}
|
||||
|
||||
.${buttonClassName}:disabled {
|
||||
opacity: 0.2;
|
||||
transition: opacity 0.25s;
|
||||
}
|
||||
|
||||
button::-moz-focus-inner {
|
||||
border: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export class Button extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.styleDefault = { ...styleDefault, ...this.props.styleDefault };
|
||||
this.styleStr = this.props.styleStr ? this.props.styleStr : styleStr;
|
||||
}
|
||||
|
||||
onClick = e => {
|
||||
if (this.props.onClick && this.props.isEnabled) {
|
||||
this.props.onClick(e);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const style = this.props.isEnabled ? this.styleDefault : this.styleDisabled;
|
||||
const icon =
|
||||
this.props.icon != null ? (
|
||||
<span style={{ ...styleIcon, ...this.props.styleIcon }}>
|
||||
{this.props.icon}
|
||||
</span>
|
||||
) : (
|
||||
<span />
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ ...styleContainer, ...this.props.styleContainer }}
|
||||
className={`${buttonClassName} ${this.props.className}`}
|
||||
onClick={this.onClick}
|
||||
>
|
||||
{icon}
|
||||
<button style={style}>
|
||||
<span style={this.props.styleLabel}>{this.props.label}</span>
|
||||
<style>{this.styleStr}</style>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Button.defaultProps = {
|
||||
className: "btn",
|
||||
isEnabled: true,
|
||||
icon: null,
|
||||
onClick: () => true,
|
||||
styleContainer: {},
|
||||
styleDefault: {},
|
||||
styleDisabled: {},
|
||||
styleLabel: {},
|
||||
styleIcon: {},
|
||||
styleStr: undefined
|
||||
};
|
128
client/components/control/input.jsx
Normal file
128
client/components/control/input.jsx
Normal file
|
@ -0,0 +1,128 @@
|
|||
import React from "react";
|
||||
|
||||
const styleContainer = {
|
||||
backgroundColor: "#ccc",
|
||||
display: "inline-block",
|
||||
height: "2.5rem"
|
||||
};
|
||||
|
||||
const styleIcon = {
|
||||
lineHeight: "2.5rem",
|
||||
height: "2.5rem",
|
||||
margin: "0 0.25rem 0 0.5rem"
|
||||
};
|
||||
|
||||
const styleInputBase = {
|
||||
backgroundColor: "transparent",
|
||||
border: "none",
|
||||
display: "inline-block",
|
||||
fontSize: "0.875rem",
|
||||
height: "2.5rem",
|
||||
lineHeight: "2.5rem",
|
||||
outline: "none",
|
||||
overflowY: "hidden",
|
||||
padding: "0 0.75rem",
|
||||
verticalAlign: "middle"
|
||||
};
|
||||
|
||||
const styleDefault = {
|
||||
...styleInputBase,
|
||||
color: "#333"
|
||||
};
|
||||
|
||||
const styleInvalid = {
|
||||
...styleInputBase,
|
||||
color: "#e74c3c"
|
||||
};
|
||||
|
||||
const inputClassName = "qs-input";
|
||||
const styleStr = `
|
||||
.${inputClassName}:hover {
|
||||
// box-shadow: 0px 0px -5px rgba(0, 0, 0, 1);
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.25s;
|
||||
}
|
||||
|
||||
.${inputClassName}:active {
|
||||
// box-shadow: 0px 0px -5px rgba(0, 0, 0, 1);
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.25s;
|
||||
}
|
||||
|
||||
.${inputClassName}:disabled {
|
||||
color: #ccc;
|
||||
}
|
||||
`;
|
||||
|
||||
export class Input extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { isValid: true };
|
||||
this.inputRef = undefined;
|
||||
}
|
||||
|
||||
onChange = e => {
|
||||
this.props.onChange(e.target.value);
|
||||
this.props.onChangeEvent(e);
|
||||
this.props.onChangeTarget(e.target);
|
||||
this.setState({ isValid: this.props.validate(e.target.value) });
|
||||
};
|
||||
|
||||
getRef = input => {
|
||||
this.inputRef = input;
|
||||
this.props.inputRef(this.inputRef);
|
||||
};
|
||||
|
||||
render() {
|
||||
const style = this.state.isValid ? styleDefault : styleInvalid;
|
||||
const icon =
|
||||
this.props.icon != null ? (
|
||||
<span style={styleIcon}>{this.props.icon}</span>
|
||||
) : (
|
||||
<span />
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ ...styleContainer, ...this.props.styleContainer }}>
|
||||
{icon}
|
||||
<input
|
||||
style={{
|
||||
...styleDefault,
|
||||
...this.props.style,
|
||||
width: this.props.width
|
||||
}}
|
||||
className={`${inputClassName} ${this.props.className}`}
|
||||
disabled={this.props.disabled}
|
||||
readOnly={this.props.readOnly}
|
||||
maxLength={this.props.maxLength}
|
||||
placeholder={this.props.placeholder}
|
||||
type={this.props.type}
|
||||
onChange={this.onChange}
|
||||
value={this.props.value}
|
||||
ref={this.getRef}
|
||||
/>
|
||||
<style>{styleStr}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Input.defaultProps = {
|
||||
className: "input",
|
||||
maxLength: "32",
|
||||
placeholder: "placeholder",
|
||||
readOnly: false,
|
||||
style: {},
|
||||
styleContainer: {},
|
||||
styleInvalid: {},
|
||||
type: "text",
|
||||
disabled: false,
|
||||
width: "auto",
|
||||
value: "",
|
||||
icon: null,
|
||||
onChange: () => true,
|
||||
onChangeEvent: () => true,
|
||||
onChangeTarget: () => true,
|
||||
validate: () => true,
|
||||
inputRef: () => true
|
||||
};
|
177
client/components/display/icon.js
Normal file
177
client/components/display/icon.js
Normal file
|
@ -0,0 +1,177 @@
|
|||
const IconFile = require("react-icons/lib/fa/file-o");
|
||||
const IconImg = require("react-icons/lib/md/image");
|
||||
const IconZip = require("react-icons/lib/md/archive");
|
||||
const IconVideo = require("react-icons/lib/md/ondemand-video");
|
||||
const IconAudio = require("react-icons/lib/md/music-video");
|
||||
const IconText = require("react-icons/lib/md/description");
|
||||
const IconExcel = require("react-icons/lib/fa/file-excel-o");
|
||||
const IconPPT = require("react-icons/lib/fa/file-powerpoint-o");
|
||||
const IconPdf = require("react-icons/lib/md/picture-as-pdf");
|
||||
const IconWord = require("react-icons/lib/fa/file-word-o");
|
||||
const IconCode = require("react-icons/lib/md/code");
|
||||
const IconApk = require("react-icons/lib/md/android");
|
||||
const IconExe = require("react-icons/lib/fa/cog");
|
||||
|
||||
const IconBars = require("react-icons/lib/fa/bars");
|
||||
const IconSpinner = require("react-icons/lib/md/autorenew");
|
||||
const IconCirUp = require("react-icons/lib/fa/arrow-circle-up");
|
||||
const IconSignIn = require("react-icons/lib/fa/sign-in");
|
||||
const IconSignOut = require("react-icons/lib/fa/sign-out");
|
||||
const IconAngUp = require("react-icons/lib/fa/angle-up");
|
||||
const IconAngRight = require("react-icons/lib/fa/angle-right");
|
||||
const IconAngDown = require("react-icons/lib/fa/angle-down");
|
||||
const IconAngLeft = require("react-icons/lib/fa/angle-left");
|
||||
const IconTimesCir = require("react-icons/lib/md/cancel");
|
||||
const IconPlusSqu = require("react-icons/lib/md/add-box");
|
||||
const IconPlusCir = require("react-icons/lib/fa/plus-circle");
|
||||
const IconPlus = require("react-icons/lib/md/add");
|
||||
const IconSearch = require("react-icons/lib/fa/search");
|
||||
const IconThList = require("react-icons/lib/fa/th-list");
|
||||
const IconCalendar = require("react-icons/lib/fa/calendar-o");
|
||||
|
||||
const IconCheckCir = require("react-icons/lib/fa/check-circle");
|
||||
const IconExTri = require("react-icons/lib/fa/exclamation-triangle");
|
||||
const IconInfoCir = require("react-icons/lib/fa/info-circle");
|
||||
const IconRefresh = require("react-icons/lib/fa/refresh");
|
||||
|
||||
const fileTypeIconMap = {
|
||||
// text
|
||||
txt: { icon: IconText, color: "#333" },
|
||||
rtf: { icon: IconText, color: "#333" },
|
||||
htm: { icon: IconText, color: "#333" },
|
||||
html: { icon: IconText, color: "#333" },
|
||||
xml: { icon: IconText, color: "#333" },
|
||||
yml: { icon: IconText, color: "#333" },
|
||||
json: { icon: IconText, color: "#333" },
|
||||
toml: { icon: IconText, color: "#333" },
|
||||
md: { icon: IconText, color: "#333" },
|
||||
// office
|
||||
ppt: { icon: IconPPT, color: "#e67e22" },
|
||||
pptx: { icon: IconPPT, color: "#e67e22" },
|
||||
xls: { icon: IconExcel, color: "#16a085" },
|
||||
xlsx: { icon: IconExcel, color: "#16a085" },
|
||||
xlsm: { icon: IconExcel, color: "#16a085" },
|
||||
doc: { icon: IconWord, color: "#2980b9" },
|
||||
docx: { icon: IconWord, color: "#2980b9" },
|
||||
docx: { icon: IconWord, color: "#2980b9" },
|
||||
pdf: { icon: IconPdf, color: "#c0392b" },
|
||||
// code
|
||||
c: { icon: IconCode, color: "#666" },
|
||||
cpp: { icon: IconCode, color: "#666" },
|
||||
java: { icon: IconCode, color: "#666" },
|
||||
js: { icon: IconCode, color: "#666" },
|
||||
py: { icon: IconCode, color: "#666" },
|
||||
pyc: { icon: IconCode, color: "#666" },
|
||||
rb: { icon: IconCode, color: "#666" },
|
||||
php: { icon: IconCode, color: "#666" },
|
||||
go: { icon: IconCode, color: "#666" },
|
||||
sh: { icon: IconCode, color: "#666" },
|
||||
vb: { icon: IconCode, color: "#666" },
|
||||
sql: { icon: IconCode, color: "#666" },
|
||||
r: { icon: IconCode, color: "#666" },
|
||||
swift: { icon: IconCode, color: "#666" },
|
||||
oc: { icon: IconCode, color: "#666" },
|
||||
// misc
|
||||
apk: { icon: IconApk, color: "#2ecc71" },
|
||||
exe: { icon: IconExe, color: "#333" },
|
||||
deb: { icon: IconExe, color: "#333" },
|
||||
rpm: { icon: IconExe, color: "#333" },
|
||||
// img
|
||||
bmp: { icon: IconImg, color: "#1abc9c" },
|
||||
gif: { icon: IconImg, color: "#1abc9c" },
|
||||
jpg: { icon: IconImg, color: "#1abc9c" },
|
||||
jpeg: { icon: IconImg, color: "#1abc9c" },
|
||||
tiff: { icon: IconImg, color: "#1abc9c" },
|
||||
psd: { icon: IconImg, color: "#1abc9c" },
|
||||
png: { icon: IconImg, color: "#1abc9c" },
|
||||
svg: { icon: IconImg, color: "#1abc9c" },
|
||||
pcx: { icon: IconImg, color: "#1abc9c" },
|
||||
dxf: { icon: IconImg, color: "#1abc9c" },
|
||||
wmf: { icon: IconImg, color: "#1abc9c" },
|
||||
emf: { icon: IconImg, color: "#1abc9c" },
|
||||
eps: { icon: IconImg, color: "#1abc9c" },
|
||||
tga: { icon: IconImg, color: "#1abc9c" },
|
||||
// compress
|
||||
gz: { icon: IconZip, color: "#34495e" },
|
||||
zip: { icon: IconZip, color: "#34495e" },
|
||||
"7z": { icon: IconZip, color: "#34495e" },
|
||||
rar: { icon: IconZip, color: "#34495e" },
|
||||
tar: { icon: IconZip, color: "#34495e" },
|
||||
gzip: { icon: IconZip, color: "#34495e" },
|
||||
cab: { icon: IconZip, color: "#34495e" },
|
||||
uue: { icon: IconZip, color: "#34495e" },
|
||||
arj: { icon: IconZip, color: "#34495e" },
|
||||
bz2: { icon: IconZip, color: "#34495e" },
|
||||
lzh: { icon: IconZip, color: "#34495e" },
|
||||
jar: { icon: IconZip, color: "#34495e" },
|
||||
ace: { icon: IconZip, color: "#34495e" },
|
||||
iso: { icon: IconZip, color: "#34495e" },
|
||||
z: { icon: IconZip, color: "#34495e" },
|
||||
// video
|
||||
asf: { icon: IconVideo, color: "#f39c12" },
|
||||
avi: { icon: IconVideo, color: "#f39c12" },
|
||||
flv: { icon: IconVideo, color: "#f39c12" },
|
||||
mkv: { icon: IconVideo, color: "#f39c12" },
|
||||
mov: { icon: IconVideo, color: "#f39c12" },
|
||||
mp4: { icon: IconVideo, color: "#f39c12" },
|
||||
mpeg: { icon: IconVideo, color: "#f39c12" },
|
||||
mpg: { icon: IconVideo, color: "#f39c12" },
|
||||
ram: { icon: IconVideo, color: "#f39c12" },
|
||||
rmvb: { icon: IconVideo, color: "#f39c12" },
|
||||
qt: { icon: IconVideo, color: "#f39c12" },
|
||||
wmv: { icon: IconVideo, color: "#f39c12" },
|
||||
// audio
|
||||
cda: { icon: IconAudio, color: "#d35400" },
|
||||
cmf: { icon: IconAudio, color: "#d35400" },
|
||||
mid: { icon: IconAudio, color: "#d35400" },
|
||||
mp1: { icon: IconAudio, color: "#d35400" },
|
||||
mp2: { icon: IconAudio, color: "#d35400" },
|
||||
mp3: { icon: IconAudio, color: "#d35400" },
|
||||
rm: { icon: IconAudio, color: "#d35400" },
|
||||
rmi: { icon: IconAudio, color: "#d35400" },
|
||||
vqf: { icon: IconAudio, color: "#d35400" },
|
||||
wav: { icon: IconAudio, color: "#d35400" }
|
||||
};
|
||||
|
||||
const fileIconMap = {
|
||||
...fileTypeIconMap,
|
||||
// other
|
||||
spinner: { icon: IconSpinner, color: "#1abc9c" },
|
||||
cirup: { icon: IconCirUp, color: "#fff" },
|
||||
signin: { icon: IconSignIn, color: "#fff" },
|
||||
signout: { icon: IconSignOut, color: "#fff" },
|
||||
angup: { icon: IconAngUp, color: "#2c3e50" },
|
||||
angright: { icon: IconAngRight, color: "#2c3e50" },
|
||||
angdown: { icon: IconAngDown, color: "#2c3e50" },
|
||||
angleft: { icon: IconAngLeft, color: "#2c3e50" },
|
||||
timescir: { icon: IconTimesCir, color: "#c0392b" },
|
||||
plussqu: { icon: IconPlusSqu, color: "#2ecc71" },
|
||||
pluscir: { icon: IconPlusCir, color: "#2ecc71" },
|
||||
plus: { icon: IconPlus, color: "#2ecc71" },
|
||||
search: { icon: IconSearch, color: "#ccc" },
|
||||
checkcir: { icon: IconCheckCir, color: "#27ae60" },
|
||||
extri: { icon: IconExTri, color: "#f39c12" },
|
||||
infocir: { icon: IconInfoCir, color: "#2c3e50" },
|
||||
refresh: { icon: IconRefresh, color: "#8e44ad" },
|
||||
thlist: { icon: IconThList, color: "#fff" },
|
||||
bars: { icon: IconBars, color: "#666" },
|
||||
calendar: { icon: IconCalendar, color: "#333" }
|
||||
};
|
||||
|
||||
export const getIcon = extend => {
|
||||
if (fileIconMap[extend.toUpperCase()]) {
|
||||
return fileIconMap[extend.toUpperCase()].icon;
|
||||
} else if (fileIconMap[extend.toLowerCase()]) {
|
||||
return fileIconMap[extend.toLowerCase()].icon;
|
||||
}
|
||||
return IconFile;
|
||||
};
|
||||
|
||||
export const getIconColor = extend => {
|
||||
if (fileIconMap[extend.toUpperCase()]) {
|
||||
return fileIconMap[extend.toUpperCase()].color;
|
||||
} else if (fileIconMap[extend.toLowerCase()]) {
|
||||
return fileIconMap[extend.toLowerCase()].color;
|
||||
}
|
||||
return "#333";
|
||||
};
|
27
client/components/layout/grids.jsx
Normal file
27
client/components/layout/grids.jsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
import React from "react";
|
||||
|
||||
const styleGridBase = {
|
||||
float: "left",
|
||||
margin: 0
|
||||
};
|
||||
|
||||
export const Grids = props => (
|
||||
<div style={props.containerStyle}>
|
||||
{props.nodes.map(node => (
|
||||
<div
|
||||
className="grid"
|
||||
key={node.key}
|
||||
style={{ ...props.gridStyle, ...node.style }}
|
||||
>
|
||||
{node.component}
|
||||
</div>
|
||||
))}
|
||||
<div style={{ clear: "both" }} />
|
||||
</div>
|
||||
);
|
||||
|
||||
Grids.defaultProps = {
|
||||
nodes: [{ key: "key", component: <span />, style: {} }],
|
||||
gridStyle: styleGridBase,
|
||||
containerStyle: {}
|
||||
};
|
7
client/config.js
Normal file
7
client/config.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
export const config = {
|
||||
serverAddr: "",
|
||||
testId: "admin",
|
||||
testPwd: "quicksh@re",
|
||||
rootSize: 16,
|
||||
colWidth: 20
|
||||
};
|
7
client/libs/__mocks__/api_auth.js
Normal file
7
client/libs/__mocks__/api_auth.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
export function login(serverAddr, adminId, adminPwd, axiosConfig) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
export function logout(serverAddr, axiosConfig) {
|
||||
return Promise.resolve(true);
|
||||
}
|
41
client/libs/__mocks__/api_share.js
Normal file
41
client/libs/__mocks__/api_share.js
Normal file
|
@ -0,0 +1,41 @@
|
|||
let _infos = [];
|
||||
const shadowedId = "shadowedId";
|
||||
const publicId = "publicId";
|
||||
|
||||
export function __addInfos(infos) {
|
||||
_infos = [..._infos, ...infos];
|
||||
}
|
||||
|
||||
export function __truncInfos(info) {
|
||||
_infos = [];
|
||||
}
|
||||
|
||||
export const del = shareId => {
|
||||
_infos = _infos.filter(info => {
|
||||
return !info.shareId == shareId;
|
||||
});
|
||||
return Promise.resolve(true);
|
||||
};
|
||||
|
||||
export const list = () => {
|
||||
return Promise.resolve(_infos);
|
||||
};
|
||||
|
||||
export const shadowId = shareId => {
|
||||
return Promise.resolve(shadowedId);
|
||||
};
|
||||
|
||||
export const publishId = shareId => {
|
||||
return Promise.resolve(publicId);
|
||||
};
|
||||
|
||||
export const setDownLimit = (shareId, downLimit) => {
|
||||
_infos = _infos.map(info => {
|
||||
return info.shareId == shareId ? { ...info, downLimit } : info;
|
||||
});
|
||||
return Promise.resolve(true);
|
||||
};
|
||||
|
||||
export const addLocalFiles = () => {
|
||||
return Promise.resolve(true);
|
||||
};
|
29
client/libs/api_auth.js
Normal file
29
client/libs/api_auth.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
import axios from "axios";
|
||||
import { config } from "../config";
|
||||
import { makePostBody } from "./utils";
|
||||
|
||||
export function login(serverAddr, adminId, adminPwd, axiosConfig) {
|
||||
return axios
|
||||
.post(
|
||||
`${serverAddr}/login`,
|
||||
makePostBody(
|
||||
{
|
||||
act: "login",
|
||||
adminid: adminId,
|
||||
adminpwd: adminPwd
|
||||
},
|
||||
axiosConfig
|
||||
)
|
||||
)
|
||||
.then(response => {
|
||||
return response.data.Code === 200;
|
||||
});
|
||||
}
|
||||
|
||||
export function logout(serverAddr, axiosConfig) {
|
||||
return axios
|
||||
.post(`${serverAddr}/login`, makePostBody({ act: "logout" }), axiosConfig)
|
||||
.then(response => {
|
||||
return response.data.Code === 200;
|
||||
});
|
||||
}
|
51
client/libs/api_share.js
Normal file
51
client/libs/api_share.js
Normal file
|
@ -0,0 +1,51 @@
|
|||
import axios from "axios";
|
||||
import { config } from "../config";
|
||||
|
||||
export const del = shareId => {
|
||||
return axios
|
||||
.delete(`${config.serverAddr}/fileinfo?shareid=${shareId}`)
|
||||
.then(response => response.data.Code === 200);
|
||||
};
|
||||
|
||||
export const list = () => {
|
||||
return axios.get(`${config.serverAddr}/fileinfo`).then(response => {
|
||||
// TODO check status code
|
||||
return response.data.List;
|
||||
});
|
||||
};
|
||||
|
||||
export const shadowId = shareId => {
|
||||
const act = "shadowid";
|
||||
return axios
|
||||
.patch(`${config.serverAddr}/fileinfo?act=${act}&shareid=${shareId}`)
|
||||
.then(response => {
|
||||
return response.data.ShareId;
|
||||
});
|
||||
};
|
||||
|
||||
export const publishId = shareId => {
|
||||
const act = "publishid";
|
||||
return axios
|
||||
.patch(`${config.serverAddr}/fileinfo?act=${act}&shareid=${shareId}`)
|
||||
.then(response => {
|
||||
return response.data.ShareId;
|
||||
});
|
||||
};
|
||||
|
||||
export const setDownLimit = (shareId, downLimit) => {
|
||||
const act = "setdownlimit";
|
||||
return axios
|
||||
.patch(
|
||||
`${
|
||||
config.serverAddr
|
||||
}/fileinfo?act=${act}&shareid=${shareId}&downlimit=${downLimit}`
|
||||
)
|
||||
.then(response => response.data.Code === 200);
|
||||
};
|
||||
|
||||
export const addLocalFiles = () => {
|
||||
const act = "addlocalfiles";
|
||||
return axios
|
||||
.patch(`${config.serverAddr}/fileinfo?act=${act}`)
|
||||
.then(response => response.data.Code === 200);
|
||||
};
|
202
client/libs/api_upload.js
Normal file
202
client/libs/api_upload.js
Normal file
|
@ -0,0 +1,202 @@
|
|||
import axios from "axios";
|
||||
import { config } from "../config";
|
||||
import { makePostBody } from "./utils";
|
||||
|
||||
const wait = 5000; // TODO: should tune according to backend
|
||||
const retryMax = 100000;
|
||||
const maxUploadLen = 20 * 1024 * 1024;
|
||||
|
||||
// TODO: add to react-intl
|
||||
const msgUploadFailed = "Fail to upload, upload is stopped.";
|
||||
const msgUploadFailedAndRetry = "Fail to upload, retrying...";
|
||||
const msgFileExists = "File exists.";
|
||||
const msgTooBigChunk = "Too big chunk.";
|
||||
const msgFileNotFound = "File not found, upload stopped.";
|
||||
|
||||
function randomWait() {
|
||||
return Math.random() * wait;
|
||||
}
|
||||
|
||||
function isKnownErr(res) {
|
||||
return res != null && res.Code != null && res.Msg != null;
|
||||
}
|
||||
|
||||
export class FileUploader {
|
||||
constructor(onStart, onProgress, onFinish, onError) {
|
||||
this.onStart = onStart;
|
||||
this.onProgress = onProgress;
|
||||
this.onFinish = onFinish;
|
||||
this.onError = onError;
|
||||
this.retry = retryMax;
|
||||
this.reader = new FileReader();
|
||||
|
||||
this.uploadFile = file => {
|
||||
return this.startUpload(file);
|
||||
};
|
||||
|
||||
this.startUpload = file => {
|
||||
return axios
|
||||
.post(
|
||||
`${config.serverAddr}/startupload`,
|
||||
makePostBody({
|
||||
fname: file.name
|
||||
})
|
||||
)
|
||||
.then(response => {
|
||||
if (
|
||||
response.data.ShareId == null ||
|
||||
response.data.Start === null ||
|
||||
response.data.Length === null
|
||||
) {
|
||||
throw response;
|
||||
} else {
|
||||
this.onStart(response.data.ShareId, file.name);
|
||||
return this.upload(
|
||||
{
|
||||
shareId: response.data.ShareId,
|
||||
start: response.data.Start,
|
||||
length: response.data.Length
|
||||
},
|
||||
file
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch(response => {
|
||||
// TODO: this is not good because error may not be response
|
||||
if (isKnownErr(response.data) && response.data.Code === 429) {
|
||||
setTimeout(this.startUpload, randomWait(), file);
|
||||
} else if (isKnownErr(response.data) && response.data.Code === 412) {
|
||||
this.onError(msgFileExists);
|
||||
} else if (isKnownErr(response.data) && response.data.Code === 404) {
|
||||
this.onError(msgFileNotFound);
|
||||
} else if (this.retry > 0) {
|
||||
this.retry--;
|
||||
this.onError(msgUploadFailedAndRetry);
|
||||
console.trace(response);
|
||||
setTimeout(this.startUpload, randomWait(), file);
|
||||
} else {
|
||||
this.onError(msgUploadFailed);
|
||||
console.trace(response);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
this.prepareReader = (shareInfo, end, resolve, reject) => {
|
||||
this.reader.onerror = err => {
|
||||
reject(err);
|
||||
};
|
||||
|
||||
this.reader.onloadend = event => {
|
||||
const formData = new FormData();
|
||||
formData.append("shareid", shareInfo.shareId);
|
||||
formData.append("start", shareInfo.start);
|
||||
formData.append("len", end - shareInfo.start);
|
||||
formData.append("chunk", new Blob([event.target.result]));
|
||||
|
||||
const url = `${config.serverAddr}/upload`;
|
||||
const headers = {
|
||||
"Content-Type": "multipart/form-data"
|
||||
};
|
||||
|
||||
try {
|
||||
axios
|
||||
.post(url, formData, { headers })
|
||||
.then(response => resolve(response))
|
||||
.catch(err => {
|
||||
reject(err);
|
||||
});
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
this.upload = (shareInfo, file) => {
|
||||
const uploaded = shareInfo.start + shareInfo.length;
|
||||
const end = uploaded < file.size ? uploaded : file.size;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (
|
||||
end == null ||
|
||||
shareInfo.start == null ||
|
||||
end - shareInfo.start >= maxUploadLen
|
||||
) {
|
||||
throw new Error(msgTooBigChunk);
|
||||
}
|
||||
|
||||
const chunk = file.slice(shareInfo.start, end);
|
||||
this.prepareReader(shareInfo, end, resolve, reject);
|
||||
this.reader.readAsArrayBuffer(chunk);
|
||||
})
|
||||
.then(response => {
|
||||
if (
|
||||
response.data.ShareId == null ||
|
||||
response.data.Start == null ||
|
||||
response.data.Length == null ||
|
||||
response.data.Start !== end
|
||||
) {
|
||||
throw response;
|
||||
} else {
|
||||
if (end < file.size) {
|
||||
this.onProgress(shareInfo.shareId, end / file.size);
|
||||
return this.upload(
|
||||
{
|
||||
shareId: shareInfo.shareId,
|
||||
start: shareInfo.start + shareInfo.length,
|
||||
length: shareInfo.length
|
||||
},
|
||||
file
|
||||
);
|
||||
} else {
|
||||
return this.finishUpload(shareInfo);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(response => {
|
||||
// possible error: response.data.Start == null || response.data.Start !== end
|
||||
if (isKnownErr(response.data) && response.data.Code === 429) {
|
||||
setTimeout(this.upload, randomWait(), shareInfo, file);
|
||||
} else if (isKnownErr(response.data) && response.data.Code === 404) {
|
||||
this.onError(msgFileNotFound);
|
||||
} else if (this.retry > 0) {
|
||||
this.retry--;
|
||||
setTimeout(this.upload, randomWait(), shareInfo, file);
|
||||
this.onError(msgUploadFailedAndRetry);
|
||||
console.trace(response);
|
||||
} else {
|
||||
this.onError(msgUploadFailed);
|
||||
console.trace(response);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
this.finishUpload = shareInfo => {
|
||||
return axios
|
||||
.post(`${config.serverAddr}/finishupload?shareid=${shareInfo.shareId}`)
|
||||
.then(response => {
|
||||
// TODO: should check Code instead of Url
|
||||
if (response.data.ShareId != null && response.data.Start == null) {
|
||||
this.onFinish();
|
||||
return response.data.ShareId;
|
||||
} else {
|
||||
throw response;
|
||||
}
|
||||
})
|
||||
.catch(response => {
|
||||
if (isKnownErr(response.data) && response.data.Code === 429) {
|
||||
setTimeout(this.finishUpload, randomWait(), shareInfo);
|
||||
} else if (isKnownErr(response.data) && response.data.Code === 404) {
|
||||
this.onError(msgFileNotFound);
|
||||
} else if (this.retry > 0) {
|
||||
this.retry--;
|
||||
setTimeout(this.finishUpload, randomWait(), shareInfo);
|
||||
this.onError(msgUploadFailedAndRetry);
|
||||
console.trace(response);
|
||||
} else {
|
||||
this.onError(msgUploadFailed);
|
||||
console.trace(response);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
18
client/libs/file_type.js
Normal file
18
client/libs/file_type.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
const fileTypeMap = {
|
||||
jpg: "image",
|
||||
jpeg: "image",
|
||||
png: "image",
|
||||
bmp: "image",
|
||||
gz: "archive",
|
||||
mov: "video",
|
||||
mp4: "video",
|
||||
mov: "video",
|
||||
avi: "video"
|
||||
};
|
||||
|
||||
export const getFileExt = fileName => fileName.split(".").pop();
|
||||
|
||||
export const getFileType = fileName => {
|
||||
const ext = getFileExt(fileName);
|
||||
return fileTypeMap[ext] != null ? fileTypeMap[ext] : "file";
|
||||
};
|
34
client/libs/test/api_auth_test.js
Normal file
34
client/libs/test/api_auth_test.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { login, logout } from "../api_auth";
|
||||
import { config } from "../../config";
|
||||
|
||||
const serverAddr = config.serverAddr;
|
||||
const testId = config.testId;
|
||||
const testPwd = config.testPwd;
|
||||
|
||||
export function testAuth() {
|
||||
return testLogin()
|
||||
.then(testLogout)
|
||||
.catch(err => {
|
||||
console.error("auth: fail", err);
|
||||
});
|
||||
}
|
||||
|
||||
export function testLogin() {
|
||||
return login(serverAddr, testId, testPwd).then(ok => {
|
||||
if (ok === true) {
|
||||
console.log("login api: ok");
|
||||
} else {
|
||||
throw new Error("login api: failed");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function testLogout() {
|
||||
return logout(serverAddr).then(ok => {
|
||||
if (ok === true) {
|
||||
console.log("logout api: ok");
|
||||
} else {
|
||||
throw new Error("logout api: failed");
|
||||
}
|
||||
});
|
||||
}
|
167
client/libs/test/api_share_test.js
Normal file
167
client/libs/test/api_share_test.js
Normal file
|
@ -0,0 +1,167 @@
|
|||
import { FileUploader } from "../api_upload";
|
||||
import {
|
||||
del,
|
||||
list,
|
||||
shadowId,
|
||||
publishId,
|
||||
setDownLimit,
|
||||
addLocalFiles
|
||||
} from "../api_share";
|
||||
import { testLogin, testLogout } from "./api_auth_test";
|
||||
|
||||
const fileName = "filename";
|
||||
|
||||
function upload(fileName) {
|
||||
return new Promise(resolve => {
|
||||
const onStart = () => true;
|
||||
const onProgress = () => true;
|
||||
const onFinish = () => resolve();
|
||||
const onError = err => {
|
||||
throw new Error(JSON.stringify(err));
|
||||
};
|
||||
const file = new File(["foo"], fileName, {
|
||||
type: "text/plain"
|
||||
});
|
||||
|
||||
const uploader = new FileUploader(onStart, onProgress, onFinish, onError);
|
||||
uploader.uploadFile(file);
|
||||
});
|
||||
}
|
||||
|
||||
function getIdFromList(list, fileName) {
|
||||
if (list == null) {
|
||||
throw new Error("list: list fail");
|
||||
}
|
||||
|
||||
// TODO: should verify file name
|
||||
const filterInfo = list.find(info => {
|
||||
return info.PathLocal.includes(fileName);
|
||||
});
|
||||
|
||||
if (filterInfo == null) {
|
||||
console.error(list);
|
||||
throw new Error("list: file name not found");
|
||||
} else {
|
||||
return filterInfo.Id;
|
||||
}
|
||||
}
|
||||
|
||||
function delWithName(fileName) {
|
||||
return list().then(infoList => {
|
||||
const infoToDel = infoList.find(info => {
|
||||
return info.PathLocal.includes(fileName);
|
||||
});
|
||||
|
||||
if (infoToDel == null) {
|
||||
console.warn("delWithName: name not found");
|
||||
} else {
|
||||
return del(infoToDel.Id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function testShadowPublishId() {
|
||||
return testLogin()
|
||||
.then(() => upload(fileName))
|
||||
.then(list)
|
||||
.then(infoList => {
|
||||
return getIdFromList(infoList, fileName);
|
||||
})
|
||||
.then(shareId => {
|
||||
return shadowId(shareId).then(secretId => {
|
||||
if (shareId === secretId) {
|
||||
throw new Error("shadowId: id not changed");
|
||||
} else {
|
||||
return secretId;
|
||||
}
|
||||
});
|
||||
})
|
||||
.then(secretId => {
|
||||
return list().then(infoList => {
|
||||
const info = infoList.find(info => {
|
||||
return info.Id === secretId;
|
||||
});
|
||||
|
||||
if (info.PathLocal.includes(fileName)) {
|
||||
console.log("shadowId api: ok", secretId);
|
||||
return secretId;
|
||||
} else {
|
||||
throw new Error("shadowId pai: file not found", infoList, fileName);
|
||||
}
|
||||
});
|
||||
})
|
||||
.then(secretId => {
|
||||
return publishId(secretId).then(publicId => {
|
||||
if (publicId === secretId) {
|
||||
// TODO: it is not enough to check they are not equal
|
||||
throw new Error("publicId: id not changed");
|
||||
} else {
|
||||
console.log("publishId api: ok", publicId);
|
||||
return publicId;
|
||||
}
|
||||
});
|
||||
})
|
||||
.then(shareId => del(shareId))
|
||||
.then(testLogout)
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
delWithName(fileName);
|
||||
});
|
||||
}
|
||||
|
||||
export function testSetDownLimit() {
|
||||
const downLimit = 777;
|
||||
|
||||
return testLogin()
|
||||
.then(() => upload(fileName))
|
||||
.then(list)
|
||||
.then(infoList => {
|
||||
return getIdFromList(infoList, fileName);
|
||||
})
|
||||
.then(shareId => {
|
||||
return setDownLimit(shareId, downLimit).then(ok => {
|
||||
if (!ok) {
|
||||
throw new Error("setDownLimit: failed");
|
||||
} else {
|
||||
return shareId;
|
||||
}
|
||||
});
|
||||
})
|
||||
.then(shareId => {
|
||||
return list().then(infoList => {
|
||||
const info = infoList.find(info => {
|
||||
return info.Id == shareId;
|
||||
});
|
||||
|
||||
if (info.DownLimit === downLimit) {
|
||||
console.log("setDownLimit api: ok");
|
||||
return shareId;
|
||||
} else {
|
||||
throw new Error("setDownLimit api: limit unchanged");
|
||||
}
|
||||
});
|
||||
})
|
||||
.then(shareId => del(shareId))
|
||||
.then(testLogout)
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
delWithName(fileName);
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: need to add local file and test
|
||||
export function testAddLocalFiles() {
|
||||
return testLogin()
|
||||
.then(() => addLocalFiles())
|
||||
.then(ok => {
|
||||
if (ok) {
|
||||
console.log("addLocalFiles api: ok");
|
||||
} else {
|
||||
throw new Error("addLocalFiles api: failed");
|
||||
}
|
||||
})
|
||||
.then(() => testLogout())
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
25
client/libs/test/api_test.js
Normal file
25
client/libs/test/api_test.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { testAuth } from "./api_auth_test";
|
||||
import { testUploadOneFile } from "./api_upload_test";
|
||||
import {
|
||||
testAddLocalFiles,
|
||||
testSetDownLimit,
|
||||
testShadowPublishId
|
||||
} from "./api_share_test";
|
||||
import { testUpDownBatch } from "./api_up_down_batch_test";
|
||||
|
||||
console.log("Test started");
|
||||
|
||||
const fileName = `test_filename${Date.now()}`;
|
||||
const file = new File(["foo"], fileName, {
|
||||
type: "text/plain"
|
||||
});
|
||||
|
||||
testAuth()
|
||||
.then(testShadowPublishId)
|
||||
.then(() => testUploadOneFile(file, fileName))
|
||||
.then(testSetDownLimit)
|
||||
.then(testAddLocalFiles)
|
||||
.then(testUpDownBatch)
|
||||
.then(() => {
|
||||
console.log("Tests are finished");
|
||||
});
|
97
client/libs/test/api_up_down_batch_test.js
Normal file
97
client/libs/test/api_up_down_batch_test.js
Normal file
|
@ -0,0 +1,97 @@
|
|||
import axios from "axios";
|
||||
import md5 from "md5";
|
||||
|
||||
import { config } from "../../config";
|
||||
import { testUpload } from "./api_upload_test";
|
||||
import { list, del } from "../api_share";
|
||||
import { testLogin, testLogout } from "./api_auth_test";
|
||||
|
||||
export function testUpDownBatch() {
|
||||
const fileInfos = [
|
||||
{
|
||||
fileName: "test_2MB_1",
|
||||
content: new Array(1024 * 1024 * 2).join("x")
|
||||
},
|
||||
{
|
||||
fileName: "test_1MB_1",
|
||||
content: new Array(1024 * 1024 * 1).join("x")
|
||||
},
|
||||
{
|
||||
fileName: "test_2MB_2",
|
||||
content: new Array(1024 * 1024 * 2).join("x")
|
||||
},
|
||||
{
|
||||
fileName: "test_1B",
|
||||
content: `${new Array(3).join("o")}${new Array(3).join("x")}`
|
||||
}
|
||||
];
|
||||
|
||||
return testLogin()
|
||||
.then(() => {
|
||||
const promises = fileInfos.map(info => {
|
||||
const file = new File([info.content], info.fileName, {
|
||||
type: "text/plain"
|
||||
});
|
||||
|
||||
return testUpAndDownOneFile(file, info.fileName);
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
})
|
||||
.then(() => {
|
||||
testLogout();
|
||||
})
|
||||
.catch(err => console.error(err));
|
||||
}
|
||||
|
||||
export function testUpAndDownOneFile(file, fileName) {
|
||||
return delTestFile(fileName)
|
||||
.then(() => testUpload(file))
|
||||
.then(shareId => testDownload(shareId, file))
|
||||
.catch(err => console.error(err));
|
||||
}
|
||||
|
||||
function delTestFile(fileName) {
|
||||
return list().then(infos => {
|
||||
const info = infos.find(info => {
|
||||
return info.PathLocal === fileName;
|
||||
});
|
||||
|
||||
if (info == null) {
|
||||
console.log("up-down: file not found", fileName);
|
||||
} else {
|
||||
return del(info.Id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function testDownload(shareId, file) {
|
||||
return axios
|
||||
.get(`${config.serverAddr}/download?shareid=${shareId}`)
|
||||
.then(response => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = event => {
|
||||
const upHash = md5(event.target.result);
|
||||
const downHash = md5(response.data);
|
||||
if (upHash !== downHash) {
|
||||
console.error(
|
||||
"up&down: hash unmatch",
|
||||
file.name,
|
||||
upHash,
|
||||
downHash,
|
||||
upHash.length,
|
||||
downHash.length
|
||||
);
|
||||
} else {
|
||||
console.log("up&down: ok: hash match", file.name, upHash, downHash);
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
reader.onerror = err => reject(err);
|
||||
|
||||
reader.readAsText(file);
|
||||
});
|
||||
});
|
||||
}
|
63
client/libs/test/api_upload_test.js
Normal file
63
client/libs/test/api_upload_test.js
Normal file
|
@ -0,0 +1,63 @@
|
|||
import { FileUploader } from "../api_upload";
|
||||
import { list, del } from "../api_share";
|
||||
import { testLogin, testLogout } from "./api_auth_test";
|
||||
|
||||
function verify(fileName) {
|
||||
return list()
|
||||
.then(list => {
|
||||
if (list == null) {
|
||||
throw new Error("upload: list fail");
|
||||
}
|
||||
|
||||
// TODO: should verify file name
|
||||
const filterInfo = list.find(info => {
|
||||
return info.PathLocal.includes(fileName);
|
||||
});
|
||||
|
||||
if (filterInfo == null) {
|
||||
console.error(list);
|
||||
throw new Error("upload: file name not found");
|
||||
} else {
|
||||
return filterInfo.Id;
|
||||
}
|
||||
})
|
||||
.then(shareId => {
|
||||
console.log("upload api: ok");
|
||||
del(shareId);
|
||||
})
|
||||
.then(testLogout)
|
||||
.catch(err => {
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
export function testUpload(file) {
|
||||
const onStart = () => true;
|
||||
const onProgress = () => true;
|
||||
const onFinish = () => true;
|
||||
const onError = err => {
|
||||
throw new Error(JSON.stringify(err));
|
||||
};
|
||||
const uploader = new FileUploader(onStart, onProgress, onFinish, onError);
|
||||
|
||||
return uploader.uploadFile(file).catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
export function testUploadOneFile(file, fileName) {
|
||||
const onStart = () => true;
|
||||
const onProgress = () => true;
|
||||
const onFinish = () => true;
|
||||
const onError = err => {
|
||||
throw new Error(JSON.stringify(err));
|
||||
};
|
||||
const uploader = new FileUploader(onStart, onProgress, onFinish, onError);
|
||||
|
||||
return testLogin()
|
||||
.then(() => uploader.uploadFile(file))
|
||||
.then(() => verify(fileName))
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
5
client/libs/utils.js
Normal file
5
client/libs/utils.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
export function makePostBody(paramMap) {
|
||||
return Object.keys(paramMap)
|
||||
.map(key => `${key}=${paramMap[key]}`)
|
||||
.join("&");
|
||||
}
|
150
client/panels/admin.jsx
Normal file
150
client/panels/admin.jsx
Normal file
|
@ -0,0 +1,150 @@
|
|||
import axios from "axios";
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
import { config } from "../config";
|
||||
import { addLocalFiles, list } from "../libs/api_share";
|
||||
import { login, logout } from "../libs/api_auth";
|
||||
import { FilePane } from "../components/composite/file_pane";
|
||||
import { InfoBar } from "../components/composite/info_bar";
|
||||
import { Log } from "../components/composite/log";
|
||||
|
||||
function getWidth() {
|
||||
if (window.innerWidth >= window.innerHeight) {
|
||||
return `${Math.floor(
|
||||
window.innerWidth * 0.95 / config.rootSize / config.colWidth
|
||||
) * config.colWidth}rem`;
|
||||
}
|
||||
return "auto";
|
||||
}
|
||||
|
||||
const styleLogContainer = {
|
||||
paddingTop: "1rem",
|
||||
textAlign: "center",
|
||||
height: "2rem",
|
||||
overflowX: "hidden" // TODO: should no hidden
|
||||
};
|
||||
|
||||
const styleLogContent = {
|
||||
color: "#333",
|
||||
fontSize: "0.875rem",
|
||||
opacity: 0.6,
|
||||
backgroundColor: "#fff",
|
||||
borderRadius: "1rem",
|
||||
whiteSpace: "nowrap"
|
||||
};
|
||||
|
||||
class AdminPanel extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isLogin: false,
|
||||
filterName: "",
|
||||
serverAddr: `${window.location.protocol}//${window.location.hostname}:${
|
||||
window.location.port
|
||||
}`,
|
||||
width: getWidth()
|
||||
};
|
||||
this.log = {
|
||||
ok: msg => console.log(msg),
|
||||
warning: msg => console.log(msg),
|
||||
info: msg => console.log(msg),
|
||||
error: msg => console.log(msg),
|
||||
start: msg => console.log(msg),
|
||||
end: msg => console.log(msg)
|
||||
};
|
||||
this.logComponent = <Log ref={this.assignLog} styleLog={styleLogContent} />;
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
list().then(infos => {
|
||||
if (infos != null) {
|
||||
this.setState({ isLogin: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setWidth = () => {
|
||||
this.setState({ width: getWidth() });
|
||||
};
|
||||
|
||||
// componentDidMount() {
|
||||
// window.addEventListener("resize", this.setWidth);
|
||||
// }
|
||||
|
||||
// componentWillUnmount() {
|
||||
// window.removeEventListener("resize", this.setWidth);
|
||||
// }
|
||||
|
||||
onLogin = (serverAddr, adminId, adminPwd) => {
|
||||
login(serverAddr, adminId, adminPwd).then(ok => {
|
||||
if (ok === true) {
|
||||
this.setState({ isLogin: true });
|
||||
} else {
|
||||
this.log.error("Fail to login");
|
||||
this.setState({ isLogin: false });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onLogout = serverAddr => {
|
||||
logout(serverAddr).then(ok => {
|
||||
if (ok === false) {
|
||||
this.log.error("Fail to log out");
|
||||
} else {
|
||||
this.log.ok("You are logged out");
|
||||
}
|
||||
this.setState({ isLogin: false });
|
||||
});
|
||||
};
|
||||
|
||||
onSearch = fileName => {
|
||||
this.setState({ filterName: fileName });
|
||||
};
|
||||
|
||||
assignLog = logRef => {
|
||||
this.log = logRef;
|
||||
this.log.info(
|
||||
<span>
|
||||
Know more about <a href="https://github.com/ihexxa">Quickshare</a>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const width = this.state.width;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<InfoBar
|
||||
compact={width === "auto"}
|
||||
width={width}
|
||||
serverAddr={this.state.serverAddr}
|
||||
isLogin={this.state.isLogin}
|
||||
onLogin={this.onLogin}
|
||||
onLogout={this.onLogout}
|
||||
onAddLocalFiles={addLocalFiles}
|
||||
onSearch={this.onSearch}
|
||||
onOk={this.log.ok}
|
||||
onError={this.log.error}
|
||||
>
|
||||
<div style={{ ...styleLogContainer, width }}>{this.logComponent}</div>
|
||||
</InfoBar>
|
||||
{this.state.isLogin ? (
|
||||
<FilePane
|
||||
width={width}
|
||||
colWidth={config.colWidth}
|
||||
onList={list}
|
||||
onOk={this.log.ok}
|
||||
onError={this.log.error}
|
||||
filterName={this.state.filterName}
|
||||
/>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ReactDOM.render(<AdminPanel />, document.getElementById("app"));
|
4
client/tests/enzyme_setup.js
Normal file
4
client/tests/enzyme_setup.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
import { configure } from "enzyme";
|
||||
import Adapter from "enzyme-adapter-react-15";
|
||||
|
||||
configure({ adapter: new Adapter() });
|
73
client/tests/test_helper.js
Normal file
73
client/tests/test_helper.js
Normal file
|
@ -0,0 +1,73 @@
|
|||
// function should be called after async operation is finished
|
||||
export function execFuncs(instance, execs) {
|
||||
// instance: enzyme mounted component
|
||||
// const execs = [
|
||||
// {
|
||||
// func: "componentWillMount",
|
||||
// args: []
|
||||
// }
|
||||
// ];
|
||||
return execs.reduce((prePromise, nextFunc) => {
|
||||
return prePromise.then(() => instance[nextFunc.func](...nextFunc.args));
|
||||
}, Promise.resolve());
|
||||
}
|
||||
|
||||
export function execsToStr(execs) {
|
||||
// const execs = [
|
||||
// {
|
||||
// func: "componentWillMount",
|
||||
// args: []
|
||||
// }
|
||||
// ];
|
||||
const execList = execs.map(
|
||||
funcInfo => `${funcInfo.func}(${funcInfo.args.join(", ")})`
|
||||
);
|
||||
|
||||
return execList.join(", ");
|
||||
}
|
||||
|
||||
export function getDesc(componentName, testCase) {
|
||||
// const testCase = {
|
||||
// execs: [
|
||||
// {
|
||||
// func: "onAddLocalFiles",
|
||||
// args: []
|
||||
// }
|
||||
// ],
|
||||
// state: {
|
||||
// filterFileName: ""
|
||||
// },
|
||||
// calls: [
|
||||
// {
|
||||
// func: "onAddLocalFiles",
|
||||
// count: 1
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
return `${componentName} should satisfy following by exec ${execsToStr(
|
||||
testCase.execs
|
||||
)}
|
||||
state=${JSON.stringify(testCase.state)}
|
||||
calls=${JSON.stringify(testCase.calls)} `;
|
||||
}
|
||||
|
||||
export function verifyCalls(calls, stubs) {
|
||||
// const calls: [
|
||||
// {
|
||||
// func: "funcName",
|
||||
// count: 1
|
||||
// }
|
||||
// ];
|
||||
// const stubs = {
|
||||
// funcName: jest.fn(),
|
||||
// };
|
||||
let err = null;
|
||||
calls.forEach(called => {
|
||||
if (stubs[called.func].mock.calls.length != called.count) {
|
||||
err = `InfoBar: ${called.func} should be called ${called.count} but ${
|
||||
stubs[called.func].mock.calls.length
|
||||
}`;
|
||||
}
|
||||
});
|
||||
return err;
|
||||
}
|
60
client/webpack.config.common.js
Normal file
60
client/webpack.config.common.js
Normal file
|
@ -0,0 +1,60 @@
|
|||
const webpack = require("webpack");
|
||||
const CleanWebpackPlugin = require("clean-webpack-plugin");
|
||||
// const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
|
||||
const outputPath = `${__dirname}/../public/dist`;
|
||||
|
||||
module.exports = {
|
||||
context: __dirname,
|
||||
entry: {
|
||||
assets: ["axios", "immutable", "react", "react-dom"],
|
||||
admin: "./panels/admin"
|
||||
},
|
||||
output: {
|
||||
path: outputPath,
|
||||
filename: "[name].bundle.js"
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js|jsx$/,
|
||||
use: [
|
||||
{
|
||||
loader: "babel-loader",
|
||||
options: {
|
||||
presets: ["es2015", "react", "stage-2"]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: ["style-loader", "css-loader"]
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpg|gif)$/,
|
||||
use: [
|
||||
{
|
||||
loader: "file-loader",
|
||||
options: {}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
extensions: [".js", ".json", ".jsx", ".css"]
|
||||
},
|
||||
plugins: [
|
||||
new webpack.optimize.CommonsChunkPlugin({
|
||||
name: "assets",
|
||||
// filename: "vendor.js"
|
||||
// (Give the chunk a different name)
|
||||
minChunks: Infinity
|
||||
// (with more entries, this ensures that no other module
|
||||
// goes into the vendor chunk)
|
||||
}),
|
||||
// new HtmlWebpackPlugin(),
|
||||
new CleanWebpackPlugin([outputPath])
|
||||
]
|
||||
};
|
17
client/webpack.config.dev.js
Normal file
17
client/webpack.config.dev.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
const merge = require("webpack-merge");
|
||||
const common = require("./webpack.config.common.js");
|
||||
|
||||
module.exports = merge(common, {
|
||||
entry: {
|
||||
api_test: "./libs/test/api_test"
|
||||
},
|
||||
devtool: "inline-source-map",
|
||||
devServer: {
|
||||
contentBase: "./dist"
|
||||
},
|
||||
watchOptions: {
|
||||
aggregateTimeout: 1000,
|
||||
poll: 1000,
|
||||
ignored: /node_modules/
|
||||
}
|
||||
});
|
16
client/webpack.config.prod.js
Normal file
16
client/webpack.config.prod.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
const common = require("./webpack.config.common.js");
|
||||
const merge = require("webpack-merge");
|
||||
const UglifyJS = require("uglifyjs-webpack-plugin");
|
||||
const webpack = require("webpack");
|
||||
|
||||
module.exports = merge(common, {
|
||||
devtool: "source-map",
|
||||
plugins: [
|
||||
new UglifyJS({
|
||||
sourceMap: true
|
||||
}),
|
||||
new webpack.DefinePlugin({
|
||||
"process.env.NODE_ENV": JSON.stringify("production")
|
||||
})
|
||||
]
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue