) => mixed) => void,
45 | cancel: (fn: () => void) => void,
46 | show: () => mixed,
47 | close: () => mixed
48 | }
49 |
50 | class DWDatasetModal extends PureComponent {
51 | datasetSelector: DatasetSelector
52 |
53 | componentDidMount() {
54 | this.datasetSelector = new window.dataworldWidgets.DatasetSelector({
55 | client_id: CLIENT_ID,
56 | linkText: 'Select',
57 | resourceFilter: this.props.limitToProjects ? 'project' : undefined
58 | })
59 |
60 | this.datasetSelector.success(selectedDatasets => {
61 | const [dataset] = selectedDatasets
62 | if (dataset) {
63 | this.props.onSelect(dataset)
64 | } else {
65 | this.props.onCancel()
66 | }
67 | })
68 |
69 | this.datasetSelector.cancel(() => {
70 | this.props.onCancel()
71 | })
72 |
73 | // Shows the dataset selector
74 | this.datasetSelector.show()
75 | }
76 | componentWillUnmount() {
77 | this.datasetSelector.close()
78 | }
79 | render() {
80 | return null
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/components/CopyField.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | /* eslint no-unused-vars: ["warn", { "args": "after-used" }] */
3 | import React, { Component } from 'react'
4 | import {
5 | FormGroup,
6 | InputGroup,
7 | FormControl,
8 | Button,
9 | Tooltip,
10 | Overlay
11 | } from 'react-bootstrap'
12 | import { decorate, observable } from 'mobx'
13 | import { CopyToClipboard } from 'react-copy-to-clipboard'
14 | import { observer } from 'mobx-react'
15 |
16 | class CopyField extends Component<{ getValue: () => string }> {
17 | copied: null | string = null
18 | value: string = this.props.getValue()
19 |
20 | copyButton: ?Button
21 |
22 | handleCopy = (text: string, result: boolean) => {
23 | this.copied = result ? 'Copied!' : 'Ctrl+C to copy'
24 | this.input && this.input.select()
25 | setTimeout(() => (this.copied = null), 700)
26 | }
27 |
28 | input: ?HTMLInputElement
29 | handleInputRef = (r: HTMLInputElement) => {
30 | this.input = r
31 | if (r) {
32 | r.select()
33 | }
34 | }
35 |
36 | render() {
37 | const { value } = this
38 |
39 | const copiedTooltip = this.copied != null && (
40 |
41 | {this.copied}
42 |
43 | )
44 |
45 | return (
46 |
47 |
48 |
54 |
55 |
56 |
59 |
60 |
61 | {this.copied != null && (
62 | this.copyButton}
67 | >
68 | {copiedTooltip}
69 |
70 | )}
71 |
72 |
73 | )
74 | }
75 | }
76 |
77 | decorate(CopyField, {
78 | copied: observable,
79 | value: observable
80 | })
81 |
82 | export default observer(CopyField)
83 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chart-builder",
3 | "version": "0.1.0",
4 | "private": true,
5 | "author": "data.world (https://data.world/)",
6 | "homepage": "https://chart-builder.data.world",
7 | "repository": {
8 | "type": "git",
9 | "url": "github:datadotworld/chart-builder"
10 | },
11 | "license": "Apache-2.0",
12 | "dependencies": {
13 | "classnames": "2.2.6",
14 | "filepicker-js": "2.4.18",
15 | "filesize": "3.6.1",
16 | "flow-bin": "0.80.0",
17 | "history": "4.7.2",
18 | "little-loader": "0.2.0",
19 | "lz-string": "1.4.4",
20 | "mobx": "4.3.1",
21 | "mobx-react": "5.2.6",
22 | "mobx-state-tree": "3.2.4",
23 | "react": "16.4.2",
24 | "react-bootstrap": "0.32.3",
25 | "react-copy-to-clipboard": "5.0.1",
26 | "react-dom": "16.4.2",
27 | "react-draggable": "3.0.5",
28 | "react-measure": "3.0.0-rc.3",
29 | "react-monaco-editor": "0.14.1",
30 | "react-router-dom": "4.3.1",
31 | "react-scripts": "^5.0.1",
32 | "react-select": "2.0.0",
33 | "workbox-background-sync": "^6.6.0",
34 | "workbox-broadcast-update": "^6.6.0",
35 | "workbox-cacheable-response": "^6.5.4",
36 | "workbox-core": "^6.6.0",
37 | "workbox-expiration": "^6.6.0",
38 | "workbox-google-analytics": "^6.6.1",
39 | "workbox-navigation-preload": "^6.6.0",
40 | "workbox-precaching": "^6.6.0",
41 | "workbox-range-requests": "^6.6.0",
42 | "workbox-routing": "^6.6.0",
43 | "workbox-strategies": "^6.6.0",
44 | "workbox-streams": "^6.6.0",
45 | "vega": "5.28.0",
46 | "vega-lite": "5.18.0",
47 | "vega-tooltip": "0.34.0"
48 | },
49 | "scripts": {
50 | "start": "react-scripts start",
51 | "dev": "react-scripts start",
52 | "build": "react-scripts build",
53 | "test": "react-scripts test",
54 | "eject": "react-scripts eject",
55 | "flow": "flow",
56 | "precommit": "pretty-quick --staged"
57 | },
58 | "devDependencies": {
59 | "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
60 | "cypress": "13.8.1",
61 | "fetch-mock": "6.5.2",
62 | "http-server": "0.11.1",
63 | "husky": "0.14.3",
64 | "mobx-react-devtools": "6.0.3",
65 | "prettier": "1.14.2",
66 | "pretty-quick": "1.6.0",
67 | "react-test-renderer": "16.4.2"
68 | },
69 | "prettier": {
70 | "semi": false,
71 | "singleQuote": true
72 | },
73 | "browserslist": {
74 | "development": [
75 | "last 2 chrome versions",
76 | "last 2 firefox versions",
77 | "last 2 edge versions"
78 | ],
79 | "production": [
80 | ">1%",
81 | "last 4 versions",
82 | "Firefox ESR",
83 | "not ie < 11"
84 | ]
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Chart Builder | data.world
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
16 |
17 |
18 |
25 |
26 |
27 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/src/components/GlobalOptions.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | import React, { Component } from 'react'
3 | import { FormControl, FormGroup, InputGroup, HelpBlock } from 'react-bootstrap'
4 | import { observer, inject } from 'mobx-react'
5 | import classes from './GlobalOptions.module.css'
6 |
7 | import type { StoreType } from '../util/Store'
8 |
9 | type Props = {
10 | store: StoreType
11 | }
12 |
13 | function getVal(val: string): null | number {
14 | // allow backspace to reset to auto
15 | if (val === '') return null
16 | return parseInt(val, 10)
17 | }
18 |
19 | class GlobalOptions extends Component {
20 | render() {
21 | const { store } = this.props
22 | return (
23 |
24 |
25 |
26 |
27 | {
33 | const val = getVal(e.target.value)
34 | if (!isNaN(val)) {
35 | store.config.setDimensions(val, store.config.height)
36 | }
37 | }}
38 | />
39 | Width
40 |
41 |
42 |
53 |
54 |
55 | {
61 | const val = getVal(e.target.value)
62 | if (!isNaN(val)) {
63 | store.config.setDimensions(store.config.width, val)
64 | }
65 | }}
66 | />
67 | Height
68 |
69 |
70 |
71 |
72 | )
73 | }
74 | }
75 |
76 | export default inject('store')(observer(GlobalOptions))
77 |
--------------------------------------------------------------------------------
/src/service-worker.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-globals */
2 |
3 | // This service worker can be customized!
4 | // See https://developers.google.com/web/tools/workbox/modules
5 | // for the list of available Workbox modules, or add any other
6 | // code you'd like.
7 | // You can also remove this file if you'd prefer not to use a
8 | // service worker, and the Workbox build step will be skipped.
9 |
10 | import { clientsClaim } from 'workbox-core'
11 | import { ExpirationPlugin } from 'workbox-expiration'
12 | import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching'
13 | import { registerRoute } from 'workbox-routing'
14 | import { StaleWhileRevalidate } from 'workbox-strategies'
15 |
16 | clientsClaim()
17 |
18 | // Precache all of the assets generated by your build process.
19 | // Their URLs are injected into the manifest variable below.
20 | // This variable must be present somewhere in your service worker file,
21 | // even if you decide not to use precaching. See https://cra.link/PWA
22 | precacheAndRoute(self.__WB_MANIFEST)
23 |
24 | // Set up App Shell-style routing, so that all navigation requests
25 | // are fulfilled with your index.html shell. Learn more at
26 | // https://developers.google.com/web/fundamentals/architecture/app-shell
27 | const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$')
28 | registerRoute(
29 | // Return false to exempt requests from being fulfilled by index.html.
30 | ({ request, url }) => {
31 | // If this isn't a navigation, skip.
32 | if (request.mode !== 'navigate') {
33 | return false
34 | } // If this is a URL that starts with /_, skip.
35 |
36 | if (url.pathname.startsWith('/_')) {
37 | return false
38 | } // If this looks like a URL for a resource, because it contains // a file extension, skip.
39 |
40 | if (url.pathname.match(fileExtensionRegexp)) {
41 | return false
42 | } // Return true to signal that we want to use the handler.
43 |
44 | return true
45 | },
46 | createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html')
47 | )
48 |
49 | // An example runtime caching route for requests that aren't handled by the
50 | // precache, in this case same-origin .png requests like those from in public/
51 | registerRoute(
52 | // Add in any other file extensions or routing criteria as needed.
53 | ({ url }) =>
54 | url.origin === self.location.origin && url.pathname.endsWith('.png'), // Customize this strategy as needed, e.g., by changing to CacheFirst.
55 | new StaleWhileRevalidate({
56 | cacheName: 'images',
57 | plugins: [
58 | // Ensure that once this runtime cache reaches a maximum size the
59 | // least-recently used images are removed.
60 | new ExpirationPlugin({ maxEntries: 50 })
61 | ]
62 | })
63 | )
64 |
65 | // This allows the web app to trigger skipWaiting via
66 | // registration.waiting.postMessage({type: 'SKIP_WAITING'})
67 | self.addEventListener('message', event => {
68 | if (event.data && event.data.type === 'SKIP_WAITING') {
69 | self.skipWaiting()
70 | }
71 | })
72 |
73 | // Any other custom service worker logic can go here.
74 |
--------------------------------------------------------------------------------
/src/components/Editor.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | import React, { Component } from 'react'
3 | import debounce from 'lodash/debounce'
4 | import MonacoEditor from 'react-monaco-editor'
5 | import { Measure } from 'react-measure'
6 | import vegaLiteSchema from '../util/vega-lite-schema-v5.json'
7 | import classes from './Editor.module.css'
8 |
9 | const monacoJsonSchema = {
10 | uri: 'https://vega.github.io/schema/vega-lite/v5.json',
11 | schema: vegaLiteSchema,
12 | fileMatch: ['*']
13 | }
14 |
15 | const requireConfig = {
16 | url: 'https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.1/require.min.js',
17 | paths: {
18 | vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.10.1/min/vs'
19 | }
20 | }
21 |
22 | const MONACO_OPTIONS = {
23 | folding: true,
24 | scrollBeyondLastLine: true,
25 | wordWrap: true,
26 | wrappingIndent: 'same',
27 | automaticLayout: true,
28 | autoIndent: true,
29 | cursorBlinking: 'smooth',
30 | lineNumbersMinChars: 4
31 | }
32 |
33 | type Props = {
34 | trackValueChanges: boolean,
35 | value: string,
36 | onChange: (s: string) => mixed
37 | }
38 |
39 | type State = {
40 | hasMadeLocalModifications: boolean,
41 | code: string
42 | }
43 |
44 | export default class Editor extends Component {
45 | state = {
46 | hasMadeLocalModifications: false,
47 | code: this.props.value
48 | }
49 |
50 | editor: any
51 | editorDidMount = (editor: any) => {
52 | editor.focus()
53 | this.editor = editor
54 | }
55 |
56 | componentDidUpdate(prevProps: Props) {
57 | if (
58 | prevProps.value !== this.props.value &&
59 | this.props.trackValueChanges &&
60 | !this.state.hasMadeLocalModifications
61 | ) {
62 | this.setState({ code: this.props.value })
63 | }
64 | }
65 |
66 | changedDebounced = debounce((spec: string) => {
67 | this.props.onChange(spec)
68 | this.editor.focus()
69 | }, 700)
70 |
71 | handleEditorChange = (spec: string) => {
72 | this.setState({ code: spec, hasMadeLocalModifications: true })
73 | this.changedDebounced(spec)
74 | }
75 |
76 | editorWillMount = (monaco: any) => {
77 | monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
78 | validate: true,
79 | allowComments: true,
80 | schemas: [monacoJsonSchema]
81 | })
82 | }
83 |
84 | render() {
85 | const { code } = this.state
86 |
87 | return (
88 |
89 | {({ bind, measurements }) => (
90 |
91 | {measurements && measurements.container.height ? (
92 |
103 | ) : null}
104 |
105 | )}
106 |
107 | )
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/src/components/DatasetSelector.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | import React, { Component } from 'react'
3 | import { decorate, observable } from 'mobx'
4 | import { observer } from 'mobx-react'
5 | import { FormGroup, InputGroup, FormControl, Button } from 'react-bootstrap'
6 | import { API_HOST } from '../util/constants'
7 | import DWDatasetModal, { type SelectedDatasetType } from './DWDatasetModal'
8 |
9 | type Props = {
10 | token: string,
11 | defaultValue: string,
12 | value: string,
13 | limitToProjects?: boolean,
14 | onChange: (id: string) => mixed
15 | }
16 |
17 | function toID(d: SelectedDatasetType) {
18 | return d.owner + '/' + d.id
19 | }
20 |
21 | class DatasetSelector extends Component {
22 | loadingInitial: boolean = true
23 | modalOpen: boolean = false
24 |
25 | componentDidMount() {
26 | this.setValueIfValid()
27 | }
28 |
29 | async setValueIfValid() {
30 | const { token, defaultValue, limitToProjects } = this.props
31 | if (!defaultValue) return
32 |
33 | try {
34 | const resp = await fetch(`${API_HOST}/v0/datasets/${defaultValue}`, {
35 | method: 'GET',
36 | headers: {
37 | Accept: 'application/json',
38 | Authorization: `Bearer ${token}`
39 | }
40 | })
41 | if (resp.ok) {
42 | const jsonResponse = await resp.json()
43 | if (
44 | jsonResponse.accessLevel === 'WRITE' ||
45 | jsonResponse.accessLevel === 'ADMIN'
46 | ) {
47 | if ((limitToProjects && jsonResponse.isProject) || !limitToProjects) {
48 | this.props.onChange(defaultValue)
49 | }
50 | }
51 | }
52 | } catch (e) {}
53 |
54 | this.loadingInitial = false
55 | }
56 |
57 | handleSelect = (d: SelectedDatasetType) => {
58 | this.props.onChange(toID(d))
59 | this.modalOpen = false
60 | }
61 |
62 | handleSelectClick = () => {
63 | this.modalOpen = true
64 | }
65 |
66 | handleCancel = () => {
67 | this.modalOpen = false
68 | }
69 |
70 | render() {
71 | const { limitToProjects } = this.props
72 | const value = this.loadingInitial
73 | ? 'Loading...'
74 | : this.props.value ||
75 | `Select ${limitToProjects ? 'project' : 'dataset/project'}`
76 |
77 | return (
78 | <>
79 |
80 |
81 |
82 |
83 |
90 |
91 |
92 |
93 | {this.modalOpen && (
94 |
99 | )}
100 | >
101 | )
102 | }
103 | }
104 |
105 | decorate(DatasetSelector, {
106 | modalOpen: observable,
107 | loadingInitial: observable
108 | })
109 |
110 | export default observer(DatasetSelector)
111 |
--------------------------------------------------------------------------------
/src/components/__tests__/__snapshots__/Header.spec.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`renders 1`] = `
4 |
68 | `;
69 |
70 | exports[`renders with dataset 1`] = `
71 |
144 | `;
145 |
--------------------------------------------------------------------------------
/src/components/VizEmpty.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | import React, { Component } from 'react'
3 | import classes from './VizEmpty.module.css'
4 |
5 | const svg = (
6 |
77 | )
78 |
79 | export default class VizEmpty extends Component<{}> {
80 | render() {
81 | return (
82 |
83 | {svg}
84 |
85 | Choose a chart type and columns
to the left and your chart will
86 | appear.
Like magic ✨
87 |
88 |
89 | )
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/components/__tests__/__snapshots__/VizEmpty.spec.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`renders 1`] = `
4 |
7 |
93 |
96 | Choose a chart type and columns
97 |
98 | to the left and your chart will appear.
99 |
100 | Like magic ✨
101 |
102 |
103 | `;
104 |
--------------------------------------------------------------------------------
/src/components/__tests__/__snapshots__/DownloadButton.spec.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`DownloadMenuItem renders 1`] = `
4 |
8 |
17 |
18 | hello
19 |
20 |
21 |
22 | `;
23 |
24 | exports[`renders 1`] = `
25 |
28 |
41 |
178 |
179 | `;
180 |
--------------------------------------------------------------------------------
/src/util/__test__/__snapshots__/Store.spec.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`ChartConfig sets a few encodings 1`] = `
4 | Object {
5 | "config": Object {
6 | "encodings": Array [
7 | Object {
8 | "_id": 7,
9 | "aggregate": null,
10 | "bin": false,
11 | "channel": "x",
12 | "field": "foo",
13 | "scale": "linear",
14 | "sort": "ascending",
15 | "sortField": null,
16 | "timeUnit": null,
17 | "type": "auto",
18 | "zero": true,
19 | },
20 | Object {
21 | "_id": 9,
22 | "aggregate": null,
23 | "bin": false,
24 | "channel": "y",
25 | "field": null,
26 | "scale": "linear",
27 | "sort": "ascending",
28 | "sortField": null,
29 | "timeUnit": null,
30 | "type": "auto",
31 | "zero": true,
32 | },
33 | Object {
34 | "_id": 11,
35 | "aggregate": null,
36 | "bin": false,
37 | "channel": "color",
38 | "field": null,
39 | "scale": "linear",
40 | "sort": "ascending",
41 | "sortField": null,
42 | "timeUnit": null,
43 | "type": "auto",
44 | "zero": true,
45 | },
46 | ],
47 | "height": null,
48 | "manualSpec": null,
49 | "mark": "bar",
50 | "title": null,
51 | "width": null,
52 | },
53 | "fields": Array [
54 | Object {
55 | "label": null,
56 | "name": "foo",
57 | "rdfType": "http://www.w3.org/2001/XMLSchema#float",
58 | },
59 | ],
60 | "location": undefined,
61 | "token": "",
62 | }
63 | `;
64 |
65 | exports[`ChartConfig sets a few encodings 2`] = `
66 | Object {
67 | "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
68 | "autosize": undefined,
69 | "config": Object {
70 | "background": "#ffffff",
71 | "padding": 20,
72 | },
73 | "data": Object {
74 | "name": "source",
75 | },
76 | "encoding": Object {
77 | "x": Object {
78 | "aggregate": undefined,
79 | "bin": undefined,
80 | "field": "foo",
81 | "scale": Object {
82 | "type": "linear",
83 | "zero": true,
84 | },
85 | "sort": undefined,
86 | "timeUnit": undefined,
87 | "type": "quantitative",
88 | },
89 | },
90 | "height": undefined,
91 | "mark": Object {
92 | "type": "bar",
93 | },
94 | "title": undefined,
95 | "width": undefined,
96 | }
97 | `;
98 |
99 | exports[`ChartConfig sets a few encodings 3`] = `
100 | Array [
101 | Object {
102 | "foo": 1,
103 | },
104 | ]
105 | `;
106 |
107 | exports[`ChartConfig works 1`] = `
108 | Object {
109 | "encodings": Array [],
110 | "height": null,
111 | "manualSpec": null,
112 | "mark": "bar",
113 | "title": null,
114 | "width": null,
115 | }
116 | `;
117 |
118 | exports[`Store detects valid params 1`] = `
119 | Object {
120 | "dataset": "foo/bar",
121 | "query": "select",
122 | }
123 | `;
124 |
125 | exports[`Store works 1`] = `
126 | Object {
127 | "config": Object {
128 | "encodings": Array [
129 | Object {
130 | "_id": 1,
131 | "aggregate": null,
132 | "bin": false,
133 | "channel": "x",
134 | "field": null,
135 | "scale": "linear",
136 | "sort": "ascending",
137 | "sortField": null,
138 | "timeUnit": null,
139 | "type": "auto",
140 | "zero": true,
141 | },
142 | Object {
143 | "_id": 3,
144 | "aggregate": null,
145 | "bin": false,
146 | "channel": "y",
147 | "field": null,
148 | "scale": "linear",
149 | "sort": "ascending",
150 | "sortField": null,
151 | "timeUnit": null,
152 | "type": "auto",
153 | "zero": true,
154 | },
155 | Object {
156 | "_id": 5,
157 | "aggregate": null,
158 | "bin": false,
159 | "channel": "color",
160 | "field": null,
161 | "scale": "linear",
162 | "sort": "ascending",
163 | "sortField": null,
164 | "timeUnit": null,
165 | "type": "auto",
166 | "zero": true,
167 | },
168 | ],
169 | "height": null,
170 | "manualSpec": null,
171 | "mark": "bar",
172 | "title": null,
173 | "width": null,
174 | },
175 | "fields": Array [],
176 | "location": undefined,
177 | "token": "",
178 | }
179 | `;
180 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2.1
2 | commands:
3 | cypress_tests:
4 | description: Setup and run cypress tests
5 | steps:
6 | - run:
7 | name: Run server for tests
8 | command: yarn http-server build/ -p 3500
9 | background: true
10 | - run:
11 | name: Cypress tests
12 | command: |
13 | yarn cypress install
14 | yarn cypress run
15 | - save_cache:
16 | key: v1-npm-deps-{{ .Branch }}-{{ checksum "yarn.lock" }}
17 | paths:
18 | - node_modules
19 | - ~/.cache/Cypress
20 | - store_artifacts:
21 | path: cypress/videos
22 | - store_artifacts:
23 | path: cypress/screenshots
24 | - store_test_results:
25 | path: cypress/junit-results
26 | jobs:
27 | build:
28 | docker:
29 | - image: cypress/base:latest
30 | environment:
31 | BASH_ENV: ~/.env
32 | CI: true
33 | steps:
34 | - checkout
35 | - restore_cache:
36 | keys:
37 | - v1-npm-deps-{{ .Branch }}-{{ checksum "yarn.lock" }}
38 | - v1-npm-deps-{{ .Branch }}
39 | - v1-npm-deps-
40 | - run: yarn install
41 | - run:
42 | name: Jest tests
43 | command: yarn test
44 | - run: yarn build
45 | - save_cache:
46 | key: v1-npm-deps-{{ .Branch }}-{{ checksum "yarn.lock" }}
47 | paths:
48 | - node_modules
49 | - cypress_tests
50 | build_main:
51 | docker:
52 | - image: cypress/base:latest
53 | environment:
54 | BASH_ENV: ~/.env
55 | steps:
56 | - checkout
57 | - restore_cache:
58 | keys:
59 | - v1-npm-deps-{{ .Branch }}-{{ checksum "yarn.lock" }}
60 | - v1-npm-deps-{{ .Branch }}
61 | - v1-npm-deps-
62 | - run: yarn install
63 | - run:
64 | name: Jest tests
65 | command: yarn test
66 | - run: yarn build
67 | - cypress_tests
68 | deploy_main:
69 | docker:
70 | - image: cimg/node:20.7.0
71 | environment:
72 | BASH_ENV: ~/.env
73 | steps:
74 | - checkout
75 | - run:
76 | name: Clone build-scripts repo
77 | command: git clone git@github.com:datadotworld/build-scripts.git
78 | - run:
79 | name: Setup aws creds
80 | command: build-scripts/cicd/setup_aws_credentials.sh
81 | - restore_cache:
82 | keys:
83 | - v1-npm-deps-{{ .Branch }}-{{ checksum "yarn.lock" }}
84 | - v1-npm-deps-{{ .Branch }}
85 | - v1-npm-deps-
86 | - run: yarn install
87 | - run:
88 | name: Generate license text
89 | command: yarn licenses generate-disclaimer --silent > src/generated/licenses.txt
90 | - run: yarn build
91 | - run:
92 | name: Install AWS Cli
93 | command: |
94 | cd /tmp
95 | curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
96 | unzip awscliv2.zip
97 | ./aws/install -i ~/.local/aws-cli -b ~/.local/bin
98 | - run:
99 | name: Sync to s3
100 | command: |
101 | aws s3 sync build s3://dataworld-chartbuilder-us-east-1 --exclude "index.html" --exclude "manifest.json" --exclude "service-worker.js" --exclude "asset-manifest.json"
102 | aws s3 cp build/index.html s3://dataworld-chartbuilder-us-east-1/index.html --cache-control max-age=300
103 | aws s3 cp build/manifest.json s3://dataworld-chartbuilder-us-east-1/manifest.json --cache-control max-age=300
104 | aws s3 cp build/service-worker.js s3://dataworld-chartbuilder-us-east-1/service-worker.js --cache-control max-age=300
105 | aws s3 cp build/asset-manifest.json s3://dataworld-chartbuilder-us-east-1/asset-manifest.json --cache-control max-age=300
106 | environment:
107 | AWS_PROFILE: artifacts
108 | workflows:
109 | build:
110 | jobs:
111 | - build:
112 | filters:
113 | branches:
114 | ignore:
115 | - main
116 | build_and_deploy_main:
117 | jobs:
118 | - build_main:
119 | filters:
120 | branches:
121 | only:
122 | - main
123 | - deploy_main:
124 | requires:
125 | - build_main
126 | context:
127 | - aws-artifacts
128 | filters:
129 | branches:
130 | only:
131 | - main
132 |
--------------------------------------------------------------------------------
/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | // In production, we register a service worker to serve assets from local cache.
2 |
3 | // This lets the app load faster on subsequent visits in production, and gives
4 | // it offline capabilities. However, it also means that developers (and users)
5 | // will only see deployed updates on the "N+1" visit to a page, since previously
6 | // cached resources are updated in the background.
7 |
8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9 | // This link also includes instructions on opting out of this behavior.
10 |
11 | const isLocalhost = Boolean(
12 | window.location.hostname === 'localhost' ||
13 | // [::1] is the IPv6 localhost address.
14 | window.location.hostname === '[::1]' ||
15 | // 127.0.0.1/8 is considered localhost for IPv4.
16 | window.location.hostname.match(
17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
18 | )
19 | );
20 |
21 | export default function register() {
22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
29 | return;
30 | }
31 |
32 | window.addEventListener('load', () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
34 |
35 | if (isLocalhost) {
36 | // This is running on localhost. Lets check if a service worker still exists or not.
37 | checkValidServiceWorker(swUrl);
38 |
39 | // Add some additional logging to localhost, pointing developers to the
40 | // service worker/PWA documentation.
41 | navigator.serviceWorker.ready.then(() => {
42 | console.log(
43 | 'This web app is being served cache-first by a service ' +
44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ'
45 | );
46 | });
47 | } else {
48 | // Is not local host. Just register service worker
49 | registerValidSW(swUrl);
50 | }
51 | });
52 | }
53 | }
54 |
55 | function registerValidSW(swUrl) {
56 | navigator.serviceWorker
57 | .register(swUrl)
58 | .then(registration => {
59 | registration.onupdatefound = () => {
60 | const installingWorker = registration.installing;
61 | installingWorker.onstatechange = () => {
62 | if (installingWorker.state === 'installed') {
63 | if (navigator.serviceWorker.controller) {
64 | // At this point, the old content will have been purged and
65 | // the fresh content will have been added to the cache.
66 | // It's the perfect time to display a "New content is
67 | // available; please refresh." message in your web app.
68 | console.log('New content is available; please refresh.');
69 | } else {
70 | // At this point, everything has been precached.
71 | // It's the perfect time to display a
72 | // "Content is cached for offline use." message.
73 | console.log('Content is cached for offline use.');
74 | }
75 | }
76 | };
77 | };
78 | })
79 | .catch(error => {
80 | console.error('Error during service worker registration:', error);
81 | });
82 | }
83 |
84 | function checkValidServiceWorker(swUrl) {
85 | // Check if the service worker can be found. If it can't reload the page.
86 | fetch(swUrl)
87 | .then(response => {
88 | // Ensure service worker exists, and that we really are getting a JS file.
89 | if (
90 | response.status === 404 ||
91 | response.headers.get('content-type').indexOf('javascript') === -1
92 | ) {
93 | // No service worker found. Probably a different app. Reload the page.
94 | navigator.serviceWorker.ready.then(registration => {
95 | registration.unregister().then(() => {
96 | window.location.reload();
97 | });
98 | });
99 | } else {
100 | // Service worker found. Proceed as normal.
101 | registerValidSW(swUrl);
102 | }
103 | })
104 | .catch(() => {
105 | console.log(
106 | 'No internet connection found. App is running in offline mode.'
107 | );
108 | });
109 | }
110 |
111 | export function unregister() {
112 | if ('serviceWorker' in navigator) {
113 | navigator.serviceWorker.ready.then(registration => {
114 | registration.unregister();
115 | });
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/src/components/SaveAsFileModal.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | import React, { Component } from 'react'
3 | import {
4 | Modal,
5 | Button,
6 | ControlLabel,
7 | FormControl,
8 | FormGroup,
9 | Alert,
10 | Col,
11 | Row,
12 | Grid
13 | } from 'react-bootstrap'
14 | import { decorate, observable } from 'mobx'
15 | import { observer, inject } from 'mobx-react'
16 | import { API_HOST } from '../util/constants'
17 | import DatasetSelector from './DatasetSelector'
18 | import LoadingAnimation from './LoadingAnimation'
19 | import VegaLiteImage from './VegaLiteImage'
20 | import classes from './Modals.module.css'
21 | import type { StoreType } from '../util/Store'
22 | import kebabCase from 'lodash/kebabCase'
23 |
24 | type Props = {
25 | onClose: Function,
26 | data: Array