Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a File Uploader Widget #488

Merged
merged 44 commits into from
Dec 9, 2019
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
2c84231
add new FileUploader widget
Oct 9, 2019
247ed6d
add new FileUploader widget
Oct 9, 2019
6caacc1
add new FileUploader widget
Oct 9, 2019
da46d7a
save chunks solution
Oct 18, 2019
eb8f340
save chunks solution
Oct 18, 2019
0b4c8eb
new server.maxUploadSize config
Oct 21, 2019
4f69872
solve lint problems
Oct 21, 2019
62345ad
add documentation
Oct 22, 2019
6a20616
fix and add tests
Oct 22, 2019
f0fc5bb
Merge branch 'develop' into issue120/file-uploader
Oct 22, 2019
48f11e1
add missing type
Oct 22, 2019
ee7dca5
improve code
Oct 24, 2019
1445872
add FileManager_test.py and fix some code problems
Oct 25, 2019
a284eae
Merge branch 'develop' into issue120/file-uploader
Oct 25, 2019
0ac4202
fix lint problem
Oct 25, 2019
5283546
add new error message format in file_uploader widget
Oct 28, 2019
e0c31c0
fix lint problem
Oct 28, 2019
ffb93eb
change comunication logic
Nov 1, 2019
e4b60b4
remove old imports
Nov 1, 2019
0434c54
use memory instead of file storage
Nov 4, 2019
f91b18a
make headers
Nov 4, 2019
0da9d5e
Merge branch 'develop' into issue120/file-uploader
Nov 4, 2019
1975c8d
fix _server_max_upload_size
Nov 4, 2019
5fd8c85
fix pytest
Nov 4, 2019
b4a71e7
fix decimal values on python 2
Nov 4, 2019
20c2763
change proto names and improve code
Nov 8, 2019
92b35a3
add comments and improve code
Nov 8, 2019
cfad578
fix file type [*] to None and others improvements
Nov 11, 2019
4ed4f8c
fix tests
Nov 11, 2019
cbc4d61
Merge branch 'develop' into issue120/file-uploader
Nov 12, 2019
56b1063
improved code quality
Nov 15, 2019
718b190
Merge branch 'develop' into issue120/file-uploader
Nov 15, 2019
0558c7a
fix Element.proto order
Nov 15, 2019
96cb276
add RuntimeError if chunk was already processed
Nov 15, 2019
09983d8
Merge branch 'develop' into issue120/file-uploader
Nov 21, 2019
5fac828
improve proto comments
Nov 21, 2019
14c005b
Add ./lib/streamlit to blacklist when in dev mode
tvst Nov 26, 2019
2959ee0
minor fixes
Nov 27, 2019
ef01b9d
Merge branch 'develop' into issue120/file-uploader
Nov 27, 2019
d0546a5
force to parse config for tests
Nov 27, 2019
cd5f59d
Merge branch 'develop' into issue120/file-uploader
Dec 4, 2019
7ea7aaf
add code to set __wrapped__ in python 2
Dec 6, 2019
3053061
add comments
Dec 6, 2019
ac11e4f
merge develop
Dec 9, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
42 changes: 42 additions & 0 deletions examples/file_uploader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
dcaminos marked this conversation as resolved.
Show resolved Hide resolved
# Copyright 2018-2019 Streamlit Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Example of file uploader."""

import streamlit as st
import csv

st.title("Streamlit file_uploader widget")

file_csv = st.file_uploader("Upload a CSV file", type=([".csv"]))

if file_csv:

file_csv_bytes = st.file_reader(file_csv)
data_csv = file_csv_bytes.decode('utf-8').splitlines()
reader = csv.reader(data_csv, quoting=csv.QUOTE_MINIMAL)

results = []
for row in reader: # each row is a list
results.append(row)

st.dataframe(results)


file_png = st.file_uploader("Upload a PNG image", type=([".png"]))

if file_png:
file_png_bytes = st.file_reader(file_png)
st.image(file_png_bytes)
16 changes: 16 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {
IBackMsg,
SessionState,
Initialize,
FileUploadStatus,
} from "autogen/proto"

