mirror of
https://github.com/filecoin-project/slate.git
synced 2024-11-23 22:12:19 +03:00
file uploads: no longer users a library, just raw XHR, adds file loading to the UI
This commit is contained in:
parent
e751a2cb48
commit
418d26bf8f
@ -1,151 +0,0 @@
|
||||
// NOTE(jim)
|
||||
// https://github.com/samundrak/fetch-progress (thank you!)
|
||||
|
||||
let tick = 1;
|
||||
let maxTick = 65535;
|
||||
let resolution = 4;
|
||||
let inc = function () {
|
||||
tick = (tick + 1) & maxTick;
|
||||
};
|
||||
|
||||
let timer = setInterval(inc, (1000 / resolution) | 0);
|
||||
if (timer.unref) timer.unref();
|
||||
|
||||
function speedometer(seconds) {
|
||||
let size = resolution * (seconds || 5);
|
||||
let buffer = [0];
|
||||
let pointer = 1;
|
||||
let last = (tick - 1) & maxTick;
|
||||
|
||||
return function (delta) {
|
||||
let dist = (tick - last) & maxTick;
|
||||
if (dist > size) dist = size;
|
||||
last = tick;
|
||||
|
||||
while (dist--) {
|
||||
if (pointer === size) pointer = 0;
|
||||
buffer[pointer] = buffer[pointer === 0 ? size - 1 : pointer - 1];
|
||||
pointer++;
|
||||
}
|
||||
|
||||
if (delta) buffer[pointer - 1] += delta;
|
||||
|
||||
let top = buffer[pointer - 1];
|
||||
let btm = buffer.length < size ? 0 : buffer[pointer === size ? 0 : pointer];
|
||||
|
||||
return buffer.length < resolution ? top : ((top - btm) * resolution) / buffer.length;
|
||||
};
|
||||
}
|
||||
|
||||
class Progress {
|
||||
constructor(length, emitDelay = 1000) {
|
||||
this.length = parseInt(length, 10) || 0;
|
||||
this.transferred = 0;
|
||||
this.speed = 0;
|
||||
this.streamSpeed = speedometer(this.speed || 5000);
|
||||
this.initial = false;
|
||||
this.emitDelay = emitDelay;
|
||||
this.eventStart = 0;
|
||||
this.percentage = 0;
|
||||
}
|
||||
|
||||
getRemainingBytes() {
|
||||
return parseInt(this.length, 10) - parseInt(this.transferred, 10);
|
||||
}
|
||||
|
||||
getEta() {
|
||||
return this.length >= this.transferred ? (this.getRemainingBytes() / this.speed) * 1000000000 : 0;
|
||||
}
|
||||
|
||||
flow(chunk, onProgress) {
|
||||
const chunkLength = chunk.length;
|
||||
this.transferred += chunkLength;
|
||||
this.speed = this.streamSpeed(chunkLength);
|
||||
this.percentage = Math.round((this.transferred / this.length) * 100);
|
||||
if (!this.initial) {
|
||||
this.eventStart = Date.now();
|
||||
this.initial = true;
|
||||
}
|
||||
if (this.length >= this.transferred || Date.now() - this.eventStart > this.emitDelay) {
|
||||
this.eventStart = Date.now();
|
||||
|
||||
const progress = {
|
||||
total: this.length,
|
||||
transferred: this.transferred,
|
||||
speed: this.speed,
|
||||
eta: this.getEta(),
|
||||
};
|
||||
if (this.length) {
|
||||
progress.remaining = this.getRemainingBytes();
|
||||
progress.percentage = this.percentage;
|
||||
}
|
||||
onProgress(progress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function isFetchProgressSupported() {
|
||||
return typeof Response !== "undefined" && typeof ReadableStream !== "undefined";
|
||||
}
|
||||
|
||||
export function progress({
|
||||
defaultSize = 0,
|
||||
emitDelay = 10,
|
||||
onProgress = () => null,
|
||||
onComplete = () => null,
|
||||
onError = () => null,
|
||||
}) {
|
||||
return function FetchProgress(response) {
|
||||
if (!isFetchProgressSupported()) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const { body, headers } = response;
|
||||
const contentLength = headers.get("content-length") || defaultSize;
|
||||
const progress = new Progress(contentLength, emitDelay);
|
||||
const reader = body.getReader();
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
function push() {
|
||||
reader
|
||||
.read()
|
||||
.then(({ done, value }) => {
|
||||
if (done) {
|
||||
onComplete({});
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
if (value) {
|
||||
progress.flow(value, onProgress);
|
||||
}
|
||||
controller.enqueue(value);
|
||||
push();
|
||||
})
|
||||
.catch((err) => {
|
||||
onError(err);
|
||||
});
|
||||
}
|
||||
|
||||
push();
|
||||
},
|
||||
});
|
||||
return new Response(stream, { headers });
|
||||
};
|
||||
}
|
||||
|
||||
export default (url, options, onProgress) =>
|
||||
new Promise((resolve, reject) =>
|
||||
fetch(url, options)
|
||||
.then(
|
||||
progress({
|
||||
onProgress,
|
||||
onError: (err) => {
|
||||
reject(err);
|
||||
},
|
||||
})
|
||||
)
|
||||
.then((data) => {
|
||||
console.log({ upload: data });
|
||||
resolve(data);
|
||||
})
|
||||
);
|
@ -5,6 +5,7 @@ import * as State from "~/common/state";
|
||||
import * as Credentials from "~/common/credentials";
|
||||
import * as Validations from "~/common/validations";
|
||||
import * as System from "~/components/system";
|
||||
import * as Window from "~/common/window";
|
||||
|
||||
// NOTE(jim):
|
||||
// Scenes each have an ID and can be navigated to with _handleAction
|
||||
@ -39,7 +40,6 @@ import ApplicationHeader from "~/components/core/ApplicationHeader";
|
||||
import ApplicationLayout from "~/components/core/ApplicationLayout";
|
||||
import WebsitePrototypeWrapper from "~/components/core/WebsitePrototypeWrapper";
|
||||
import Cookies from "universal-cookie";
|
||||
import progressFetch from "~/common/fetch-utilities";
|
||||
|
||||
const cookies = new Cookies();
|
||||
|
||||
@ -102,38 +102,63 @@ export default class ApplicationPage extends React.Component {
|
||||
};
|
||||
|
||||
_handleSetFile = async ({ file, slate }) => {
|
||||
this.setState({ fileLoading: true });
|
||||
let formData = new FormData();
|
||||
formData.append("data", file);
|
||||
console.log({ file });
|
||||
|
||||
let data = new FormData();
|
||||
data.append("data", file);
|
||||
|
||||
const options = {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
this.setState({
|
||||
fileLoading: {
|
||||
[file.lastModified]: {
|
||||
name: file.name,
|
||||
loaded: 0,
|
||||
total: file.size,
|
||||
},
|
||||
},
|
||||
body: data,
|
||||
};
|
||||
|
||||
const response = await progressFetch(`/api/data/${file.name}`, options, (p) => {
|
||||
console.log(p);
|
||||
});
|
||||
|
||||
const json = await response.json();
|
||||
const upload = (path) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const XHR = new XMLHttpRequest();
|
||||
XHR.open("post", path, true);
|
||||
XHR.onerror = (event) => {
|
||||
console.log(event);
|
||||
};
|
||||
XHR.onprogress = (event) => {
|
||||
console.log("FILE UPLOAD PROGRESS", event);
|
||||
this.setState({
|
||||
fileLoading: {
|
||||
...this.state.fileLoading,
|
||||
[file.lastModified]: {
|
||||
name: file.name,
|
||||
loaded: event.loaded,
|
||||
total: event.total,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
XHR.onloadend = (event) => {
|
||||
console.log("FILE UPLOAD END", event);
|
||||
resolve(JSON.parse(event.target.response));
|
||||
};
|
||||
XHR.send(formData);
|
||||
});
|
||||
|
||||
const json = await upload(`/api/data/${file.name}`);
|
||||
console.log(json);
|
||||
|
||||
if (!json) {
|
||||
this.setState({ sidebar: null, fileLoading: false });
|
||||
return;
|
||||
this.setState({ sidebar: null, fileLoading: null });
|
||||
return { error: "NO_RESPONSE" };
|
||||
}
|
||||
|
||||
if (json.error) {
|
||||
this.setState({ sidebar: null, fileLoading: false });
|
||||
return;
|
||||
this.setState({ sidebar: null, fileLoading: null });
|
||||
return json;
|
||||
}
|
||||
|
||||
if (!json.data) {
|
||||
this.setState({ sidebar: null, fileLoading: false });
|
||||
return;
|
||||
this.setState({ sidebar: null, fileLoading: null });
|
||||
return json;
|
||||
}
|
||||
|
||||
if (json && slate) {
|
||||
@ -152,9 +177,7 @@ export default class ApplicationPage extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
await this.rehydrate();
|
||||
|
||||
this.setState({ sidebar: null, fileLoading: false });
|
||||
return json;
|
||||
};
|
||||
|
||||
_handleDragEnter = (e) => {
|
||||
@ -196,13 +219,12 @@ export default class ApplicationPage extends React.Component {
|
||||
slate = { ...current.target, id: current.target.slateId };
|
||||
}
|
||||
|
||||
let isUploading = false;
|
||||
if (e.dataTransfer.items) {
|
||||
for (var i = 0; i < e.dataTransfer.items.length; i++) {
|
||||
if (e.dataTransfer.items[i].kind === "file") {
|
||||
var file = e.dataTransfer.items[i].getAsFile();
|
||||
|
||||
console.log(file);
|
||||
|
||||
if (Validations.isFileTypeAllowed(file.type)) {
|
||||
this._handleAction({
|
||||
type: "SIDEBAR",
|
||||
@ -210,6 +232,7 @@ export default class ApplicationPage extends React.Component {
|
||||
data: slate,
|
||||
});
|
||||
|
||||
isUploading = true;
|
||||
await this._handleSetFile({ file, slate });
|
||||
}
|
||||
|
||||
@ -218,10 +241,15 @@ export default class ApplicationPage extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({ fileLoading: false });
|
||||
if (!isUploading) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.rehydrate();
|
||||
this.setState({ sidebar: null, fileLoading: null });
|
||||
};
|
||||
|
||||
rehydrate = async () => {
|
||||
rehydrate = async (options) => {
|
||||
const response = await Actions.hydrateAuthenticatedUser();
|
||||
|
||||
console.log("REHYDRATION CALL", response);
|
||||
@ -230,10 +258,16 @@ export default class ApplicationPage extends React.Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
const updates = {
|
||||
viewer: State.getInitialState(response.data),
|
||||
selected: State.getSelectedState(response.data),
|
||||
});
|
||||
};
|
||||
|
||||
if (options && options.resetFiles) {
|
||||
updates.fileLoading = null;
|
||||
}
|
||||
|
||||
this.setState(updates);
|
||||
|
||||
return { rehydrated: true };
|
||||
};
|
||||
|
@ -30,11 +30,7 @@ const STYLES_DATA_METER = css`
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
background-color: ${Constants.system.brand};
|
||||
background-image: linear-gradient(
|
||||
315deg,
|
||||
${Constants.system.brand} 0%,
|
||||
#009ffd 74%
|
||||
);
|
||||
background-image: linear-gradient(315deg, ${Constants.system.brand} 0%, #009ffd 74%);
|
||||
`;
|
||||
|
||||
const STYLES_ROW = css`
|
||||
@ -75,35 +71,14 @@ const STYLES_TITLE = css`
|
||||
margin-bottom: 4px;
|
||||
`;
|
||||
|
||||
const STYLES_HREF = css`
|
||||
font-family: ${Constants.font.semiBold};
|
||||
font-weight: 400;
|
||||
cursor: pointer;
|
||||
transition: 200ms ease color;
|
||||
|
||||
:hover {
|
||||
color: ${Constants.system.brand};
|
||||
}
|
||||
`;
|
||||
|
||||
export default (props) => {
|
||||
const percentage = props.stats.bytes / props.stats.maximumBytes;
|
||||
export const DataMeterBar = (props) => {
|
||||
const percentage = props.bytes / props.maximumBytes;
|
||||
|
||||
return (
|
||||
<div css={STYLES_CONTAINER} style={props.style}>
|
||||
<System.P style={{ fontSize: 12 }}>
|
||||
<strong css={STYLES_TITLE}>Usage</strong>
|
||||
Slate users get 1GB of IPFS storage from Textile. In the future you can
|
||||
extend this with your own plugins using our SDK.
|
||||
<br />
|
||||
<br />
|
||||
</System.P>
|
||||
|
||||
<React.Fragment>
|
||||
<div css={STYLES_STATS_ROW}>
|
||||
<div css={STYLES_LEFT}>{Strings.bytesToSize(props.stats.bytes)}</div>
|
||||
<div css={STYLES_RIGHT}>
|
||||
{Strings.bytesToSize(props.stats.maximumBytes)}
|
||||
</div>
|
||||
<div css={STYLES_LEFT}>{Strings.bytesToSize(props.bytes)}</div>
|
||||
<div css={STYLES_RIGHT}>{Strings.bytesToSize(props.maximumBytes)}</div>
|
||||
</div>
|
||||
|
||||
<div css={STYLES_ROW}>
|
||||
@ -112,11 +87,24 @@ export default (props) => {
|
||||
</div>
|
||||
|
||||
<div css={STYLES_DATA} style={{ marginTop: 4 }}>
|
||||
<div
|
||||
css={STYLES_DATA_METER}
|
||||
style={{ width: `${percentage * 100}%` }}
|
||||
/>
|
||||
<div css={STYLES_DATA_METER} style={{ width: `${percentage * 100}%` }} />
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default (props) => {
|
||||
return (
|
||||
<div css={STYLES_CONTAINER} style={props.style}>
|
||||
<System.P style={{ fontSize: 12 }}>
|
||||
<strong css={STYLES_TITLE}>Usage</strong>
|
||||
Slate users get 1GB of IPFS storage from Textile. In the future you can extend this with your own plugins using
|
||||
our SDK.
|
||||
<br />
|
||||
<br />
|
||||
</System.P>
|
||||
|
||||
<DataMeterBar bytes={props.stats.bytes} maximumBytes={props.stats.maximumBytes} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -4,6 +4,7 @@ import * as System from "~/components/system";
|
||||
import * as Validations from "~/common/validations";
|
||||
|
||||
import { css } from "@emotion/react";
|
||||
import { DataMeterBar } from "~/components/core/DataMeter";
|
||||
|
||||
const STYLES_FILE_HIDDEN = css`
|
||||
height: 1px;
|
||||
@ -42,6 +43,14 @@ const STYLES_IMAGE_PREVIEW = css`
|
||||
margin-top: 48px;
|
||||
`;
|
||||
|
||||
const STYLES_STRONG = css`
|
||||
display: block;
|
||||
margin: 16px 0 4px 0;
|
||||
font-weight: 400;
|
||||
font-family: ${Constants.font.medium};
|
||||
font-size: 0.8rem;
|
||||
`;
|
||||
|
||||
export default class SidebarAddFileToBucket extends React.Component {
|
||||
_handleUpload = async (e) => {
|
||||
e.persist();
|
||||
@ -64,9 +73,13 @@ export default class SidebarAddFileToBucket extends React.Component {
|
||||
file,
|
||||
slate: this.props.data && this.props.data.slateId ? { id: this.props.data.slateId } : null,
|
||||
});
|
||||
|
||||
await this.props.onRehydrate({ resetFiles: true });
|
||||
};
|
||||
|
||||
render() {
|
||||
console.log(this.props.fileLoading);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<System.P style={{ fontFamily: Constants.font.semiBold }}>Upload data</System.P>
|
||||
@ -83,20 +96,28 @@ export default class SidebarAddFileToBucket extends React.Component {
|
||||
type="label"
|
||||
htmlFor="file"
|
||||
style={{ marginTop: 24 }}
|
||||
loading={this.props.fileLoading}
|
||||
>
|
||||
loading={!!this.props.fileLoading}>
|
||||
Add file
|
||||
</System.ButtonPrimary>
|
||||
|
||||
{!this.props.fileLoading ? (
|
||||
<System.ButtonSecondary
|
||||
full
|
||||
style={{ marginTop: 16 }}
|
||||
onClick={this.props.onCancel}
|
||||
>
|
||||
<System.ButtonSecondary full style={{ marginTop: 16 }} onClick={this.props.onCancel}>
|
||||
Cancel
|
||||
</System.ButtonSecondary>
|
||||
) : null}
|
||||
|
||||
{this.props.fileLoading
|
||||
? Object.keys(this.props.fileLoading).map((timestamp) => {
|
||||
const p = this.props.fileLoading[timestamp];
|
||||
console.log({ p });
|
||||
return (
|
||||
<React.Fragment key={timestamp}>
|
||||
<strong css={STYLES_STRONG}>{p.name}</strong>
|
||||
<DataMeterBar bytes={p.loaded} maximumBytes={p.total} />
|
||||
</React.Fragment>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user