import { RERUN_PROMPT_MODAL_DIALOG } from "lib/baseconsts"
Expand Down Expand Up @@ -220,6 +221,7 @@ class App extends PureComponent<Props, State> {
handleMessage(msgProto: ForwardMsg): void {
// We don't have an immutableProto here, so we can't use
// the dispatchOneOf helper

const dispatchProto = (obj: any, name: string, funcs: any): any => {
const whichOne = obj[name]
if (whichOne in funcs) {
Expand Down Expand Up @@ -259,6 +261,20 @@ class App extends PureComponent<Props, State> {
}
this.openDialog(newDialog)
},
fileUploadStatus: (fileUploadStatus: FileUploadStatus) => {
dcaminos marked this conversation as resolved.
Show resolved Hide resolved
if (
fileUploadStatus.state ===
FileUploadStatus.FileUploadState.FINISHED ||
fileUploadStatus.state === FileUploadStatus.FileUploadState.DELETED
) {
// FINISHED TO UPLOAD
dcaminos marked this conversation as resolved.
Show resolved Hide resolved
this.widgetMgr.setStringValue(
dcaminos marked this conversation as resolved.
Show resolved Hide resolved
fileUploadStatus.id,
fileUploadStatus.fileId,
{ fromUi: true }
dcaminos marked this conversation as resolved.
Show resolved Hide resolved
)
}
},
})
} catch (err) {
logError(err)
Expand Down
12 changes: 12 additions & 0 deletions frontend/src/components/core/Block/Block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ const Progress = React.lazy(() => import("components/elements/Progress/"))
const Radio = React.lazy(() => import("components/widgets/Radio/"))
const Selectbox = React.lazy(() => import("components/widgets/Selectbox/"))
const Slider = React.lazy(() => import("components/widgets/Slider/"))
const FileUploader = React.lazy(() =>
import("components/widgets/FileUploader/")
)
const TextArea = React.lazy(() => import("components/widgets/TextArea/"))
const TextInput = React.lazy(() => import("components/widgets/TextInput/"))
const TimeInput = React.lazy(() => import("components/widgets/TimeInput/"))
Expand Down Expand Up @@ -303,6 +306,15 @@ class Block extends PureComponent<Props> {
{...widgetProps}
/>
),
fileUploader: (el: SimpleElement) => (
<FileUploader
key={el.get("id")}
element={el}
width={width}
widgetStateManager={widgetProps.widgetMgr}
disabled={widgetProps.disabled}
/>
),
textArea: (el: SimpleElement) => (
<TextArea
key={el.get("id")}
Expand Down
68 changes: 68 additions & 0 deletions frontend/src/components/widgets/FileUploader/FileUploader.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* Copyright 2018-2019 Streamlit Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

@import "src/assets/css/variables";

.file-uploader button:focus {
outline: 0;
}

.file-uploader-error {
outline: 0;
line-height: 1.6;
font-weight: normal;
font-size: 1rem;
font-family: "IBM Plex Sans", sans-serif;

height: 10rem;
-webkit-box-pack: center;
justify-content: center;
width: 100%;
padding-left: 1rem;
padding-bottom: 32px;
padding-right: 1rem;
padding-top: 1rem;
outline: 0px;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are all these -webkit properties needed? It's best to avoid browser-specific code.

flex-direction: column;
display: flex;
box-sizing: border-box;
border-width: 2px;
border-color: rgb(128, 132, 149);
background-color: rgb(240, 242, 246);
-webkit-box-align: center;
align-items: center;
}

.file-uploader-error__text {
line-height: 1.6;
font-weight: normal;
font-size: 1rem;
font-family: "IBM Plex Sans", sans-serif;
box-sizing: border-box;
box-sizing: border-box;
display: block;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
flex-direction: column;
color: $red;
}

.file-uploader-error__button {
margin-top: 2rem;
color: $red;
}
158 changes: 158 additions & 0 deletions frontend/src/components/widgets/FileUploader/FileUploader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/**
* @license
* Copyright 2018-2019 Streamlit Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import React from "react"
import { Map as ImmutableMap } from "immutable"
import { WidgetStateManager } from "lib/WidgetStateManager"
import { FileUploader as FileUploaderBaseui } from "baseui/file-uploader"
import { fileUploaderOverrides } from "lib/widgetTheme"
import "./FileUploader.scss"
import { Button } from "reactstrap"

interface Props {
disabled: boolean
element: ImmutableMap<string, any>
widgetStateManager: WidgetStateManager
width: number
}

interface State {
status: "READY" | "READING" | "UPLOADING"
errorMessage?: string
}

class FileUploader extends React.PureComponent<Props, State> {
public constructor(props: Props) {
super(props)
this.state = {
status: "READY",
}
}

private handleFileRead = (
ev: ProgressEvent<FileReader>,
file: File
): any => {
dcaminos marked this conversation as resolved.
Show resolved Hide resolved
if (ev.target !== null) {
if (ev.target.result instanceof ArrayBuffer) {
dcaminos marked this conversation as resolved.
Show resolved Hide resolved
this.props.widgetStateManager.sendUploadFileMessage(
this.props.element.get("id"),
file.name,
file.lastModified,
new Uint8Array(ev.target.result)
)
}
}
this.setState({ status: "UPLOADING" })
}

private dropHandler = (
acceptedFiles: File[],
rejectedFiles: File[],
event: React.SyntheticEvent<HTMLElement>
): void => {
const { element } = this.props
const maxSize = element.get("maxUploadSize")

if (rejectedFiles.length > 0) {
this.setState({
status: "READY",
errorMessage: `${rejectedFiles[0].type} files are not allowed`,
})
return
}

this.setState({ status: "READING" })
acceptedFiles.forEach((file: File) => {
const fileSilzeMB = file.size / 1024 / 1024
dcaminos marked this conversation as resolved.
Show resolved Hide resolved
if (fileSilzeMB < maxSize) {
const fileReader = new FileReader()
fileReader.onloadend = (ev: ProgressEvent<FileReader>) =>
this.handleFileRead(ev, file)
fileReader.readAsArrayBuffer(file)
} else {
this.setState({
status: "READY",
errorMessage: `The max file size allowed is ${maxSize}MB`,
})
}
})
}

public componentDidUpdate(): void {
const uiValue = this.props.widgetStateManager.getStringValue(
dcaminos marked this conversation as resolved.
Show resolved Hide resolved
this.props.element.get("id")
)
if (uiValue !== undefined && this.state.status === "UPLOADING") {
this.setState({ status: "READY" })
}
}

closeErrorMessage = (): void => {
this.setState({ status: "READY", errorMessage: undefined })
}

renderErrorMessage = () => {
const { errorMessage } = this.state
return (
<div className="file-uploader-error">
<span className="file-uploader-error__text">{errorMessage}</span>
<Button
className="file-uploader-error__button"
outline
onClick={this.closeErrorMessage}
>
Ok
</Button>
</div>
)
}

public render = (): React.ReactNode => {
const { status, errorMessage } = this.state
const { element } = this.props
const accept: string[] = element.get("type").toArray()
const label: string = element.get("label")
return (
<div className="file-uploader">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The className should be Widget stFileUploader.

Without Widget, the <label> doesn't look right.

And all Streamlit elements need to be wrapped in a DOM element with class stElementName (see our style guide)

<label>{label}</label>
{errorMessage ? (
this.renderErrorMessage()
) : (
<FileUploaderBaseui
onDrop={this.dropHandler}
errorMessage={errorMessage}
accept={accept}
progressMessage={status !== "READY" ? status : undefined}
onRetry={() =>
this.setState({ status: "READY", errorMessage: undefined })
}
onCancel={() => {
this.setState({ status: "READY", errorMessage: undefined })
this.props.widgetStateManager.sendDeleteFileMessage(
this.props.element.get("id")
)
}}
overrides={fileUploaderOverrides}
/>
)}
</div>
)
}
}

export default FileUploader
18 changes: 18 additions & 0 deletions frontend/src/components/widgets/FileUploader/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* @license
* Copyright 2018-2019 Streamlit Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

export { default } from "./FileUploader"