├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── .storybook ├── fsMock.js └── main.js ├── CONTRIBUTING.md ├── License ├── README.md ├── dist ├── App.css ├── App.js ├── App.test.js ├── assets │ └── images │ │ └── computing-cloud.svg ├── components │ ├── Choose │ │ ├── Choose.css │ │ └── index.js │ ├── InputFile │ │ ├── InputFile.css │ │ ├── InputFile.test.js │ │ └── index.js │ ├── InputUrl │ │ ├── InputUrl.css │ │ ├── InputUrl.test.js │ │ └── index.js │ ├── Metadata │ │ ├── Metadata.css │ │ ├── Metadata.test.js │ │ └── index.js │ ├── ProgressBar │ │ ├── ProgressBar.css │ │ └── index.js │ ├── Switcher │ │ ├── Switcher.css │ │ └── index.js │ ├── TableSchema │ │ ├── TableSchema.css │ │ ├── TableSchema.test.js │ │ └── index.js │ └── Upload │ │ ├── Upload.test.js │ │ └── index.js ├── core.js ├── db │ ├── encode.json │ ├── licenses.json │ ├── resource_formats.json │ └── types.json ├── index.js ├── setupTests.js └── utils │ ├── Utils.test.js │ └── index.js ├── examples ├── basic │ ├── README.md │ ├── assets │ │ ├── after-css.png │ │ ├── before-css.png │ │ ├── table-schema-example.md │ │ └── upload.png │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── manifest.json │ │ └── robots.txt │ └── yarn.lock └── table-schema │ ├── README.md │ ├── assets │ ├── app.png │ └── table.png │ ├── package.json │ ├── public │ ├── index.html │ ├── manifest.json │ └── robots.txt │ ├── src │ ├── App.css │ ├── App.js │ ├── App.test.js │ ├── index.css │ ├── index.js │ └── setupTests.js │ └── yarn.lock ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.css ├── App.js ├── App.test.js ├── assets │ └── images │ │ └── computing-cloud.svg ├── components │ ├── Choose │ │ ├── Choose.css │ │ └── index.jsx │ ├── InputFile │ │ ├── InputFile.css │ │ ├── InputFile.test.js │ │ └── index.jsx │ ├── InputUrl │ │ ├── InputUrl.css │ │ ├── InputUrl.test.js │ │ └── index.jsx │ ├── Metadata │ │ ├── Metadata.css │ │ ├── Metadata.test.js │ │ └── index.jsx │ ├── ProgressBar │ │ ├── ProgressBar.css │ │ └── index.jsx │ ├── Switcher │ │ ├── Switcher.css │ │ └── index.jsx │ ├── TableSchema │ │ ├── TableSchema.css │ │ ├── TableSchema.test.js │ │ └── index.jsx │ └── Upload │ │ ├── Upload.test.js │ │ └── index.jsx ├── core.js ├── db │ ├── encode.json │ ├── licenses.json │ ├── resource_formats.json │ └── types.json ├── index.js ├── setupTests.js └── utils │ ├── Utils.test.js │ └── index.js ├── stories ├── 1-app..stories.js ├── 2-uploader.stories.js ├── 3-metadata.stories.js ├── 4-TableSchema.stories.js ├── 5-InputFile.stories.js ├── 6-InputUrl.stories.js └── 7-Choose.stories.js └── yarn.lock /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: datapub actions 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [12.x] 20 | 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@v2 24 | 25 | - name: Set up Node.js ${{ matrix.node-version }} 26 | uses: actions/setup-node@v1 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | 30 | - name: Install dependencies 31 | run: npm install 32 | 33 | - name: Run the tests 34 | run: npm test 35 | 36 | - name: Build 37 | run: CI=false npm run build 38 | 39 | - name: Deploy storybook to Github Pages 40 | run: npm run deploy-storybook -- --ci 41 | env: 42 | GH_TOKEN: ${{ github.actor }}:${{ secrets.GITHUB_TOKEN }} 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # IDEs 4 | /.idea 5 | 6 | # dependencies 7 | /node_modules 8 | /.pnp 9 | .pnp.js 10 | 11 | # testing 12 | /coverage 13 | 14 | # production 15 | /build 16 | 17 | # misc 18 | .DS_Store 19 | .env.local 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | /storybook-static/* -------------------------------------------------------------------------------- /.storybook/fsMock.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | readFileSync: () => 'mocked file', 3 | } -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | // Add fs mockFile because storybook broken without the fs 4 | module.exports = { 5 | stories: ['../stories/**/*.stories.js'], 6 | addons: ['@storybook/addon-actions', '@storybook/addon-links', '@storybook/addon-controls', '@storybook/addon-docs'], 7 | webpackFinal: async (storybookBaseConfig, configType) => { 8 | storybookBaseConfig.resolve.alias = { 9 | ...storybookBaseConfig.resolve.alias, 10 | 'fs': path.resolve(__dirname, 'fsMock.js') 11 | }; 12 | 13 | return storybookBaseConfig; 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | 1. Fork it! 4 | 2. Create your feature branch: `git checkout -b my-new-feature` 5 | 3. Commit your changes: `git commit -m '[ex][m]: plotly json examples - fixes #23.'` 6 | 4. Push to the branch: `git push origin my-new-feature` 7 | 8 | - After your pull request is merged, you can safely delete your branch. 9 | - More information related version control in [datopian documentation](https://playbook.datopian.com/style-guide/version-control/#commit-messages) 10 | 11 | ### [<-- Back](https://github.com/datopian/datapub) -------------------------------------------------------------------------------- /License: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Datopian 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # Datapub 4 | 5 | [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/datopian/datapub/issues) 6 | ![build](https://github.com/datopian/datapub/workflows/datapub%20actions/badge.svg) 7 | [![The MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](http://opensource.org/licenses/MIT) 8 | 9 | DataPub is a React-based framework for rapidly building modern data publishing flows (esp for CKAN). It provides a variety of core components as well as example apps and flows. 10 | 11 |
12 | 13 | ## Background 14 | 15 | This is a brief summary from https://tech.datopian.com/publish/ -- read that for more detail. 16 | 17 | ### What do we mean by data publishing? 18 | 19 | The process of publishing data files and datasets (collections of files). 20 | 21 | Specifically, the process of getting your data files stored and described in a data portal or other platform. Usually it involves steps like: 22 | 23 | * Uploading or linking the files into the platform 24 | * Describing thoese files with high level metadata e.g. the name and description of the dataset or files, their license etc 25 | * Specific metadata about the data e.g. its structure, what fields there are and their types (e.g. integer, string) 26 | 27 | ### Why DataPub? 28 | 29 | At Datopian we have been building data publishing flows for nearly 15 years both in tools like CKAN and OpenSpending and in custom applications. Our experience has taught us two things: 30 | 31 | * Data Publishing flows almost always have some custom aspect. Whether it is a small tweak like adding a specific metadata field or a complex change like add data validation. 32 | * There are many common components e.g. file upload and common patterns to many overall flows e.g. add a file, add metadata, save! 33 | 34 | This indicates the need for a **framework** -- rather than a single one-size-fits-all application. 35 | 36 | ### The DataPub approach 37 | 38 | * **🏗️ React-based**: individual data publishing flows will be React apps that you can boot with standard tools like `create-react-app` and where you can use the full ecosystem of React tooling and components 39 | * **📦 Core components**: provide a suite of tried and tested core components common to many publishing flows such as file upload, table schema editor etc 40 | * **🚂 [Template apps](./examples/)**: provide examples of full-scale apps which developers building new flows can use for inspiration and instruction e.g. copy and paste an example and then modify it 41 | 42 | ## Components 43 | 44 | Components include: 45 | 46 | * [File Upload](https://datopian.github.io/datapub/iframe.html?id=components-upload--idle) 47 | * [File Input](https://datopian.github.io/datapub/iframe.html?id=components-input-file--drag-and-drop) 48 | * [File Input Url](https://datopian.github.io/datapub/iframe.html?id=components-input-url--default) 49 | * [Table Schema](https://datopian.github.io/datapub/iframe.html?id=components-tableschema--idle) 50 | * [Metadata](https://datopian.github.io/datapub/iframe.html?id=components-metadata--idle) 51 | * ProgressBar 52 | * Switcher 53 | 54 | 55 | To see all the available components visit our Storybook: 56 | 57 | https://datopian.github.io/datapub 58 | 59 | ## Example Apps 60 | 61 | See the [`examples`][ex] directory. 62 | 63 | For other full scale apps using DataPub in the wild see: 64 | 65 | * https://github.com/datopian/datapub-nhs 66 | 67 | [ex]: ./examples/ 68 | 69 | ## Getting started 70 | 71 | There are two ways to get started 72 | 73 | * Copy an existing example app from the [`examples`][ex] directory and then modify it 74 | * Add DataPub components into either an existing React App or into a newly created a React app created from scratch using e.g. `create-react-app` 75 | 76 | Of these two options the former is better when experimenting or for small changes. The latter is better if you are building a more complex application or integrating into an existing application. 77 | 78 | In order to add DataPub components into a newly created React application, follow the steps below: 79 | 80 | **Step 1:** create a new react application: 81 | ```bash 82 | create-react-app datapub-extend 83 | ``` 84 | Change directory into datapub-extend and run the application to ensure it was created successfully: 85 | ```bash 86 | cd datapub-extend 87 | yarn start 88 | ``` 89 | **Step 2:** Install Datapub 90 | 91 | ```bash 92 | yarn add datapub 93 | ``` 94 | **Step 3:** In the App.js, initialises your app with the Resource editor 95 | 96 | ```javascript 97 | ... 98 | export class ResourceEditor extends React.Component { 99 | constructor(props) { 100 | super(props); 101 | this.state = { 102 | datasetId: this.props.config.datasetId, 103 | resourceId: "", 104 | resource: this.props.resource || {}, 105 | ... 106 | }; 107 | } 108 | ... 109 | 110 | ``` 111 | 112 | **Step 4:** Also in App.js, import the components you need. For instance in the code below we import Upload and TableSchema component. 113 | 114 | ```javascript 115 | ... 116 | import { Upload, TableSchema } from "datapub"; 117 | ... 118 | ``` 119 | **Step 5:** In the render section of your resource editor, add the Upload and TableSchema components you just imported. 120 | 121 | ```javascript 122 | ... 123 |
124 | 125 | {this.state.resource.schema && ( 126 | 130 | )} 131 | 132 | {!this.state.isResourceEdit ? ( 133 | 136 | ) : ( 137 |
138 | 145 | 146 |
147 | )} 148 |
149 | ... 150 | ``` 151 | 152 | See the full example with code and explanations [here](examples/table-schema) 153 | 154 | --- 155 | 156 | ## Developers 157 | 158 | ### Install 159 | 160 | Install a recent [Node](https://nodejs.org/en/) version. 161 | 162 | First, clone the repo via git: 163 | 164 | ```bash 165 | $ git clone git@github.com:datopian/datapub.git 166 | ``` 167 | 168 | And then install dependencies with npm. 169 | 170 | ```bash 171 | $ cd datapub 172 | $ npm install 173 | ``` 174 | 175 | ### Run 176 | 177 | Run the app in the development mode. 178 | 179 | ```bash 180 | $ npm run start 181 | ``` 182 | 183 | Then open [http://localhost:3000/](http://localhost:3000/) to view it in the browser. 184 | 185 | The page will reload if you make edits. 186 | 187 | ### Storybook 188 | 189 | Storybook is a tool that prepares a development environment for UI components. It allows you to develop and design your graphical interfaces quickly, isolated, and independently. Making it possible to define different states for components, thus documenting their states. 190 | 191 | **Note**: Every push will run GitHub actions to deploy in GitHub pages. You can check online at https://datopian.github.io/datapub 192 | 193 | #### Run storybook 194 | 195 | ```bash 196 | $ npm run storybook 197 | ``` 198 | 199 | or 200 | 201 | ```bash 202 | $ yarn storybook 203 | ``` 204 | 205 | ### Run Tests 206 | 207 | ```bash 208 | $ npm test 209 | ``` 210 | 211 | or 212 | 213 | ```bash 214 | $ yarn test 215 | ``` 216 | 217 | To run tests + coverage 218 | 219 | ```bash 220 | $ yarn test:watch 221 | ``` 222 | 223 | ## Contributing 224 | 225 | Please make sure to read the [CONTRIBUTING.md](CONTRIBUTING.md) Guide before making a pull request. 226 | 227 | ## License 228 | 229 | This project is licensed under the MIT License - see the [LICENSE](License) file for details 230 | -------------------------------------------------------------------------------- /dist/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 12 | monospace; 13 | } 14 | 15 | .App { 16 | text-align: center; 17 | min-height: 100vh; 18 | color: #222; 19 | } 20 | 21 | .upload-wrapper { 22 | width: 100%; 23 | min-width: 360px; 24 | max-width: 960px; 25 | margin: 0 auto; 26 | background-color: #fff; 27 | padding: 32px 24px; 28 | border-radius: 12px; 29 | min-height: 60%; 30 | display: grid; 31 | grid-template-columns: 1fr 1fr; 32 | grid-template-rows: auto 300px auto auto; 33 | grid-template-areas: 34 | "header header" 35 | "uploadArea uploadArea" 36 | "editArea editArea"; 37 | row-gap: 20px; 38 | column-gap: 20px; 39 | } 40 | 41 | .upload-header { 42 | grid-area: header; 43 | border-bottom: 1px solid #e3ebff; 44 | } 45 | 46 | .upload-header__title { 47 | color: #3f4656; 48 | font-weight: 400; 49 | } 50 | 51 | .upload-area { 52 | grid-area: uploadArea; 53 | display: grid; 54 | grid-template-columns: 40% auto; 55 | grid-template-rows: 1fr 1fr; 56 | grid-template-areas: "drop info"; 57 | align-items: center; 58 | } 59 | 60 | .upload-area__info { 61 | grid-area: info; 62 | padding-top: 20px; 63 | } 64 | 65 | .upload-list { 66 | padding: 0; 67 | } 68 | 69 | .list-item { 70 | display: flex; 71 | } 72 | 73 | .upload-list-item { 74 | margin: 0 auto; 75 | background-color: #fff; 76 | padding: 8px 24px; 77 | list-style: none; 78 | max-width: 480px; 79 | width: 85%; 80 | border-top-left-radius: 8px; 81 | border-bottom-left-radius: 8px; 82 | color: #222; 83 | display: flex; 84 | align-items: center; 85 | justify-content: space-between; 86 | box-shadow: 0 4px 11px 3px rgba(18, 22, 33, 0.05); 87 | } 88 | 89 | .upload-file-name { 90 | max-width: 19ch; 91 | white-space: nowrap; 92 | overflow: hidden; 93 | text-overflow: ellipsis; 94 | color: #3f4656; 95 | } 96 | 97 | .upload-file-size { 98 | text-align: left; 99 | font-size: 0.75rem; 100 | color: lightslategrey; 101 | } 102 | 103 | .upload-message { 104 | color: #3f4656; 105 | } 106 | 107 | .upload-switcher { 108 | grid-area: switcher; 109 | } 110 | 111 | .upload-edit-area { 112 | grid-area: editArea; 113 | } 114 | 115 | .btn { 116 | max-width: 220px; 117 | margin: 2rem; 118 | border-radius: 5px; 119 | padding: 10px 25px; 120 | height: 45px; 121 | font-size: 1.2rem; 122 | color: #fff; 123 | background-color: #00A7E1; 124 | outline: none; 125 | border: none; 126 | cursor: pointer; 127 | box-shadow: 0 4px 11px 3px rgba(18, 22, 33, 0.05); 128 | } 129 | 130 | .btn:disabled { 131 | background: #e3e9ed !important; 132 | cursor: default; 133 | } 134 | 135 | .btn:hover { 136 | background-color: #009ace; 137 | } 138 | 139 | .btn-delete { 140 | margin: 2rem; 141 | border-radius: 5px; 142 | padding: 10px 25px; 143 | height: 45px; 144 | font-size: 1.2rem; 145 | color: #fff; 146 | outline: none; 147 | border: none; 148 | cursor: pointer; 149 | box-shadow: 0 4px 11px 3px rgba(18, 22, 33, 0.05); 150 | background-color: #F11301; 151 | } 152 | 153 | .btn-delete :disabled { 154 | background: #e3e9ed !important; 155 | cursor: default; 156 | } 157 | 158 | .btn-delete:hover { 159 | background-color: #C70E00; 160 | } 161 | 162 | .resource-edit-actions { 163 | display: flex; 164 | justify-content: space-between; 165 | } -------------------------------------------------------------------------------- /dist/App.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _react = _interopRequireDefault(require("react")); 4 | 5 | var _enzyme = require("enzyme"); 6 | 7 | var _App = _interopRequireDefault(require("./App")); 8 | 9 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 10 | 11 | describe("", function () { 12 | it("should render in add a resource", function () { 13 | var wrapper = (0, _enzyme.shallow)( /*#__PURE__*/_react.default.createElement(_App.default, null)); 14 | expect(wrapper.find(".btn")).toHaveLength(1); 15 | expect(wrapper.find(".btn-delete")).toHaveLength(0); 16 | }); 17 | }); -------------------------------------------------------------------------------- /dist/assets/images/computing-cloud.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /dist/components/Choose/Choose.css: -------------------------------------------------------------------------------- 1 | .upload-choose { 2 | 3 | } 4 | 5 | .choose-btn { 6 | padding: 10px; 7 | width: 90%; 8 | background-color: #4FA8FD; 9 | border: none; 10 | border-radius: 3px; 11 | color: #fff; 12 | font-weight: 500; 13 | cursor: pointer; 14 | outline: 0; 15 | font-weight: bold; 16 | } 17 | .choose-text { 18 | font-size: 1rem; 19 | font-weight: 300; 20 | } 21 | -------------------------------------------------------------------------------- /dist/components/Choose/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports.default = void 0; 9 | 10 | var _react = _interopRequireWildcard(require("react")); 11 | 12 | var _propTypes = _interopRequireDefault(require("prop-types")); 13 | 14 | var _InputFile = _interopRequireDefault(require("../InputFile")); 15 | 16 | var _InputUrl = _interopRequireDefault(require("../InputUrl")); 17 | 18 | require("./Choose.css"); 19 | 20 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 21 | 22 | function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function _getRequireWildcardCache() { return cache; }; return cache; } 23 | 24 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } 25 | 26 | function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); } 27 | 28 | function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } 29 | 30 | function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } 31 | 32 | function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } 33 | 34 | function _iterableToArrayLimit(arr, i) { if (typeof Symbol === "undefined" || !(Symbol.iterator in Object(arr))) return; var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } 35 | 36 | function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } 37 | 38 | var Choose = function Choose(_ref) { 39 | var onChangeUrl = _ref.onChangeUrl, 40 | onChangeHandler = _ref.onChangeHandler; 41 | 42 | var _useState = (0, _react.useState)(false), 43 | _useState2 = _slicedToArray(_useState, 2), 44 | uploadOption = _useState2[0], 45 | setUploadOption = _useState2[1]; 46 | 47 | return /*#__PURE__*/_react.default.createElement("div", { 48 | className: "upload-choose" 49 | }, uploadOption ? /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, uploadOption === "file" && /*#__PURE__*/_react.default.createElement(_InputFile.default, { 50 | onChangeHandler: onChangeHandler 51 | }), uploadOption === "url" && /*#__PURE__*/_react.default.createElement(_InputUrl.default, { 52 | onChangeUrl: onChangeUrl 53 | })) : /*#__PURE__*/_react.default.createElement("div", null, /*#__PURE__*/_react.default.createElement("button", { 54 | className: "choose-btn", 55 | onClick: function onClick() { 56 | return setUploadOption("file"); 57 | } 58 | }, "Choose a file to Upload "), /*#__PURE__*/_react.default.createElement("p", { 59 | className: "choose-text" 60 | }, "OR"), /*#__PURE__*/_react.default.createElement("button", { 61 | className: "choose-btn", 62 | onClick: function onClick() { 63 | return setUploadOption("url"); 64 | } 65 | }, "Link a file already online"))); 66 | }; 67 | 68 | Choose.propTypes = { 69 | onChangeUrl: _propTypes.default.func.isRequired, 70 | onChangeHandler: _propTypes.default.func.isRequired 71 | }; 72 | var _default = Choose; 73 | exports.default = _default; -------------------------------------------------------------------------------- /dist/components/InputFile/InputFile.css: -------------------------------------------------------------------------------- 1 | 2 | .upload-area__drop { 3 | position: relative; 4 | text-align: center; 5 | display: flex; 6 | justify-content: center; 7 | margin-top: 20px; 8 | height: 220px; 9 | } 10 | 11 | .upload-area__drop__input { 12 | grid-area: drop; 13 | padding: 1rem 1rem; 14 | position: relative; 15 | display: grid; 16 | grid-template-columns: auto auto; 17 | align-items: center; 18 | min-height: 200px; 19 | border: 1px dashed #3f4c6b; 20 | margin: 0px auto; 21 | width: 95%; 22 | border-radius: 8px; 23 | cursor: pointer; 24 | outline: 0; 25 | box-sizing: border-box; 26 | z-index: 10; 27 | color: transparent; 28 | } 29 | 30 | .upload-area__drop__input::-webkit-file-upload-button { 31 | flex-shrink: 0; 32 | font-size: 0.65rem; 33 | color: #ffffff; 34 | border-radius: 3px; 35 | padding: 8px 15px; 36 | margin: 0; 37 | text-transform: uppercase; 38 | text-align: center; 39 | border: none; 40 | outline: 0; 41 | margin: 0 auto; 42 | visibility: hidden; 43 | } 44 | 45 | .upload-area__drop__icon { 46 | width: 80px; 47 | position: absolute; 48 | top: 20%; 49 | left: 40%; 50 | cursor: pointer; 51 | fill: #e3ebff; 52 | } 53 | 54 | .upload-area__drop__text { 55 | color: lightslategrey; 56 | opacity: 0.7; 57 | font-weight: 400; 58 | position: absolute; 59 | top: 55%; 60 | font-size: 0.78rem; 61 | cursor: pointer; 62 | } -------------------------------------------------------------------------------- /dist/components/InputFile/InputFile.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _react = _interopRequireDefault(require("react")); 4 | 5 | var _enzyme = require("enzyme"); 6 | 7 | var _ = _interopRequireDefault(require(".")); 8 | 9 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 10 | 11 | describe("", function () { 12 | var onChangeHandler = jest.fn(function () { 13 | return "sample"; 14 | }); 15 | it("render InputFile without crashing", function () { 16 | var wrapper = (0, _enzyme.shallow)( /*#__PURE__*/_react.default.createElement(_.default, { 17 | onChangeHandler: onChangeHandler 18 | })); 19 | expect(wrapper.contains("Drag and drop your files")).toEqual(true); 20 | }); 21 | it("can select a file", function () { 22 | var wrapper = (0, _enzyme.shallow)( /*#__PURE__*/_react.default.createElement(_.default, { 23 | onChangeHandler: onChangeHandler 24 | })); 25 | var input = wrapper.find("input"); 26 | input.simulate("change", { 27 | target: { 28 | files: ["sample.csv"] 29 | } 30 | }); 31 | expect(onChangeHandler).toHaveBeenCalled(); 32 | }); 33 | }); -------------------------------------------------------------------------------- /dist/components/InputFile/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = void 0; 7 | 8 | var _react = _interopRequireDefault(require("react")); 9 | 10 | var _propTypes = _interopRequireDefault(require("prop-types")); 11 | 12 | require("./InputFile.css"); 13 | 14 | var _computingCloud = _interopRequireDefault(require("../../assets/images/computing-cloud.svg")); 15 | 16 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 17 | 18 | var InputFile = function InputFile(_ref) { 19 | var onChangeHandler = _ref.onChangeHandler; 20 | return /*#__PURE__*/_react.default.createElement("div", { 21 | className: "upload-area__drop" 22 | }, /*#__PURE__*/_react.default.createElement("input", { 23 | className: "upload-area__drop__input", 24 | type: "file", 25 | name: "file", 26 | onChange: onChangeHandler 27 | }), /*#__PURE__*/_react.default.createElement("img", { 28 | className: "upload-area__drop__icon", 29 | src: "https://www.shareicon.net/data/256x256/2015/09/05/96087_cloud_512x512.png", 30 | alt: "upload-icon" 31 | }), /*#__PURE__*/_react.default.createElement("span", { 32 | className: "upload-area__drop__text" 33 | }, "Drag and drop your files", /*#__PURE__*/_react.default.createElement("br", null), "or ", /*#__PURE__*/_react.default.createElement("br", null), "click to select")); 34 | }; 35 | 36 | InputFile.propTypes = { 37 | onChangeHandler: _propTypes.default.func.isRequired 38 | }; 39 | var _default = InputFile; 40 | exports.default = _default; -------------------------------------------------------------------------------- /dist/components/InputUrl/InputUrl.css: -------------------------------------------------------------------------------- 1 | .upload-area__url { 2 | text-align: left; 3 | width: 95%; 4 | margin: 0 auto; 5 | } 6 | 7 | .upload-area__url__label { 8 | display: block; 9 | padding: 4px; 10 | } 11 | 12 | .upload-area__url__input { 13 | width: 100%; 14 | height: 40px; 15 | border-radius: 5px; 16 | box-shadow: none; 17 | border: 1px solid #ced6e0; 18 | transition: all 0.3s ease-in-out; 19 | font-size: 18px; 20 | padding: 5px 15px; 21 | background: none; 22 | color: #1a3b5d; 23 | font-family: "Source Sans Pro", sans-serif; 24 | box-sizing: border-box; 25 | font-size: 1rem; 26 | } 27 | -------------------------------------------------------------------------------- /dist/components/InputUrl/InputUrl.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _react = _interopRequireDefault(require("react")); 4 | 5 | var _enzyme = require("enzyme"); 6 | 7 | var _ = _interopRequireDefault(require(".")); 8 | 9 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 10 | 11 | describe("", function () { 12 | var onChangeUrl = jest.fn(function () { 13 | return "https://www.datopian.com"; 14 | }); 15 | it("render InputUrl without crashing", function () { 16 | var wrapper = (0, _enzyme.shallow)( /*#__PURE__*/_react.default.createElement(_.default, { 17 | onChangeUrl: onChangeUrl 18 | })); 19 | expect(wrapper.contains("URL:")).toEqual(true); 20 | }); 21 | it("can edit url", function () { 22 | var wrapper = (0, _enzyme.shallow)( /*#__PURE__*/_react.default.createElement(_.default, { 23 | onChangeUrl: onChangeUrl 24 | })); 25 | var input = wrapper.find("input"); 26 | input.simulate("blur", { 27 | target: { 28 | value: "https://www.datopian.com" 29 | } 30 | }); 31 | expect(onChangeUrl).toHaveBeenCalled(); 32 | }); 33 | }); -------------------------------------------------------------------------------- /dist/components/InputUrl/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = void 0; 7 | 8 | var _react = _interopRequireDefault(require("react")); 9 | 10 | var _propTypes = _interopRequireDefault(require("prop-types")); 11 | 12 | require("./InputUrl.css"); 13 | 14 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 15 | 16 | var InputUrl = function InputUrl(_ref) { 17 | var onChangeUrl = _ref.onChangeUrl; 18 | 19 | var handleKeyPress = function handleKeyPress(event) { 20 | if (event.keyCode === 13) { 21 | event.target.blur(); 22 | } 23 | }; 24 | 25 | return /*#__PURE__*/_react.default.createElement("div", { 26 | className: "upload-area__url" 27 | }, /*#__PURE__*/_react.default.createElement("label", { 28 | className: "upload-area__url__label", 29 | htmlFor: "input-url" 30 | }, "URL:"), /*#__PURE__*/_react.default.createElement("input", { 31 | className: "upload-area__url__input", 32 | id: "input-url", 33 | type: "url", 34 | name: "input-url", 35 | onBlur: onChangeUrl, 36 | onKeyDown: function onKeyDown(event) { 37 | return handleKeyPress(event); 38 | }, 39 | placeholder: "https://www.data.com/sample.csv" 40 | })); 41 | }; 42 | 43 | InputUrl.propTypes = { 44 | onChangeUrl: _propTypes.default.func.isRequired 45 | }; 46 | var _default = InputUrl; 47 | exports.default = _default; -------------------------------------------------------------------------------- /dist/components/Metadata/Metadata.css: -------------------------------------------------------------------------------- 1 | .metadata-form { 2 | display: grid; 3 | grid-template-columns: 1fr 1fr; 4 | column-gap: 20px; 5 | } 6 | 7 | .metadata-name { 8 | color: #1a3b5d; 9 | text-align: left; 10 | grid-area: name; 11 | } 12 | 13 | .metadata-label { 14 | font-size: 14px; 15 | margin-bottom: 5px; 16 | margin-left: 5px; 17 | text-align: left; 18 | font-weight: 500; 19 | color: #1a3b5d; 20 | width: 100%; 21 | display: block; 22 | user-select: none; 23 | } 24 | 25 | .metadata-input { 26 | margin-bottom: 20px; 27 | } 28 | 29 | .metadata-input__input { 30 | width: 100%; 31 | height: 40px; 32 | border-radius: 5px; 33 | box-shadow: none; 34 | border: 1px solid #ced6e0; 35 | transition: all 0.3s ease-in-out; 36 | font-size: 18px; 37 | padding: 5px 15px; 38 | background: none; 39 | color: #1a3b5d; 40 | font-family: "Source Sans Pro", sans-serif; 41 | box-sizing: border-box; 42 | } 43 | 44 | .btn { 45 | margin-top: 17px; 46 | border-radius: 5px; 47 | padding: 10px; 48 | width: 50%; 49 | height: 45px; 50 | font-size: 1.2rem; 51 | color: #fff; 52 | background-color: #00A7E1; 53 | outline: none; 54 | border: none; 55 | cursor: pointer; 56 | box-shadow: 0 4px 11px 3px rgba(18, 22, 33, 0.05); 57 | } 58 | 59 | .btn:disabled { 60 | background: #e3e9ed !important; 61 | cursor: default; 62 | } 63 | 64 | .btn:hover { 65 | background-color: #009ace; 66 | } -------------------------------------------------------------------------------- /dist/components/Metadata/Metadata.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _react = _interopRequireDefault(require("react")); 4 | 5 | var _reactDom = _interopRequireDefault(require("react-dom")); 6 | 7 | var _enzyme = require("enzyme"); 8 | 9 | var _ = _interopRequireDefault(require(".")); 10 | 11 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 12 | 13 | describe("", function () { 14 | var handleChange = jest.fn(2); 15 | var handleSubmit = jest.fn(); 16 | var deleteResource = jest.fn(); 17 | var updateResource = jest.fn(); 18 | it("render Metadata without crashing", function () { 19 | var div = document.createElement("div"); 20 | 21 | _reactDom.default.render( /*#__PURE__*/_react.default.createElement(_.default, { 22 | metadata: {}, 23 | handleChange: handleChange, 24 | handleSubmit: handleSubmit, 25 | deleteResource: deleteResource, 26 | updateResource: updateResource, 27 | uploadSuccess: false, 28 | isResourceEdit: false 29 | }), div); 30 | }); 31 | it("auto-populate input fields", function () { 32 | var wrapper = (0, _enzyme.shallow)( /*#__PURE__*/_react.default.createElement(_.default, { 33 | metadata: { 34 | title: "sample", 35 | format: "csv", 36 | description: "Lorem ...", 37 | restricted: "private", 38 | encoding: "utf-8" 39 | }, 40 | handleChange: handleChange, 41 | handleSubmit: handleSubmit, 42 | deleteResource: deleteResource, 43 | updateResource: updateResource, 44 | uploadSuccess: false, 45 | isResourceEdit: false 46 | })); 47 | var inputTitle = wrapper.find("#title"); 48 | var inputFormat = wrapper.find("#format"); 49 | var inputDescription = wrapper.find("#description"); 50 | var inputRestricted = wrapper.find("#restricted"); 51 | var inputEncoding = wrapper.find("#encoding"); 52 | expect(inputTitle.props().value).toEqual("sample"); 53 | expect(inputFormat.props().value).toEqual("csv"); 54 | expect(inputDescription.props().value).toEqual("Lorem ..."); 55 | expect(inputRestricted.props().value).toEqual("private"); 56 | expect(inputEncoding.props().value).toEqual("utf-8"); 57 | }); 58 | }); -------------------------------------------------------------------------------- /dist/components/Metadata/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = void 0; 7 | 8 | var _react = _interopRequireDefault(require("react")); 9 | 10 | var _propTypes = _interopRequireDefault(require("prop-types")); 11 | 12 | require("./Metadata.css"); 13 | 14 | var _encode = _interopRequireDefault(require("../../db/encode.json")); 15 | 16 | var _resource_formats = _interopRequireDefault(require("../../db/resource_formats.json")); 17 | 18 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 19 | 20 | //TODO: add the custom fields as a props and render it in metadata component 21 | var customFields = [{ 22 | label: "Access Restriction", 23 | name: "restricted", 24 | input_type: "select", 25 | values: ['{"level": "public"}', '{"level": "private"}'], 26 | options: ["Public", "Private"] 27 | }]; 28 | 29 | var Metadata = function Metadata(_ref) { 30 | var metadata = _ref.metadata, 31 | handleChange = _ref.handleChange; 32 | return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement("h3", { 33 | className: "metadata-name" 34 | }, metadata.path), /*#__PURE__*/_react.default.createElement("div", { 35 | className: "metadata-form" 36 | }, /*#__PURE__*/_react.default.createElement("div", { 37 | className: "metadata-input" 38 | }, /*#__PURE__*/_react.default.createElement("label", { 39 | className: "metadata-label", 40 | htmlFor: "title" 41 | }, "Title:"), /*#__PURE__*/_react.default.createElement("input", { 42 | className: "metadata-input__input", 43 | type: "text", 44 | name: "title", 45 | id: "title", 46 | value: metadata.title || metadata.name, 47 | onChange: handleChange 48 | })), /*#__PURE__*/_react.default.createElement("div", { 49 | className: "metadata-input" 50 | }, /*#__PURE__*/_react.default.createElement("label", { 51 | className: "metadata-label", 52 | htmlFor: "description" 53 | }, "Description:"), /*#__PURE__*/_react.default.createElement("input", { 54 | className: "metadata-input__input", 55 | type: "text", 56 | name: "description", 57 | id: "description", 58 | value: metadata.description || "", 59 | onChange: handleChange 60 | })), /*#__PURE__*/_react.default.createElement("div", { 61 | className: "metadata-input" 62 | }, /*#__PURE__*/_react.default.createElement("label", { 63 | className: "metadata-label", 64 | htmlFor: "encoding" 65 | }, "Encoding:"), /*#__PURE__*/_react.default.createElement("select", { 66 | className: "metadata-input__input", 67 | name: "encoding", 68 | id: "encoding", 69 | value: metadata.encoding || "", 70 | onChange: handleChange, 71 | required: true 72 | }, /*#__PURE__*/_react.default.createElement("option", { 73 | value: "", 74 | disabled: true 75 | }, "Select..."), _encode.default.map(function (item) { 76 | return /*#__PURE__*/_react.default.createElement("option", { 77 | key: "format-".concat(item.value), 78 | value: item.value 79 | }, item.label); 80 | }))), /*#__PURE__*/_react.default.createElement("div", { 81 | className: "metadata-input" 82 | }, /*#__PURE__*/_react.default.createElement("label", { 83 | className: "metadata-label", 84 | htmlFor: "format" 85 | }, "Format:"), /*#__PURE__*/_react.default.createElement("select", { 86 | className: "metadata-input__input", 87 | name: "format", 88 | id: "format", 89 | value: (metadata.format || "").toLowerCase(), 90 | onChange: handleChange, 91 | required: true 92 | }, /*#__PURE__*/_react.default.createElement("option", { 93 | value: "", 94 | disabled: true 95 | }, "Select..."), _resource_formats.default.map(function (item) { 96 | return /*#__PURE__*/_react.default.createElement("option", { 97 | key: "format-".concat(item[0]), 98 | value: item[0].toLowerCase() 99 | }, item[0]); 100 | }))), customFields && customFields.map(function (item) { 101 | return /*#__PURE__*/_react.default.createElement("div", { 102 | key: "metadata-custom-".concat(item.name), 103 | className: "metadata-input" 104 | }, /*#__PURE__*/_react.default.createElement("label", { 105 | className: "metadata-label", 106 | htmlFor: "format" 107 | }, item.label, ":"), /*#__PURE__*/_react.default.createElement("select", { 108 | className: "metadata-input__input", 109 | name: item.name, 110 | id: item.name, 111 | value: metadata[item.name] || "", 112 | onChange: handleChange, 113 | required: true 114 | }, /*#__PURE__*/_react.default.createElement("option", { 115 | value: "", 116 | disabled: true 117 | }, "Select..."), item.options.map(function (option, index) { 118 | return /*#__PURE__*/_react.default.createElement("option", { 119 | key: "".concat(item.name, "-").concat(index), 120 | value: item.values[index] 121 | }, option); 122 | }))); 123 | }))); 124 | }; 125 | 126 | Metadata.propTypes = { 127 | metadata: _propTypes.default.object.isRequired, 128 | handleChange: _propTypes.default.func.isRequired 129 | }; 130 | var _default = Metadata; 131 | exports.default = _default; -------------------------------------------------------------------------------- /dist/components/ProgressBar/ProgressBar.css: -------------------------------------------------------------------------------- 1 | .svg{ 2 | display: block; 3 | margin: 20px auto; 4 | max-width: 100%; 5 | } 6 | 7 | .svg-circle-bg { 8 | fill: none; 9 | } 10 | 11 | .svg-circle { 12 | fill: none; 13 | } 14 | 15 | .svg-circle-text { 16 | font-size: 0.68rem; 17 | text-anchor: middle; 18 | margin-top: 30px; 19 | fill:#3F4656; 20 | font-weight: bold; 21 | } 22 | 23 | .time-remaining { 24 | font-size: 0.55rem; 25 | float: left; 26 | margin-top: -13px; 27 | color: #3F4656; 28 | } -------------------------------------------------------------------------------- /dist/components/ProgressBar/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports.default = void 0; 9 | 10 | var _react = _interopRequireWildcard(require("react")); 11 | 12 | var _propTypes = _interopRequireDefault(require("prop-types")); 13 | 14 | require("./ProgressBar.css"); 15 | 16 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 17 | 18 | function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function _getRequireWildcardCache() { return cache; }; return cache; } 19 | 20 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } 21 | 22 | function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); } 23 | 24 | function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } 25 | 26 | function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } 27 | 28 | function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } 29 | 30 | function _iterableToArrayLimit(arr, i) { if (typeof Symbol === "undefined" || !(Symbol.iterator in Object(arr))) return; var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } 31 | 32 | function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } 33 | 34 | var ProgressBar = function ProgressBar(props) { 35 | var _useState = (0, _react.useState)(0), 36 | _useState2 = _slicedToArray(_useState, 2), 37 | offset = _useState2[0], 38 | setOffset = _useState2[1]; 39 | 40 | var circleRef = (0, _react.useRef)(null); 41 | var size = props.size, 42 | progress = props.progress, 43 | strokeWidth = props.strokeWidth, 44 | circleOneStroke = props.circleOneStroke, 45 | circleTwoStroke = props.circleTwoStroke, 46 | timeRemaining = props.timeRemaining; 47 | var center = size / 2; 48 | var radius = size / 2 - strokeWidth / 2; 49 | var circumference = 2 * Math.PI * radius; 50 | (0, _react.useEffect)(function () { 51 | var progressOffset = (100 - progress) / 100 * circumference; 52 | setOffset(progressOffset); 53 | circleRef.current.style = "transition: stroke-dashoffset 850ms ease-in-out"; 54 | }, [setOffset, progress, circumference, offset]); 55 | return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement("svg", { 56 | className: "svg", 57 | width: size, 58 | height: size 59 | }, /*#__PURE__*/_react.default.createElement("circle", { 60 | className: "svg-circle-bg", 61 | stroke: circleOneStroke, 62 | cx: center, 63 | cy: center, 64 | r: radius, 65 | strokeWidth: strokeWidth 66 | }), /*#__PURE__*/_react.default.createElement("circle", { 67 | className: "svg-circle", 68 | ref: circleRef, 69 | stroke: circleTwoStroke, 70 | cx: center, 71 | cy: center, 72 | r: radius, 73 | strokeWidth: strokeWidth, 74 | strokeDasharray: circumference, 75 | strokeDashoffset: offset 76 | }), /*#__PURE__*/_react.default.createElement("text", { 77 | x: "".concat(center), 78 | y: "".concat(center + 2), 79 | className: "svg-circle-text" 80 | }, progress, "%")), timeRemaining > 0 && /*#__PURE__*/_react.default.createElement("span", { 81 | className: "time-remaining" 82 | }, timeRemaining > 60 ? "".concat(Math.floor(timeRemaining / 60), " minute").concat(Math.floor(timeRemaining / 60) > 1 ? 's' : '') : "".concat(Math.floor(timeRemaining), " second").concat(timeRemaining > 1 ? 's' : ''), " left")); 83 | }; 84 | 85 | ProgressBar.propTypes = { 86 | size: _propTypes.default.number.isRequired, 87 | progress: _propTypes.default.number.isRequired, 88 | strokeWidth: _propTypes.default.number.isRequired, 89 | circleOneStroke: _propTypes.default.string.isRequired, 90 | circleTwoStroke: _propTypes.default.string.isRequired 91 | }; 92 | var _default = ProgressBar; 93 | exports.default = _default; -------------------------------------------------------------------------------- /dist/components/Switcher/Switcher.css: -------------------------------------------------------------------------------- 1 | .switch-wrapper { 2 | color: #222222; 3 | display: grid; 4 | grid-template-columns: 160px 200px 160px; 5 | grid-template-areas: "switchOne divLine switchTwo"; 6 | width: 80%; 7 | align-items: center; 8 | justify-items: center; 9 | align-content: center; 10 | justify-content: center; 11 | margin: 1rem auto; 12 | } 13 | 14 | .switch-number { 15 | border-radius: 50%; 16 | width: 20px; 17 | height: 20px; 18 | padding: 3px; 19 | text-align: center; 20 | margin-bottom: 1.2rem; 21 | margin: 0 auto; 22 | cursor: pointer; 23 | } 24 | 25 | .switch-one { 26 | grid-area: switchOne; 27 | } 28 | 29 | .switch-two { 30 | grid-area: switchTwo; 31 | } 32 | 33 | .switch-number-selected { 34 | background-color: #00A7E1; 35 | color: #ffffff; 36 | } 37 | 38 | .switch-number-disabled { 39 | border: 1px solid #e3ebff; 40 | } 41 | 42 | .switch-description { 43 | margin-top: 1rem; 44 | width: 100%; 45 | cursor: pointer; 46 | } 47 | 48 | .switch-description-active { 49 | font-weight: bold; 50 | } 51 | 52 | .divider-line { 53 | grid-area: divLine; 54 | height: 2px; 55 | background-color: #e3ebff; 56 | width: 100%; 57 | margin-bottom: 3rem; 58 | } 59 | -------------------------------------------------------------------------------- /dist/components/Switcher/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = void 0; 7 | 8 | var _react = _interopRequireDefault(require("react")); 9 | 10 | var _propTypes = _interopRequireDefault(require("prop-types")); 11 | 12 | require("./Switcher.css"); 13 | 14 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 15 | 16 | var Switcher = function Switcher(props) { 17 | return /*#__PURE__*/_react.default.createElement("div", { 18 | className: "switch-wrapper" 19 | }, /*#__PURE__*/_react.default.createElement("div", { 20 | onClick: function onClick() { 21 | return props.switcher('metadata'); 22 | } 23 | }, /*#__PURE__*/_react.default.createElement("div", { 24 | className: "switch-number ".concat(props.metadataOrSchema === 'metadata' ? "switch-number-selected" : "switch-number-disabled") 25 | }, "1"), /*#__PURE__*/_react.default.createElement("p", { 26 | className: "switch-description \"switch-description-active\"}" 27 | }, "Edit Metadata")), /*#__PURE__*/_react.default.createElement("div", { 28 | onClick: function onClick() { 29 | return props.switcher('schema'); 30 | } 31 | }, /*#__PURE__*/_react.default.createElement("div", { 32 | className: "switch-number ".concat(props.metadataOrSchema === 'schema' ? "switch-number-selected" : "switch-number-disabled") 33 | }, "2"), /*#__PURE__*/_react.default.createElement("p", { 34 | className: "switch-description \"switch-description-active\"}" 35 | }, "Edit Schema")), /*#__PURE__*/_react.default.createElement("div", { 36 | className: "divider-line" 37 | })); 38 | }; 39 | 40 | Switcher.propTypes = { 41 | metadataOrSchema: _propTypes.default.string.isRequired 42 | }; 43 | var _default = Switcher; 44 | exports.default = _default; -------------------------------------------------------------------------------- /dist/components/TableSchema/TableSchema.css: -------------------------------------------------------------------------------- 1 | .table-container { 2 | margin-top: 2.6rem; 3 | margin-bottom: 2rem; 4 | display: flex; 5 | } 6 | 7 | .table-schema-info_container { 8 | overflow: overlay; 9 | } 10 | 11 | .table-schema-info_container::-webkit-scrollbar-track { 12 | -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.2); 13 | border-radius: 10px; 14 | background-color: #fff; 15 | } 16 | 17 | .table-schema-info_container::-webkit-scrollbar { 18 | width: 8px; 19 | height: 8px; 20 | background-color: #fff; 21 | } 22 | 23 | .table-schema-info_container::-webkit-scrollbar-thumb { 24 | border-radius: 10px; 25 | -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.2); 26 | background-color: #e3e9ed; 27 | } 28 | 29 | .table-schema-info_table { 30 | width: auto !important; 31 | } 32 | 33 | .table-schema-help { 34 | vertical-align: middle; 35 | } 36 | 37 | .table-schema-help_row { 38 | padding: 1.3rem; 39 | } 40 | 41 | .table-thead-tr { 42 | height: 50px; 43 | border-radius: 8px; 44 | } 45 | 46 | .table-thead-th { 47 | background: #f1f1f1; 48 | color: #363844; 49 | font-weight: bold; 50 | text-align: center; 51 | vertical-align: middle; 52 | position: sticky; 53 | top: 0; 54 | z-index: 10; 55 | } 56 | 57 | .table-tbody-help-tr { 58 | color: #363844; 59 | height: 40px; 60 | } 61 | 62 | .table-tbody-help-td { 63 | vertical-align: middle; 64 | font-weight: bold; 65 | } 66 | 67 | .table-tbody-help-td-empty { 68 | height: 50px; 69 | } 70 | 71 | .table-tbody-td { 72 | padding: 5px; 73 | border: solid 1px #f1f1f1; 74 | background: #fff; 75 | max-width: 100px; 76 | overflow: hidden; 77 | text-overflow: ellipsis; 78 | white-space: nowrap; 79 | font-size: 14px; 80 | } 81 | 82 | .table-tbody-input { 83 | border: none; 84 | padding: 0 10px 0 10px; 85 | height: 40px; 86 | } 87 | 88 | .table-tbody-select { 89 | position: relative; 90 | display: inline-block; 91 | vertical-align: middle; 92 | background-color: #fafafa; 93 | width: 100%; 94 | border: none; 95 | padding: 10px; 96 | height: 40px; 97 | } 98 | 99 | .table-btn-save { 100 | padding: 10px; 101 | width: 90%; 102 | background-color: #4fa8fd; 103 | border: none; 104 | border-radius: 3px; 105 | color: #fff; 106 | font-weight: 500; 107 | cursor: pointer; 108 | outline: 0; 109 | } 110 | 111 | .table-btn-save:disabled { 112 | background: #e3e9ed !important; 113 | cursor: default; 114 | } 115 | -------------------------------------------------------------------------------- /dist/components/TableSchema/TableSchema.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _react = _interopRequireDefault(require("react")); 4 | 5 | var _enzyme = require("enzyme"); 6 | 7 | var _ = _interopRequireDefault(require(".")); 8 | 9 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 10 | 11 | describe("", function () { 12 | var onChange = jest.fn(function () { 13 | return "https://www.datopian.com"; 14 | }); 15 | it("render TableSchema without crashing", function () { 16 | var wrapper = (0, _enzyme.shallow)( /*#__PURE__*/_react.default.createElement(_.default, { 17 | data: [], 18 | schema: { 19 | fields: [] 20 | } 21 | })); 22 | expect(wrapper.contains("Title")).toEqual(true); 23 | expect(wrapper.contains("Description")).toEqual(true); 24 | expect(wrapper.contains("Type")).toEqual(true); 25 | expect(wrapper.contains("Format")).toEqual(true); 26 | }); 27 | }); -------------------------------------------------------------------------------- /dist/components/TableSchema/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports.default = void 0; 9 | 10 | var _react = _interopRequireWildcard(require("react")); 11 | 12 | var _propTypes = _interopRequireDefault(require("prop-types")); 13 | 14 | var _reactTable = require("react-table"); 15 | 16 | var _types = _interopRequireDefault(require("../../db/types.json")); 17 | 18 | require("./TableSchema.css"); 19 | 20 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 21 | 22 | function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function _getRequireWildcardCache() { return cache; }; return cache; } 23 | 24 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } 25 | 26 | function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } 27 | 28 | function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } 29 | 30 | function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } 31 | 32 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 33 | 34 | function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); } 35 | 36 | function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } 37 | 38 | function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && Symbol.iterator in Object(iter)) return Array.from(iter); } 39 | 40 | function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); } 41 | 42 | function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); } 43 | 44 | function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } 45 | 46 | function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } 47 | 48 | function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } 49 | 50 | function _iterableToArrayLimit(arr, i) { if (typeof Symbol === "undefined" || !(Symbol.iterator in Object(arr))) return; var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } 51 | 52 | function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } 53 | 54 | var TableSchema = function TableSchema(props) { 55 | var _useState = (0, _react.useState)(props.schema), 56 | _useState2 = _slicedToArray(_useState, 2), 57 | schema = _useState2[0], 58 | setSchema = _useState2[1]; // eslint-disable-next-line react-hooks/exhaustive-deps 59 | 60 | 61 | var data = _react.default.useMemo(function () { 62 | return _toConsumableArray(props.data); 63 | }, [schema]); 64 | 65 | var columnsSchema = schema.fields.map(function (item, index) { 66 | return { 67 | Header: item.name ? item.name : "column_".concat(index + 1), 68 | accessor: item.name ? item.name : "column_".concat(index + 1) 69 | }; 70 | }); // eslint-disable-next-line react-hooks/exhaustive-deps 71 | 72 | var columns = _react.default.useMemo(function () { 73 | return _toConsumableArray(columnsSchema); 74 | }, [schema]); 75 | 76 | var _useTable = (0, _reactTable.useTable)({ 77 | columns: columns, 78 | data: data 79 | }), 80 | getTableProps = _useTable.getTableProps, 81 | getTableBodyProps = _useTable.getTableBodyProps, 82 | headerGroups = _useTable.headerGroups, 83 | rows = _useTable.rows, 84 | prepareRow = _useTable.prepareRow; 85 | 86 | var handleChange = function handleChange(event, key, index) { 87 | var newSchema = _objectSpread({}, schema); 88 | 89 | newSchema.fields[index][key] = event.target.value; 90 | setSchema(newSchema); 91 | }; //if the the user upload a new file, will update the state 92 | //and render with the new values 93 | 94 | 95 | (0, _react.useEffect)(function () { 96 | setSchema(props.schema); 97 | }, [props.schema]); 98 | 99 | var renderEditSchemaField = function renderEditSchemaField(key) { 100 | if (key === "type") { 101 | return schema.fields.map(function (item, index) { 102 | return /*#__PURE__*/_react.default.createElement("td", { 103 | key: "schema-type-field-".concat(key, "-").concat(index) 104 | }, /*#__PURE__*/_react.default.createElement("select", { 105 | className: "table-tbody-select", 106 | value: item[key] || "", 107 | onChange: function onChange(event) { 108 | return handleChange(event, key, index); 109 | } 110 | }, _types.default.type.map(function (item, index) { 111 | return /*#__PURE__*/_react.default.createElement("option", { 112 | key: "schema-type-field-option-".concat(item, "-").concat(index), 113 | value: item 114 | }, item); 115 | }))); 116 | }); 117 | } 118 | 119 | return schema.fields.map(function (item, index) { 120 | return /*#__PURE__*/_react.default.createElement("td", { 121 | key: "schema-field-".concat(key, "-").concat(index) 122 | }, /*#__PURE__*/_react.default.createElement("input", { 123 | className: "table-tbody-input", 124 | type: "text", 125 | value: item[key], 126 | onChange: function onChange(event) { 127 | return handleChange(event, key, index); 128 | } 129 | })); 130 | }); 131 | }; 132 | 133 | return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement("div", { 134 | className: "table-container" 135 | }, /*#__PURE__*/_react.default.createElement("table", { 136 | className: "table-schema-help" 137 | }, /*#__PURE__*/_react.default.createElement("tbody", null, /*#__PURE__*/_react.default.createElement("tr", { 138 | className: "table-tbody-help-tr" 139 | }, /*#__PURE__*/_react.default.createElement("td", { 140 | className: "table-tbody-help-td-empty" 141 | })), /*#__PURE__*/_react.default.createElement("tr", { 142 | className: "table-tbody-help-tr" 143 | }, /*#__PURE__*/_react.default.createElement("td", { 144 | className: "table-tbody-help-td" 145 | }, "Title")), /*#__PURE__*/_react.default.createElement("tr", { 146 | className: "table-tbody-help-tr" 147 | }, /*#__PURE__*/_react.default.createElement("td", { 148 | className: "table-tbody-help-td" 149 | }, "Description")), /*#__PURE__*/_react.default.createElement("tr", { 150 | className: "table-tbody-help-tr" 151 | }, /*#__PURE__*/_react.default.createElement("td", { 152 | className: "table-tbody-help-td" 153 | }, "Type")), /*#__PURE__*/_react.default.createElement("tr", { 154 | className: "table-tbody-help-tr" 155 | }, /*#__PURE__*/_react.default.createElement("td", { 156 | className: "table-tbody-help-td" 157 | }, "Format")))), /*#__PURE__*/_react.default.createElement("div", { 158 | className: "table-schema-info_container" 159 | }, /*#__PURE__*/_react.default.createElement("table", _extends({ 160 | className: "table-schema-info_table" 161 | }, getTableProps()), /*#__PURE__*/_react.default.createElement("thead", null, headerGroups.map(function (headerGroup) { 162 | return /*#__PURE__*/_react.default.createElement("tr", _extends({ 163 | className: "table-thead-tr" 164 | }, headerGroup.getHeaderGroupProps()), headerGroup.headers.map(function (column) { 165 | return /*#__PURE__*/_react.default.createElement("th", _extends({ 166 | className: "table-thead-th" 167 | }, column.getHeaderProps()), column.render("Header")); 168 | })); 169 | })), /*#__PURE__*/_react.default.createElement("tbody", getTableBodyProps(), /*#__PURE__*/_react.default.createElement("tr", { 170 | className: "table-tbody-tr-help" 171 | }, renderEditSchemaField("title")), /*#__PURE__*/_react.default.createElement("tr", { 172 | className: "table-tbody-tr-help" 173 | }, renderEditSchemaField("description")), /*#__PURE__*/_react.default.createElement("tr", null, renderEditSchemaField("type")), /*#__PURE__*/_react.default.createElement("tr", null, renderEditSchemaField("format")), rows.map(function (row) { 174 | prepareRow(row); 175 | return /*#__PURE__*/_react.default.createElement("tr", row.getRowProps(), row.cells.map(function (cell) { 176 | return /*#__PURE__*/_react.default.createElement("td", _extends({}, cell.getCellProps(), { 177 | className: "table-tbody-td" 178 | }), cell.render("Cell")); 179 | })); 180 | })))))); 181 | }; 182 | 183 | TableSchema.propTypes = { 184 | schema: _propTypes.default.object.isRequired, 185 | data: _propTypes.default.array.isRequired 186 | }; 187 | var _default = TableSchema; 188 | exports.default = _default; -------------------------------------------------------------------------------- /dist/components/Upload/Upload.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _react = _interopRequireDefault(require("react")); 4 | 5 | var _react2 = require("@testing-library/react"); 6 | 7 | var _reactDom = _interopRequireDefault(require("react-dom")); 8 | 9 | var _enzyme = require("enzyme"); 10 | 11 | var _ = _interopRequireDefault(require(".")); 12 | 13 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 14 | 15 | describe("", function () { 16 | it("render Upload without crashing", function () { 17 | var div = document.createElement("div"); 18 | 19 | _reactDom.default.render( /*#__PURE__*/_react.default.createElement(_.default, null), div); 20 | }); 21 | it.skip('can select a file', function () { 22 | var wrapper = (0, _enzyme.shallow)( /*#__PURE__*/_react.default.createElement(_.default, null)); 23 | var input = wrapper.find('input'); 24 | input.simulate('change', { 25 | target: { 26 | files: ['sample.csv'] 27 | } 28 | }); 29 | expect(wrapper.state().selectedFile).toEqual('sample.csv'); 30 | }); 31 | }); -------------------------------------------------------------------------------- /dist/components/Upload/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = void 0; 7 | 8 | var _react = _interopRequireDefault(require("react")); 9 | 10 | var data = _interopRequireWildcard(require("frictionless.js")); 11 | 12 | var _streamToArray = _interopRequireDefault(require("stream-to-array")); 13 | 14 | var _ProgressBar = _interopRequireDefault(require("../ProgressBar")); 15 | 16 | var _utils = require("../../utils"); 17 | 18 | var _Choose = _interopRequireDefault(require("../Choose")); 19 | 20 | function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function _getRequireWildcardCache() { return cache; }; return cache; } 21 | 22 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } 23 | 24 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 25 | 26 | function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 27 | 28 | function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } } 29 | 30 | function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; } 31 | 32 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 33 | 34 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } 35 | 36 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } 37 | 38 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); } 39 | 40 | function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } 41 | 42 | function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; } 43 | 44 | function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); } 45 | 46 | function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } 47 | 48 | function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } } 49 | 50 | function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } 51 | 52 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 53 | 54 | var Upload = /*#__PURE__*/function (_React$Component) { 55 | _inherits(Upload, _React$Component); 56 | 57 | var _super = _createSuper(Upload); 58 | 59 | function Upload(props) { 60 | var _this; 61 | 62 | _classCallCheck(this, Upload); 63 | 64 | _this = _super.call(this, props); 65 | 66 | _defineProperty(_assertThisInitialized(_this), "onChangeHandler", /*#__PURE__*/function () { 67 | var _ref = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee(event) { 68 | var _this$state, formattedSize, selectedFile, file, rowStream, hash; 69 | 70 | return regeneratorRuntime.wrap(function _callee$(_context) { 71 | while (1) { 72 | switch (_context.prev = _context.next) { 73 | case 0: 74 | _this$state = _this.state, formattedSize = _this$state.formattedSize, selectedFile = _this$state.selectedFile; 75 | 76 | if (!(event.target.files.length > 0)) { 77 | _context.next = 23; 78 | break; 79 | } 80 | 81 | selectedFile = event.target.files[0]; 82 | file = data.open(selectedFile); 83 | _context.prev = 4; 84 | _context.next = 7; 85 | return file.rows({ 86 | size: 20, 87 | keyed: true 88 | }); 89 | 90 | case 7: 91 | rowStream = _context.sent; 92 | _context.next = 10; 93 | return (0, _streamToArray.default)(rowStream); 94 | 95 | case 10: 96 | file.descriptor.sample = _context.sent; 97 | _context.next = 13; 98 | return file.addSchema(); 99 | 100 | case 13: 101 | _context.next = 18; 102 | break; 103 | 104 | case 15: 105 | _context.prev = 15; 106 | _context.t0 = _context["catch"](4); 107 | console.error(_context.t0); 108 | 109 | case 18: 110 | formattedSize = (0, _utils.onFormatBytes)(file.size); 111 | _context.next = 21; 112 | return file.hashSha256(); 113 | 114 | case 21: 115 | hash = _context.sent; 116 | 117 | _this.props.metadataHandler(Object.assign(file.descriptor, { 118 | hash: hash 119 | })); 120 | 121 | case 23: 122 | _this.setState({ 123 | selectedFile: selectedFile, 124 | loaded: 0, 125 | success: false, 126 | fileExists: false, 127 | error: false, 128 | formattedSize: formattedSize 129 | }); 130 | 131 | _this.onClickHandler(); 132 | 133 | case 25: 134 | case "end": 135 | return _context.stop(); 136 | } 137 | } 138 | }, _callee, null, [[4, 15]]); 139 | })); 140 | 141 | return function (_x) { 142 | return _ref.apply(this, arguments); 143 | }; 144 | }()); 145 | 146 | _defineProperty(_assertThisInitialized(_this), "onUploadProgress", function (progressEvent) { 147 | _this.onTimeRemaining(progressEvent.loaded); 148 | 149 | _this.setState({ 150 | loaded: progressEvent.loaded / progressEvent.total * 100 151 | }); 152 | }); 153 | 154 | _defineProperty(_assertThisInitialized(_this), "onTimeRemaining", function (progressLoaded) { 155 | var end = new Date().getTime(); 156 | var duration = (end - _this.state.start) / 1000; 157 | var bps = progressLoaded / duration; 158 | var kbps = bps / 1024; 159 | var timeRemaining = (_this.state.fileSize - progressLoaded) / kbps; 160 | 161 | _this.setState({ 162 | timeRemaining: timeRemaining / 1000 163 | }); 164 | }); 165 | 166 | _defineProperty(_assertThisInitialized(_this), "onClickHandler", /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee2() { 167 | var start, selectedFile, client, resource; 168 | return regeneratorRuntime.wrap(function _callee2$(_context2) { 169 | while (1) { 170 | switch (_context2.prev = _context2.next) { 171 | case 0: 172 | start = new Date().getTime(); 173 | selectedFile = _this.state.selectedFile; 174 | client = _this.props.client; 175 | resource = data.open(selectedFile); 176 | 177 | _this.setState({ 178 | fileSize: resource.size, 179 | start: start, 180 | loading: true 181 | }); 182 | 183 | _this.props.handleUploadStatus({ 184 | loading: true, 185 | error: false, 186 | success: false 187 | }); // Use client to upload file to the storage and track the progress 188 | 189 | 190 | client.pushBlob(resource, _this.onUploadProgress).then(function (response) { 191 | _this.setState({ 192 | success: response.success, 193 | loading: false, 194 | fileExists: response.fileExists, 195 | loaded: 100 196 | }); 197 | 198 | _this.props.handleUploadStatus({ 199 | loading: false, 200 | success: response.success 201 | }); 202 | }).catch(function (error) { 203 | _this.setState({ 204 | error: true, 205 | loading: false 206 | }); 207 | 208 | _this.props.handleUploadStatus({ 209 | loading: false, 210 | success: false, 211 | error: true 212 | }); 213 | }); 214 | 215 | case 7: 216 | case "end": 217 | return _context2.stop(); 218 | } 219 | } 220 | }, _callee2); 221 | }))); 222 | 223 | _this.state = { 224 | datasetId: props.datasetId, 225 | selectedFile: null, 226 | fileSize: 0, 227 | formattedSize: "0 KB", 228 | start: "", 229 | loaded: 0, 230 | success: false, 231 | error: false, 232 | fileExists: false, 233 | loading: false, 234 | timeRemaining: 0 235 | }; 236 | return _this; 237 | } 238 | 239 | _createClass(Upload, [{ 240 | key: "render", 241 | value: function render() { 242 | var _this$state2 = this.state, 243 | success = _this$state2.success, 244 | fileExists = _this$state2.fileExists, 245 | error = _this$state2.error, 246 | timeRemaining = _this$state2.timeRemaining, 247 | selectedFile = _this$state2.selectedFile, 248 | formattedSize = _this$state2.formattedSize; 249 | return /*#__PURE__*/_react.default.createElement("div", { 250 | className: "upload-area" 251 | }, /*#__PURE__*/_react.default.createElement(_Choose.default, { 252 | onChangeHandler: this.onChangeHandler, 253 | onChangeUrl: function onChangeUrl(event) { 254 | return console.log("Get url:", event.target.value); 255 | } 256 | }), /*#__PURE__*/_react.default.createElement("div", { 257 | className: "upload-area__info" 258 | }, selectedFile && /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement("ul", { 259 | className: "upload-list" 260 | }, /*#__PURE__*/_react.default.createElement("li", { 261 | className: "list-item" 262 | }, /*#__PURE__*/_react.default.createElement("div", { 263 | className: "upload-list-item" 264 | }, /*#__PURE__*/_react.default.createElement("div", null, /*#__PURE__*/_react.default.createElement("p", { 265 | className: "upload-file-name" 266 | }, selectedFile.name), /*#__PURE__*/_react.default.createElement("p", { 267 | className: "upload-file-size" 268 | }, formattedSize)), /*#__PURE__*/_react.default.createElement("div", null, /*#__PURE__*/_react.default.createElement(_ProgressBar.default, { 269 | progress: Math.round(this.state.loaded), 270 | size: 50, 271 | strokeWidth: 5, 272 | circleOneStroke: "#d9edfe", 273 | circleTwoStroke: "#7ea9e1", 274 | timeRemaining: timeRemaining 275 | }))))), /*#__PURE__*/_react.default.createElement("h2", { 276 | className: "upload-message" 277 | }, success && !fileExists && !error && "File uploaded successfully", fileExists && "File uploaded successfully", error && "Upload failed")))); 278 | } 279 | }]); 280 | 281 | return Upload; 282 | }(_react.default.Component); 283 | 284 | var _default = Upload; 285 | exports.default = _default; -------------------------------------------------------------------------------- /dist/core.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | Object.defineProperty(exports, "Choose", { 7 | enumerable: true, 8 | get: function get() { 9 | return _Choose.default; 10 | } 11 | }); 12 | Object.defineProperty(exports, "InputFile", { 13 | enumerable: true, 14 | get: function get() { 15 | return _InputFile.default; 16 | } 17 | }); 18 | Object.defineProperty(exports, "InputUrl", { 19 | enumerable: true, 20 | get: function get() { 21 | return _InputUrl.default; 22 | } 23 | }); 24 | Object.defineProperty(exports, "Metadata", { 25 | enumerable: true, 26 | get: function get() { 27 | return _Metadata.default; 28 | } 29 | }); 30 | Object.defineProperty(exports, "ProgressBar", { 31 | enumerable: true, 32 | get: function get() { 33 | return _ProgressBar.default; 34 | } 35 | }); 36 | Object.defineProperty(exports, "Switcher", { 37 | enumerable: true, 38 | get: function get() { 39 | return _Switcher.default; 40 | } 41 | }); 42 | Object.defineProperty(exports, "TableSchema", { 43 | enumerable: true, 44 | get: function get() { 45 | return _TableSchema.default; 46 | } 47 | }); 48 | Object.defineProperty(exports, "Upload", { 49 | enumerable: true, 50 | get: function get() { 51 | return _Upload.default; 52 | } 53 | }); 54 | Object.defineProperty(exports, "encodeData", { 55 | enumerable: true, 56 | get: function get() { 57 | return _encode.default; 58 | } 59 | }); 60 | Object.defineProperty(exports, "formatData", { 61 | enumerable: true, 62 | get: function get() { 63 | return _resource_formats.default; 64 | } 65 | }); 66 | Object.defineProperty(exports, "licenses", { 67 | enumerable: true, 68 | get: function get() { 69 | return _licenses.default; 70 | } 71 | }); 72 | Object.defineProperty(exports, "types", { 73 | enumerable: true, 74 | get: function get() { 75 | return _types.default; 76 | } 77 | }); 78 | 79 | var _Choose = _interopRequireDefault(require("./components/Choose")); 80 | 81 | var _InputFile = _interopRequireDefault(require("./components/InputFile")); 82 | 83 | var _InputUrl = _interopRequireDefault(require("./components/InputUrl")); 84 | 85 | var _Metadata = _interopRequireDefault(require("./components/Metadata")); 86 | 87 | var _ProgressBar = _interopRequireDefault(require("./components/ProgressBar")); 88 | 89 | var _Switcher = _interopRequireDefault(require("./components/Switcher")); 90 | 91 | var _TableSchema = _interopRequireDefault(require("./components/TableSchema")); 92 | 93 | var _Upload = _interopRequireDefault(require("./components/Upload")); 94 | 95 | var _encode = _interopRequireDefault(require("./db/encode.json")); 96 | 97 | var _resource_formats = _interopRequireDefault(require("./db/resource_formats.json")); 98 | 99 | var _licenses = _interopRequireDefault(require("./db/licenses.json")); 100 | 101 | var _types = _interopRequireDefault(require("./db/types.json")); 102 | 103 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } -------------------------------------------------------------------------------- /dist/db/encode.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "value": "utf_8", 4 | "label": "UTF-8" 5 | }, 6 | { 7 | "value": "iso_8859_1", 8 | "label": "ISO-8859-1" 9 | }, 10 | { 11 | "value": "windows_1251", 12 | "label": "Windows-1251" 13 | }, 14 | { 15 | "value": "windows_1252", 16 | "label": "Windows-1252" 17 | }, 18 | { 19 | "value": "shift_jis", 20 | "label": "Shift JIS" 21 | }, 22 | { 23 | "value": "gb2312", 24 | "label": "GB2312" 25 | }, 26 | { 27 | "value": "euc_kr", 28 | "label": "EUC-KR" 29 | }, 30 | { 31 | "value": "iso_8859_2", 32 | "label": "ISO-8859-2" 33 | }, 34 | { 35 | "value": "windows_1250", 36 | "label": "Windows-1250" 37 | }, 38 | { 39 | "value": "euc_jp", 40 | "label": "EUC-JP" 41 | }, 42 | { 43 | "value": "gbk", 44 | "label": "GBK" 45 | }, 46 | { 47 | "value": "big5", 48 | "label": "Big5" 49 | }, 50 | { 51 | "value": "iso_8859_15", 52 | "label": "ISO-8859-15" 53 | }, 54 | { 55 | "value": "windows_1256", 56 | "label": "Windows-1256" 57 | }, 58 | { 59 | "value": "iso_8859_9", 60 | "label": "ISO-8859-9" 61 | } 62 | ] -------------------------------------------------------------------------------- /dist/db/licenses.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "value": "cc-by", 4 | "label": "Creative Commons Attribution" 5 | }, 6 | { 7 | "value": "cc-by-sa", 8 | "label": "Creative Commons Attribution Share-Alike" 9 | }, 10 | { 11 | "value": "cc-zero", 12 | "label": "Creative Commons CCZero" 13 | }, 14 | { 15 | "value": "cc-nc", 16 | "label": "Creative Commons Non-Commercial (Any)" 17 | }, 18 | { 19 | "value": "gfdl", 20 | "label": "GNU Free Documentation License" 21 | }, 22 | { 23 | "value": "notspecified", 24 | "label": "License not specified" 25 | }, 26 | { 27 | "value": "odc-by", 28 | "label": "Open Data Commons Attribution License" 29 | }, 30 | { 31 | "value": "odc-odbl", 32 | "label": "Open Data Commons Open Database License (ODbL)" 33 | }, 34 | { 35 | "value": "odc-pddl", 36 | "label": "Open Data Commons Public Domain Dedication and License (PDDL)" 37 | }, 38 | { 39 | "value": "other-at", 40 | "label": "Other (Attribution)" 41 | }, 42 | { 43 | "value": "other-nc", 44 | "label": "Other (Non-Commercial)" 45 | }, 46 | { 47 | "value": "other-closed", 48 | "label": "Other (Not Open)" 49 | }, 50 | { 51 | "value": "other-open", 52 | "label": "Other (Open)" 53 | }, 54 | { 55 | "value": "other-pd", 56 | "label": "Other (Public Domain)" 57 | }, 58 | { 59 | "value": "uk-ogl", 60 | "label": "UK Open Government Licence (OGL)" 61 | } 62 | ] -------------------------------------------------------------------------------- /dist/db/resource_formats.json: -------------------------------------------------------------------------------- 1 | [ 2 | ["PPTX", "Powerpoint OOXML Presentation", "application/vnd.openxmlformats-officedocument.presentationml.presentation", []], 3 | ["EXE", "Windows Executable Program", "application/x-msdownload", []], 4 | ["DOC", "Word Document", "application/msword", []], 5 | ["KML", "KML File", "application/vnd.google-earth.kml+xml", []], 6 | ["XLS", "Excel Document", "application/vnd.ms-excel", ["Excel", "application/msexcel", "application/x-msexcel", "application/x-ms-excel", "application/x-excel", "application/x-dos_ms_excel", "application/xls", "application/x-xls"]], 7 | ["WCS", "Web Coverage Service", "wcs", []], 8 | ["JS", "JavaScript", "application/x-javascript", []], 9 | ["MDB", "Access Database", "application/x-msaccess", []], 10 | ["NetCDF", "NetCDF File", "application/netcdf", []], 11 | ["ArcGIS Map Service", "ArcGIS Map Service", "ArcGIS Map Service", ["arcgis map service"]], 12 | ["TSV", "Tab Separated Values File", "text/tab-separated-values", ["text/tsv"]], 13 | ["WFS", "Web Feature Service", null, []], 14 | ["WMTS", "Web Map Tile Service", null, []], 15 | ["ArcGIS Online Map", "ArcGIS Online Map", "ArcGIS Online Map", ["web map application"]], 16 | ["Perl", "Perl Script", "text/x-perl", []], 17 | ["KMZ", "KMZ File", "application/vnd.google-earth.kmz+xml", ["application/vnd.google-earth.kmz"]], 18 | ["OWL", "Web Ontology Language", "application/owl+xml", []], 19 | ["N3", "N3 Triples", "application/x-n3", []], 20 | ["ZIP", "Zip File", "application/zip", ["zip", "http://purl.org/NET/mediatypes/application/zip"]], 21 | ["GZ", "Gzip File", "application/gzip", ["application/x-gzip"]], 22 | ["QGIS", "QGIS File", "application/x-qgis", []], 23 | ["ODS", "OpenDocument Spreadsheet", "application/vnd.oasis.opendocument.spreadsheet", []], 24 | ["ODT", "OpenDocument Text", "application/vnd.oasis.opendocument.text", []], 25 | ["JSON", "JavaScript Object Notation", "application/json", []], 26 | ["BMP", "Bitmap Image File", "image/x-ms-bmp", []], 27 | ["HTML", "Web Page", "text/html", ["htm", "http://purl.org/net/mediatypes/text/html"]], 28 | ["RAR", "RAR Compressed File", "application/rar", []], 29 | ["TIFF", "TIFF Image File", "image/tiff", []], 30 | ["ODB", "OpenDocument Database", "application/vnd.oasis.opendocument.database", []], 31 | ["TXT", "Text File", "text/plain", []], 32 | ["DCR", "Adobe Shockwave format", "application/x-director", []], 33 | ["ODF", "OpenDocument Math Formula", "application/vnd.oasis.opendocument.formula", []], 34 | ["ODG", "OpenDocument Image", "application/vnd.oasis.opendocument.graphics", []], 35 | ["XML", "XML File", "application/xml", ["text/xml", "http://purl.org/net/mediatypes/application/xml"]], 36 | ["XLSX", "Excel OOXML Spreadsheet", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", []], 37 | ["DOCX", "Word OOXML Document", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", []], 38 | ["BIN", "Binary Data", "application/octet-stream", ["bin"]], 39 | ["XSLT", "Extensible Stylesheet Language Transformations", "application/xslt+xml", []], 40 | ["WMS", "Web Mapping Service", "WMS", ["wms"]], 41 | ["SVG", "SVG vector image", "image/svg+xml", ["svg"]], 42 | ["PPT", "Powerpoint Presentation", "application/vnd.ms-powerpoint", []], 43 | ["ODP", "OpenDocument Presentation", "application/vnd.oasis.opendocument.presentation", []], 44 | ["JPEG", "JPG Image File", "image/jpeg", ["jpeg", "jpg"]], 45 | ["SPARQL", "SPARQL end-point", "application/sparql-results+xml", []], 46 | ["GIF", "GIF Image File", "image/gif", []], 47 | ["RDF", "RDF", "application/rdf+xml", ["rdf/xml"]], 48 | ["E00", " ARC/INFO interchange file format", "application/x-e00", []], 49 | ["PDF", "PDF File", "application/pdf", []], 50 | ["CSV", "Comma Separated Values File", "text/csv", ["text/comma-separated-values"]], 51 | ["ODC", "OpenDocument Chart", "application/vnd.oasis.opendocument.chart", []], 52 | ["Atom Feed", "Atom Feed", "application/atom+xml", []], 53 | ["MrSID", "MrSID", "image/x-mrsid", []], 54 | ["ArcGIS Map Preview", "ArcGIS Map Preview", "ArcGIS Map Preview", ["arcgis map preview"]], 55 | ["XYZ", "XYZ Chemical File", "chemical/x-xyz", []], 56 | ["MOP", "MOPAC Input format", "chemical/x-mopac-input", []], 57 | ["Esri REST", "Esri Rest API Endpoint", "Esri REST", ["arcgis_rest"]], 58 | ["dBase", "dBase Database", "application/x-dbf", ["dbf"]], 59 | ["MXD", "ESRI ArcGIS project file", "application/x-mxd", []], 60 | ["TAR", "TAR Compressed File", "application/x-tar", []], 61 | ["PNG", "PNG Image File", "image/png", []], 62 | ["RSS", "RSS feed", "application/rss+xml", []], 63 | ["GeoJSON", "Geographic JavaScript Object Notation", "application/geo+json", ["geojson"]], 64 | ["SHP", "Shapefile", null, ["esri shapefile"]], 65 | ["TORRENT", "Torrent", "application/x-bittorrent", ["bittorrent"]], 66 | ["ICS", "iCalendar", "text/calendar", ["ifb", "iCal"]], 67 | ["GTFS", "General Transit Feed Specification", null, []], 68 | ["XLSB", "Excel OOXML Spreadsheet - Binary Workbook", "application/vnd.ms-excel.sheet.binary.macroEnabled.12", []], 69 | ["XLSM", "Excel OOXML Spreadsheet - Macro-Enabled", "application/vnd.ms-excel.sheet.macroEnabled.12", []], 70 | ["XLTM", "Excel OOXML Spreadsheet - Macro-Enabled Template", "application/vnd.ms-excel.template.macroEnabled.12", []], 71 | ["XLAM", "Excel OOXML Spreadsheet - Macro-Enabled Add-In", "application/vnd.ms-excel.addin.macroEnabled.12", []], 72 | ["DOTX", "Word OOXML - Template", "application/vnd.openxmlformats-officedocument.wordprocessingml.template", []], 73 | ["DOCM", "Word OOXML - Macro Enabled", "application/vnd.ms-word.document.macroEnabled.12", []], 74 | ["DOTM", "Word OOXML Template - Macro Enabled", "application/vnd.ms-word.template.macroEnabled.12", []], 75 | ["XLTX", "Excel OOXML Spreadsheet - Template", "application/vnd.openxmlformats-officedocument.spreadsheetml.template", []], 76 | ["POTX", "PowerPoint OOXML Presentation - Template", "application/vnd.openxmlformats-officedocument.presentationml.template", []], 77 | ["PPSX", "PowerPoint Open XML - Slide Show", "application/vnd.openxmlformats-officedocument.presentationml.slideshow", []], 78 | ["PPAM", "PowerPoint Open XML - Macro-Enabled Add-In", "application/vnd.ms-powerpoint.addin.macroEnabled.12", []], 79 | ["PPTM", "PowerPoint Open XML - Macro-Enabled", "application/vnd.ms-powerpoint.presentation.macroEnabled.12", []], 80 | ["POTM", "PowerPoint Open XML - Macro-Enabled Template", "application/vnd.ms-powerpoint.template.macroEnabled.12", []], 81 | ["PPSM", "PowerPoint Open XML - Macro-Enabled Slide Show", "application/vnd.ms-powerpoint.slideshow.macroEnabled.12", []] 82 | ] -------------------------------------------------------------------------------- /dist/db/types.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": [ 3 | "string", 4 | "number", 5 | "integer", 6 | "boolean", 7 | "object", 8 | "array", 9 | "date", 10 | "time", 11 | "datetime", 12 | "year", 13 | "yearmonth", 14 | "duration", 15 | "geopoint", 16 | "geojson", 17 | "any" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | Object.defineProperty(exports, "ResourceEditor", { 9 | enumerable: true, 10 | get: function get() { 11 | return _App.ResourceEditor; 12 | } 13 | }); 14 | 15 | var _react = _interopRequireDefault(require("react")); 16 | 17 | var _reactDom = _interopRequireDefault(require("react-dom")); 18 | 19 | var _App = _interopRequireWildcard(require("./App")); 20 | 21 | function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function _getRequireWildcardCache() { return cache; }; return cache; } 22 | 23 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } 24 | 25 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 26 | 27 | function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); } 28 | 29 | function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } 30 | 31 | function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } 32 | 33 | function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } 34 | 35 | function _iterableToArrayLimit(arr, i) { if (typeof Symbol === "undefined" || !(Symbol.iterator in Object(arr))) return; var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } 36 | 37 | function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } 38 | 39 | // Mount the ResourceEditor app explicitly 40 | // (make sure you call the function after it's loaded) 41 | function mountResourceEditorApp() { 42 | var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ['root', { 43 | authToken: null, 44 | api: null, 45 | lfs: null, 46 | organizationId: null, 47 | datasetId: null, 48 | resourceId: null 49 | }, {}], 50 | _ref2 = _slicedToArray(_ref, 3), 51 | elementId = _ref2[0], 52 | config = _ref2[1], 53 | resource = _ref2[2]; 54 | 55 | _reactDom.default.render( /*#__PURE__*/_react.default.createElement(_react.default.StrictMode, null, /*#__PURE__*/_react.default.createElement(_App.default, { 56 | config: config, 57 | resource: resource 58 | })), document.getElementById(elementId)); 59 | } 60 | 61 | ; // Automatically mount the app if an element with id='ResourceEditor' exists 62 | 63 | var element = document.getElementById('ResourceEditor'); 64 | 65 | if (element) { 66 | var config = { 67 | datasetId: element.getAttribute('data-dataset-id'), 68 | api: element.getAttribute('data-api'), 69 | lfs: element.getAttribute('data-lfs'), 70 | authToken: element.getAttribute('data-auth-token'), 71 | organizationId: element.getAttribute('data-organization-id'), 72 | resourceId: element.getAttribute('data-resource-id') 73 | }; 74 | 75 | _reactDom.default.render( /*#__PURE__*/_react.default.createElement(_react.default.StrictMode, null, /*#__PURE__*/_react.default.createElement(_App.default, { 76 | config: config, 77 | resource: element.getAttribute('data-resource') 78 | })), element); 79 | } -------------------------------------------------------------------------------- /dist/setupTests.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | require("@testing-library/jest-dom/extend-expect"); 4 | 5 | var _enzyme = require("enzyme"); 6 | 7 | var _enzymeAdapterReact = _interopRequireDefault(require("enzyme-adapter-react-16")); 8 | 9 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 10 | 11 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 12 | // allows you to do things like: 13 | // expect(element).toHaveTextContent(/react/i) 14 | // learn more: https://github.com/testing-library/jest-dom 15 | (0, _enzyme.configure)({ 16 | adapter: new _enzymeAdapterReact.default() 17 | }); -------------------------------------------------------------------------------- /dist/utils/Utils.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _react = _interopRequireDefault(require("react")); 4 | 5 | var _reactDom = _interopRequireDefault(require("react-dom")); 6 | 7 | var _index = require("./index"); 8 | 9 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 10 | 11 | describe("Utils", function () { 12 | it('format size in Bytes, KB, MB, GB', function () { 13 | expect((0, _index.onFormatBytes)(100)).toEqual('100 Bytes'); 14 | expect((0, _index.onFormatBytes)(1000)).toEqual('1 KB'); 15 | expect((0, _index.onFormatBytes)(1222222)).toEqual('1.2 MB'); 16 | expect((0, _index.onFormatBytes)(1222222222)).toEqual('1.2 GB'); 17 | }); 18 | it('format title', function () { 19 | expect((0, _index.onFormatTitle)("sample-data.csv")).toEqual('sample data'); 20 | expect((0, _index.onFormatTitle)("sample_data.csv")).toEqual('sample data'); 21 | expect((0, _index.onFormatTitle)("sample-data_csv.csv")).toEqual('sample data csv'); 22 | expect((0, _index.onFormatTitle)("sampleData.csv")).toEqual('sampleData'); 23 | }); 24 | it('get file extension', function () { 25 | expect((0, _index.getFileExtension)("sample.csv")).toEqual('csv'); 26 | expect((0, _index.getFileExtension)("sample.html")).toEqual('html'); 27 | expect((0, _index.getFileExtension)("sample.xls")).toEqual('xls'); 28 | expect((0, _index.getFileExtension)("sampleData.doc")).toEqual('doc'); 29 | }); 30 | it('format name', function () { 31 | expect((0, _index.onFormatName)("sample.csv")).toEqual('sample'); 32 | expect((0, _index.onFormatName)("sample_data.html")).toEqual('sample_data'); 33 | expect((0, _index.onFormatName)("sample-data.xls")).toEqual('sample-data'); 34 | expect((0, _index.onFormatName)("sample-Data.doc")).toEqual('sample-Data'); 35 | }); 36 | it('remove hyphen from uuid', function () { 37 | expect((0, _index.removeHyphen)("fd77e419-32ae-4025-8d14-890343b605a3")).toEqual('fd77e41932ae40258d14890343b605a3'); 38 | expect((0, _index.removeHyphen)("9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d")).toEqual('9b1deb4d3b7d4bad9bdd2b0d7b3dcb6d'); 39 | expect((0, _index.removeHyphen)("should not broken without hyphen")).toEqual('should not broken without hyphen'); 40 | }); 41 | }); -------------------------------------------------------------------------------- /dist/utils/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.removeHyphen = exports.onFormatBytes = exports.onFormatName = exports.onFormatTitle = exports.getFileExtension = void 0; 7 | 8 | var getFileExtension = function getFileExtension(filename) { 9 | return /[.]/.exec(filename) ? /[^.]+$/.exec(filename)[0] : undefined; 10 | }; 11 | 12 | exports.getFileExtension = getFileExtension; 13 | 14 | var onFormatTitle = function onFormatTitle(name) { 15 | return name.replace(/\.[^/.]+$/, "").replace(/_/g, " ").replace(/-/g, " "); 16 | }; 17 | 18 | exports.onFormatTitle = onFormatTitle; 19 | 20 | var onFormatName = function onFormatName(name) { 21 | return name.replace(/\.[^/.]+$/, ""); 22 | }; 23 | 24 | exports.onFormatName = onFormatName; 25 | 26 | var onFormatBytes = function onFormatBytes(bytes) { 27 | var decimals = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1; 28 | if (bytes === 0) return "0 Bytes"; 29 | var k = 1000; 30 | var dm = decimals < 0 ? 0 : decimals; 31 | var sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; 32 | var i = Math.floor(Math.log(bytes) / Math.log(k)); 33 | return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i]; 34 | }; 35 | 36 | exports.onFormatBytes = onFormatBytes; 37 | 38 | var removeHyphen = function removeHyphen(id) { 39 | return id.replace(/-/g, ""); 40 | }; 41 | 42 | exports.removeHyphen = removeHyphen; -------------------------------------------------------------------------------- /examples/basic/assets/after-css.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datopian/datapub/d0662ab261e849ae47a45043a53eaa0689bd737a/examples/basic/assets/after-css.png -------------------------------------------------------------------------------- /examples/basic/assets/before-css.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datopian/datapub/d0662ab261e849ae47a45043a53eaa0689bd737a/examples/basic/assets/before-css.png -------------------------------------------------------------------------------- /examples/basic/assets/table-schema-example.md: -------------------------------------------------------------------------------- 1 | ## Adding TableSchema Component to your react application 2 | 3 | In this example, you'll learn how to use two Datapub components in your react application. By the end of this example, you'll have a working application like the one below: 4 | 5 | Datapub reusable page after test upload 6 | 7 | ### Pre-requisite 8 | 9 | - Have Node and Npm/Yarn installed 10 | - Have create-react-app 11 | 12 | ### Set-up 13 | In a new folder of your work space, open a terminal and create a new react application: 14 | ```bash 15 | create-react-app datapub-extend 16 | ``` 17 | Change directory into datapub-extend and run the application to ensure it was created successfully: 18 | ```bash 19 | cd datapub-extend 20 | yarn start 21 | ``` 22 | If you can see the react logo in your browser, then everything works fine. In the next section, you'll install Datapub and import some components for use. 23 | 24 | Open `datapub-extend` in your code editor, and add the following to your dependencies in `package.json`: 25 | 26 | ```json 27 | ... 28 | "datapub": "git+https://github.com/datopian/datapub.git", 29 | "ckan-client": "git+https://github.com/datopian/ckan-client-js.git", 30 | 31 | ... 32 | ``` 33 | 34 | Run `yarn` or `npm install` in your terminal to install Datapub. This installs the latest version of datapub and ckan-client from Github. 35 | 36 | In your App.js script in `src` folder, delete the auto generated contents and add the import statements below: 37 | 38 | ```javascript 39 | import React from 'react'; 40 | import { Client } from "ckan-client"; 41 | import PropTypes from "prop-types"; 42 | import frictionlessCkanMapper from "frictionless-ckan-mapper-js"; 43 | import { Upload, Metadata } from "datapub"; 44 | import './App.css'; 45 | ``` 46 | You're are creating a client for accessing CKAN using the ckan-client package, importing the frictionless-ckan-mapper for mapping schema, and most importantly importing two components (upload and metadata) from datapub. 47 | 48 | __Note:__ Available components that you can reuse from datapub are: 49 | - Metadata 50 | - Choose 51 | - InputFile 52 | - InputUrl 53 | - ProgressBar 54 | - Upload 55 | - TableSchema 56 | 57 | Next, you can add your custom `ResourceEditor`. Copy and paste the code below just in your App.js script: 58 | 59 | ```javascript 60 | 61 | export class ResourceEditor extends React.Component { 62 | constructor(props) { 63 | super(props); 64 | this.state = { 65 | datasetId: this.props.config.datasetId, 66 | resourceId: "", 67 | resource: this.props.resource || {}, 68 | ui: { 69 | fileOrLink: "", 70 | uploadComplete: undefined, 71 | success: false, 72 | error: false, 73 | loading: false, 74 | }, 75 | client: null, 76 | isResourceEdit: false, 77 | }; 78 | this.metadataHandler = this.metadataHandler.bind(this); 79 | } 80 | 81 | async componentDidMount() { 82 | const { config } = this.props; 83 | const { 84 | authToken, 85 | api, 86 | lfs, 87 | organizationId, 88 | datasetId, 89 | resourceId, 90 | } = config; 91 | 92 | const client = new Client( 93 | `${authToken}`, 94 | `${organizationId}`, 95 | `${datasetId}`, 96 | `${api}`, 97 | `${lfs}` 98 | ); 99 | 100 | //Check if the user is editing resource 101 | if (resourceId) { 102 | const resource = await client.action("resource_show", { id: resourceId }); 103 | const resourceSchema = await client.action("resource_schema_show", { 104 | id: resourceId, 105 | }); 106 | const resourceSample = await client.action("resource_sample_show", { 107 | id: resourceId, 108 | }); 109 | 110 | let resourceCopy = resource.result; 111 | let sampleCopy = []; 112 | 113 | try { 114 | // push the values to an array 115 | for (const property in resourceSample.result) { 116 | sampleCopy.push(resourceSample.result[property]); 117 | } 118 | // push sample as an array to be able to render in tableschema component 119 | resourceCopy.sample = sampleCopy; 120 | resourceCopy.schema = resourceSchema.result; 121 | } catch (e) { 122 | console.error(e); 123 | //generate empty values not to break the tableschema component 124 | resourceCopy.schema = { fields: [] }; 125 | resourceCopy.sample = []; 126 | } 127 | 128 | return this.setState({ 129 | client, 130 | resourceId, 131 | resource: resourceCopy, 132 | isResourceEdit: true, 133 | }); 134 | } 135 | 136 | this.setState({ client }); 137 | } 138 | 139 | metadataHandler(resource) { 140 | this.setState({ 141 | resource, 142 | }); 143 | } 144 | 145 | handleChangeMetadata = (event) => { 146 | const target = event.target; 147 | const value = target.value; 148 | const name = target.name; 149 | let resourceCopy = this.state.resource; 150 | resourceCopy[name] = value; 151 | 152 | this.setState({ 153 | resource: resourceCopy, 154 | }); 155 | }; 156 | 157 | handleSubmitMetadata = async () => { 158 | const { resource, client } = this.state; 159 | 160 | await this.createResource(resource); 161 | const isResourceCreate = true; 162 | if (isResourceCreate) { 163 | const datasetMetadata = await client.action("package_show", { 164 | id: this.state.datasetId, 165 | }); 166 | let result = datasetMetadata.result; 167 | 168 | if (result.state === "draft") { 169 | result.state = "active"; 170 | await client.action("package_update", result); 171 | } 172 | } 173 | 174 | // Redirect to dataset page 175 | return (window.location.href = `/dataset/${this.state.datasetId}`); 176 | }; 177 | 178 | createResource = async (resource) => { 179 | const { client } = this.state; 180 | const { config } = this.props; 181 | const { organizationId, datasetId, resourceId } = config; 182 | 183 | const ckanResource = frictionlessCkanMapper.resourceFrictionlessToCkan( 184 | resource 185 | ); 186 | 187 | //create a valid format from sample 188 | let data = { ...ckanResource.sample }; 189 | //delete sample because is an invalid format 190 | delete ckanResource.sample; 191 | 192 | // create a copy from ckanResource to add package_id, name, url, sha256,size, lfs_prefix, url, url_type 193 | // without this properties ckan-blob-storage doesn't work properly 194 | let ckanResourceCopy = { 195 | ...ckanResource, 196 | package_id: this.state.datasetId, 197 | name: resource.name || resource.title, 198 | sha256: resource.hash, 199 | size: resource.size, 200 | lfs_prefix: `${organizationId}/${datasetId}`, 201 | url: resource.name, 202 | url_type: "upload", 203 | sample: data, 204 | }; 205 | 206 | //Check if the user is editing resource, call resource_update and redirect to the dataset page 207 | if (resourceId) { 208 | ckanResourceCopy = { 209 | ...ckanResourceCopy, 210 | id: resourceId, 211 | }; 212 | await client.action("resource_update", ckanResourceCopy); 213 | 214 | return (window.location.href = `/dataset/${datasetId}`); 215 | } 216 | await client 217 | .action("resource_create", ckanResourceCopy) 218 | .then((response) => { 219 | this.onChangeResourceId(response.result.id); 220 | }); 221 | }; 222 | 223 | deleteResource = async () => { 224 | const { resource, client, datasetId } = this.state; 225 | if (window.confirm("Are you sure to delete this resource?")) { 226 | await client.action("resource_delete", { id: resource.id }); 227 | 228 | return (window.location.href = `/dataset/${datasetId}`); 229 | } 230 | }; 231 | 232 | handleUploadStatus = (status) => { 233 | const { ui } = this.state; 234 | const newUiState = { 235 | ...ui, 236 | success: status.success, 237 | error: status.error, 238 | loading: status.loading, 239 | }; 240 | 241 | this.setState({ ui: newUiState }); 242 | }; 243 | 244 | onChangeResourceId = (resourceId) => { 245 | this.setState({ resourceId }); 246 | }; 247 | } 248 | ``` 249 | 250 | # TODO: Some brief explanation on the ResourceEditor 251 | 252 | Next, you'll add two Datapub components (Upload and Metadata). At the end of `ResourceEditor` class, add the react render method, and initialize the Datapub components you have imported as shown below: 253 | 254 | ```javascript 255 | 256 | ... 257 | ... 258 | 259 | render() { 260 | const { success } = this.state.ui; 261 | 262 | return ( 263 |
264 |
{ 267 | event.preventDefault(); 268 | if (this.state.isResourceEdit) { 269 | return this.createResource(this.state.resource); 270 | } 271 | return this.handleSubmitMetadata(); 272 | }} 273 | > 274 |
275 |

Datapub GDX

276 |
277 | 278 | 286 | 287 |
288 | 292 | 293 | {!this.state.isResourceEdit ? ( 294 | 297 | ) : ( 298 |
299 | 306 | 307 |
308 | )} 309 |
310 | 311 |
312 | ); 313 | } 314 | ``` 315 | 316 | The Upload component requires 5 properties: 317 | - client 318 | - resource 319 | - metadataHandler 320 | - datasetId 321 | - handleUploadStatus 322 | - onChangeResourceId 323 | 324 | While the Metadata componet requires two properties 325 | - metadata 326 | - handleChange 327 | 328 | 329 | Finally, add this last bit of code to define your default configuration for the ResourceEditor before exporting the component. 330 | 331 | ```javascript 332 | 333 | ResourceEditor.defaultProps = { 334 | config: { 335 | authToken: "be270cae-1c77-4853-b8c1-30b6cf5e9878", 336 | api: "http://127.0.0.1:5000", 337 | lfs: "http://localhost:9419", 338 | organizationId: "myorg", 339 | datasetId: "sample_1", 340 | }, 341 | }; 342 | 343 | ResourceEditor.propTypes = { 344 | config: PropTypes.object.isRequired, 345 | }; 346 | 347 | export default ResourceEditor; 348 | ``` 349 | 350 | You can see the full App.js script [here](./code/reusable_datapub_basic/src/App.js) 351 | 352 | Next, open your `index.js` script, delete the existing code and paste the one below: 353 | 354 | ```javascript 355 | import React from 'react'; 356 | import ReactDOM from 'react-dom'; 357 | import App from './App'; 358 | 359 | 360 | const element = document.getElementById('ResourceEditor'); 361 | if (element) { 362 | const config = { 363 | datasetId: element.getAttribute('data-dataset-id'), 364 | api: element.getAttribute('data-api'), 365 | lfs: element.getAttribute('data-lfs'), 366 | authToken: element.getAttribute('data-auth-token'), 367 | organizationId: element.getAttribute('data-organization-id'), 368 | resourceId: element.getAttribute('data-resource-id') 369 | } 370 | 371 | ReactDOM.render( 372 | 373 | 377 | , 378 | element 379 | ); 380 | } 381 | 382 | 383 | export { ResourceEditor } from './App'; 384 | ``` 385 | 386 | This automatically mounts the ResourceEditor component in the your homepage if an element with the ID `ResourceEditor` exists. 387 | 388 | The `ResourceEditor` element does not exist since you've not created one, so open your index.html file in the public folder, delete the: 389 | 390 | ```html 391 |
392 | ``` 393 | and add: 394 | 395 | ```html 396 |
402 |
403 | ``` 404 | 405 | The properties `data-api`, `data-lfs`, `data-auth-token`, `data-dataset-id`, `data-organization-id` are for connecting to your Ckan instance using ckan-client. 406 | 407 | Now you're all set, you can start your application by running: 408 | ```bash 409 | yarn start 410 | ``` 411 | 412 | Your browser should open at port 3000 if its available and display something similar to the page below: 413 | 414 | Datapub reusable page before CSS 415 | 416 | You can customize styling, or add some pre-written ones. Copy the CSS style [here](./code/reusable_datapub_basic/src/App.css) and add to your App.css. 417 | 418 | Your app should now display like: 419 | 420 | Datapub reusable page after ading CSS 421 | 422 | Try uploading a file, and the Metadata will displayed. This proves that you have been able to successfully reuse the `Upload` and `Metadata` components from `Datapub`. 423 | 424 | Datapub reusable page after test upload 425 | 426 | __Note:__ The upload only failed because you do not have a Ckan client running. 427 | 428 | See the full code for this example project [here](./code/reusable_datapub_basic) 429 | 430 | ## See Also: -------------------------------------------------------------------------------- /examples/basic/assets/upload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datopian/datapub/d0662ab261e849ae47a45043a53eaa0689bd737a/examples/basic/assets/upload.png -------------------------------------------------------------------------------- /examples/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "datapub-extend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.3.2", 8 | "@testing-library/user-event": "^7.1.2", 9 | "react": "^16.14.0", 10 | "react-dom": "^16.14.0", 11 | "react-scripts": "3.4.3", 12 | "datapub": "git+https://github.com/datopian/datapub.git", 13 | "ckanClient": "git+https://github.com/datopian/ckan-client-js.git" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test", 19 | "eject": "react-scripts eject" 20 | }, 21 | "eslintConfig": { 22 | "extends": "react-app" 23 | }, 24 | "browserslist": { 25 | "production": [ 26 | ">0.2%", 27 | "not dead", 28 | "not op_mini all" 29 | ], 30 | "development": [ 31 | "last 1 chrome version", 32 | "last 1 firefox version", 33 | "last 1 safari version" 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /examples/basic/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datopian/datapub/d0662ab261e849ae47a45043a53eaa0689bd737a/examples/basic/public/favicon.ico -------------------------------------------------------------------------------- /examples/basic/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /examples/basic/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /examples/basic/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /examples/table-schema/assets/app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datopian/datapub/d0662ab261e849ae47a45043a53eaa0689bd737a/examples/table-schema/assets/app.png -------------------------------------------------------------------------------- /examples/table-schema/assets/table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datopian/datapub/d0662ab261e849ae47a45043a53eaa0689bd737a/examples/table-schema/assets/table.png -------------------------------------------------------------------------------- /examples/table-schema/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "table-schema-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.3.2", 8 | "@testing-library/user-event": "^7.1.2", 9 | "datapub": "0.2.2", 10 | "ckanClient": "git+https://github.com/datopian/ckan-client-js.git", 11 | "react": "^16.14.0", 12 | "react-dom": "^16.14.0", 13 | "react-scripts": "3.4.3" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test", 19 | "eject": "react-scripts eject" 20 | 21 | }, 22 | "eslintConfig": { 23 | "extends": "react-app" 24 | }, 25 | "browserslist": { 26 | "production": [ 27 | ">0.2%", 28 | "not dead", 29 | "not op_mini all" 30 | ], 31 | "development": [ 32 | "last 1 chrome version", 33 | "last 1 firefox version", 34 | "last 1 safari version" 35 | ] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /examples/table-schema/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /examples/table-schema/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /examples/table-schema/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /examples/table-schema/src/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 12 | monospace; 13 | } 14 | 15 | .App { 16 | text-align: center; 17 | min-height: 100vh; 18 | color: #222; 19 | } 20 | 21 | .upload-wrapper { 22 | width: 100%; 23 | min-width: 360px; 24 | max-width: 960px; 25 | margin: 0 auto; 26 | background-color: #fff; 27 | padding: 32px 24px; 28 | border-radius: 12px; 29 | min-height: 60%; 30 | display: grid; 31 | grid-template-columns: 1fr 1fr; 32 | grid-template-rows: auto 300px auto auto; 33 | grid-template-areas: 34 | "header header" 35 | "uploadArea uploadArea" 36 | "editArea editArea"; 37 | row-gap: 20px; 38 | column-gap: 20px; 39 | } 40 | 41 | .upload-header { 42 | grid-area: header; 43 | border-bottom: 1px solid #e3ebff; 44 | } 45 | 46 | .upload-header__title { 47 | color: #3f4656; 48 | font-weight: 400; 49 | } 50 | 51 | .upload-area { 52 | grid-area: uploadArea; 53 | display: grid; 54 | grid-template-columns: 40% auto; 55 | grid-template-rows: 1fr 1fr; 56 | grid-template-areas: "drop info"; 57 | align-items: center; 58 | } 59 | 60 | .upload-area__info { 61 | grid-area: info; 62 | padding-top: 20px; 63 | } 64 | 65 | .upload-list { 66 | padding: 0; 67 | } 68 | 69 | .list-item { 70 | display: flex; 71 | } 72 | 73 | .upload-list-item { 74 | margin: 0 auto; 75 | background-color: #fff; 76 | padding: 8px 24px; 77 | list-style: none; 78 | max-width: 480px; 79 | width: 85%; 80 | border-top-left-radius: 8px; 81 | border-bottom-left-radius: 8px; 82 | color: #222; 83 | display: flex; 84 | align-items: center; 85 | justify-content: space-between; 86 | box-shadow: 0 4px 11px 3px rgba(18, 22, 33, 0.05); 87 | } 88 | 89 | .upload-file-name { 90 | max-width: 19ch; 91 | white-space: nowrap; 92 | overflow: hidden; 93 | text-overflow: ellipsis; 94 | color: #3f4656; 95 | } 96 | 97 | .upload-file-size { 98 | text-align: left; 99 | font-size: 0.75rem; 100 | color: lightslategrey; 101 | } 102 | 103 | .upload-message { 104 | color: #3f4656; 105 | } 106 | 107 | .upload-switcher { 108 | grid-area: switcher; 109 | } 110 | 111 | .upload-edit-area { 112 | grid-area: editArea; 113 | } 114 | 115 | .btn { 116 | max-width: 220px; 117 | margin: 2rem; 118 | border-radius: 5px; 119 | padding: 10px 25px; 120 | height: 45px; 121 | font-size: 1.2rem; 122 | color: #fff; 123 | background-color: #00A7E1; 124 | outline: none; 125 | border: none; 126 | cursor: pointer; 127 | box-shadow: 0 4px 11px 3px rgba(18, 22, 33, 0.05); 128 | } 129 | 130 | .btn:disabled { 131 | background: #e3e9ed !important; 132 | cursor: default; 133 | } 134 | 135 | .btn:hover { 136 | background-color: #009ace; 137 | } 138 | 139 | .btn-delete { 140 | margin: 2rem; 141 | border-radius: 5px; 142 | padding: 10px 25px; 143 | height: 45px; 144 | font-size: 1.2rem; 145 | color: #fff; 146 | outline: none; 147 | border: none; 148 | cursor: pointer; 149 | box-shadow: 0 4px 11px 3px rgba(18, 22, 33, 0.05); 150 | background-color: #F11301; 151 | } 152 | 153 | .btn-delete :disabled { 154 | background: #e3e9ed !important; 155 | cursor: default; 156 | } 157 | 158 | .btn-delete:hover { 159 | background-color: #C70E00; 160 | } 161 | 162 | .resource-edit-actions { 163 | display: flex; 164 | justify-content: space-between; 165 | } -------------------------------------------------------------------------------- /examples/table-schema/src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Client } from "ckanClient"; 3 | import PropTypes from "prop-types"; 4 | import frictionlessCkanMapper from "frictionless-ckan-mapper-js"; 5 | import { Upload, Metadata, TableSchema } from "datapub"; 6 | 7 | import "./App.css"; 8 | 9 | export class ResourceEditor extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | this.state = { 13 | datasetId: this.props.config.datasetId, 14 | resourceId: "", 15 | resource: this.props.resource || {}, 16 | ui: { 17 | fileOrLink: "", 18 | uploadComplete: undefined, 19 | success: false, 20 | error: false, 21 | loading: false, 22 | }, 23 | client: null, 24 | isResourceEdit: false, 25 | }; 26 | this.metadataHandler = this.metadataHandler.bind(this); 27 | } 28 | 29 | async componentDidMount() { 30 | const { config } = this.props; 31 | const { 32 | authToken, 33 | api, 34 | lfs, 35 | organizationId, 36 | datasetId, 37 | resourceId, 38 | } = config; 39 | 40 | const client = new Client( 41 | `${authToken}`, 42 | `${organizationId}`, 43 | `${datasetId}`, 44 | `${api}`, 45 | `${lfs}` 46 | ); 47 | 48 | //Check if the user is editing resource 49 | if (resourceId) { 50 | const resource = await client.action("resource_show", { id: resourceId }); 51 | const resourceSchema = await client.action("resource_schema_show", { 52 | id: resourceId, 53 | }); 54 | const resourceSample = await client.action("resource_sample_show", { 55 | id: resourceId, 56 | }); 57 | 58 | let resourceCopy = resource.result; 59 | let sampleCopy = []; 60 | 61 | try { 62 | // push the values to an array 63 | for (const property in resourceSample.result) { 64 | sampleCopy.push(resourceSample.result[property]); 65 | } 66 | // push sample as an array to be able to render in tableschema component 67 | resourceCopy.sample = sampleCopy; 68 | resourceCopy.schema = resourceSchema.result; 69 | } catch (e) { 70 | console.error(e); 71 | //generate empty values not to break the tableschema component 72 | resourceCopy.schema = { fields: [] }; 73 | resourceCopy.sample = []; 74 | } 75 | 76 | return this.setState({ 77 | client, 78 | resourceId, 79 | resource: resourceCopy, 80 | isResourceEdit: true, 81 | }); 82 | } 83 | 84 | this.setState({ client }); 85 | } 86 | 87 | metadataHandler(resource) { 88 | this.setState({ 89 | resource, 90 | }); 91 | } 92 | 93 | handleChangeMetadata = (event) => { 94 | const target = event.target; 95 | const value = target.value; 96 | const name = target.name; 97 | let resourceCopy = this.state.resource; 98 | resourceCopy[name] = value; 99 | 100 | this.setState({ 101 | resource: resourceCopy, 102 | }); 103 | }; 104 | 105 | handleSubmitMetadata = async () => { 106 | const { resource, client } = this.state; 107 | 108 | await this.createResource(resource); 109 | const isResourceCreate = true; 110 | if (isResourceCreate) { 111 | const datasetMetadata = await client.action("package_show", { 112 | id: this.state.datasetId, 113 | }); 114 | let result = datasetMetadata.result; 115 | 116 | if (result.state === "draft") { 117 | result.state = "active"; 118 | await client.action("package_update", result); 119 | } 120 | } 121 | 122 | // Redirect to dataset page 123 | return (window.location.href = `/dataset/${this.state.datasetId}`); 124 | }; 125 | 126 | createResource = async (resource) => { 127 | const { client } = this.state; 128 | const { config } = this.props; 129 | const { organizationId, datasetId, resourceId } = config; 130 | 131 | const ckanResource = frictionlessCkanMapper.resourceFrictionlessToCkan( 132 | resource 133 | ); 134 | 135 | //create a valid format from sample 136 | let data = { ...ckanResource.sample }; 137 | //delete sample because is an invalid format 138 | delete ckanResource.sample; 139 | 140 | // create a copy from ckanResource to add package_id, name, url, sha256,size, lfs_prefix, url, url_type 141 | // without this properties ckan-blob-storage doesn't work properly 142 | let ckanResourceCopy = { 143 | ...ckanResource, 144 | package_id: this.state.datasetId, 145 | name: resource.name || resource.title, 146 | sha256: resource.hash, 147 | size: resource.size, 148 | lfs_prefix: `${organizationId}/${datasetId}`, 149 | url: resource.name, 150 | url_type: "upload", 151 | sample: data, 152 | }; 153 | 154 | //Check if the user is editing resource, call resource_update and redirect to the dataset page 155 | if (resourceId) { 156 | ckanResourceCopy = { 157 | ...ckanResourceCopy, 158 | id: resourceId, 159 | }; 160 | await client.action("resource_update", ckanResourceCopy); 161 | 162 | return (window.location.href = `/dataset/${datasetId}`); 163 | } 164 | await client 165 | .action("resource_create", ckanResourceCopy) 166 | .then((response) => { 167 | this.onChangeResourceId(response.result.id); 168 | }); 169 | }; 170 | 171 | deleteResource = async () => { 172 | const { resource, client, datasetId } = this.state; 173 | if (window.confirm("Are you sure to delete this resource?")) { 174 | await client.action("resource_delete", { id: resource.id }); 175 | 176 | return (window.location.href = `/dataset/${datasetId}`); 177 | } 178 | }; 179 | 180 | handleUploadStatus = (status) => { 181 | const { ui } = this.state; 182 | const newUiState = { 183 | ...ui, 184 | success: status.success, 185 | error: status.error, 186 | loading: status.loading, 187 | }; 188 | 189 | this.setState({ ui: newUiState }); 190 | }; 191 | 192 | onChangeResourceId = (resourceId) => { 193 | this.setState({ resourceId }); 194 | }; 195 | 196 | render() { 197 | const { success } = this.state.ui; 198 | 199 | return ( 200 |
201 |
{ 204 | event.preventDefault(); 205 | if (this.state.isResourceEdit) { 206 | return this.createResource(this.state.resource); 207 | } 208 | return this.handleSubmitMetadata(); 209 | }} 210 | > 211 |
212 |

Datapub Extend

213 |
214 | 215 | 223 | 224 |
225 | {this.state.resource.schema && ( 226 | 230 | )} 231 | {!this.state.isResourceEdit ? ( 232 | 235 | ) : ( 236 |
237 | 244 | 245 |
246 | )} 247 |
248 | 249 | 250 |
251 | ); 252 | } 253 | } 254 | 255 | 256 | ResourceEditor.defaultProps = { 257 | config: { 258 | authToken: "be270cae-1c77-4853-b8c1-30b6cf5e9878", 259 | api: "http://127.0.0.1:5000", 260 | lfs: "http://localhost:9419", 261 | organizationId: "myorg", 262 | datasetId: "sample_1", 263 | }, 264 | }; 265 | 266 | ResourceEditor.propTypes = { 267 | config: PropTypes.object.isRequired, 268 | }; 269 | 270 | export default ResourceEditor; -------------------------------------------------------------------------------- /examples/table-schema/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /examples/table-schema/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /examples/table-schema/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | 6 | const element = document.getElementById('ResourceEditor'); 7 | if (element) { 8 | const config = { 9 | datasetId: element.getAttribute('data-dataset-id'), 10 | api: element.getAttribute('data-api'), 11 | lfs: element.getAttribute('data-lfs'), 12 | authToken: element.getAttribute('data-auth-token'), 13 | organizationId: element.getAttribute('data-organization-id'), 14 | resourceId: element.getAttribute('data-resource-id') 15 | } 16 | 17 | ReactDOM.render( 18 | 19 | 23 | , 24 | element 25 | ); 26 | } 27 | 28 | 29 | export { ResourceEditor } from './App'; -------------------------------------------------------------------------------- /examples/table-schema/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "datapub", 3 | "version": "0.2.3", 4 | "private": false, 5 | "main": "dist/core.js", 6 | "module": "dist/core.js", 7 | "files": [ 8 | "dist", 9 | "README.md" 10 | ], 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/datopian/datapub" 14 | }, 15 | "dependencies": { 16 | "axios": "^0.19.2", 17 | "ckan-client": "git+https://github.com/datopian/ckan-client-js.git#v0.4.3", 18 | "frictionless-ckan-mapper-js": "^1.0.0", 19 | "frictionless.js": "^0.13.4", 20 | "react-scripts": "^3.4.3", 21 | "react-table": "^7.5.0", 22 | "stream-to-array": "^2.3.0", 23 | "uuidv4": "^6.2.3" 24 | }, 25 | "peerDependencies": { 26 | "react": "^16.13.0", 27 | "react-dom": "^16.13.0" 28 | }, 29 | "scripts": { 30 | "start": "react-scripts start", 31 | "build:ckan": "react-scripts build", 32 | "build": "rm -rf dist && NODE_ENV=production babel src --out-dir dist --copy-files --ignore __tests__,spec.js,test.js,__snapshots__", 33 | "dist": "rm -rf dist/ && mkdir dist && mkdir dist/css && NODE_ENV=production babel src/ -d dist/ --copy-files", 34 | "test": "react-scripts test", 35 | "test:watch": "npm run test -- --coverage --watchAll", 36 | "eject": "react-scripts eject", 37 | "storybook": "start-storybook -p 6006", 38 | "build-storybook": "build-storybook", 39 | "deploy-storybook": "storybook-to-ghpages" 40 | }, 41 | "eslintConfig": { 42 | "extends": "react-app" 43 | }, 44 | "browserslist": { 45 | "production": [ 46 | ">0.2%", 47 | "not dead", 48 | "not op_mini all" 49 | ], 50 | "development": [ 51 | "last 1 chrome version", 52 | "last 1 firefox version", 53 | "last 1 safari version" 54 | ] 55 | }, 56 | "devDependencies": { 57 | "@babel/cli": "^7.11.6", 58 | "@babel/core": "^7.11.1", 59 | "@babel/preset-env": "^7.11.5", 60 | "@babel/preset-react": "^7.10.4", 61 | "@storybook/addon-actions": "^6.0.16", 62 | "@storybook/addon-controls": "^6.0.16", 63 | "@storybook/addon-docs": "^6.0.21", 64 | "@storybook/addon-info": "^5.3.21", 65 | "@storybook/addon-links": "^6.0.16", 66 | "@storybook/addons": "^6.0.16", 67 | "@storybook/react": "^6.0.16", 68 | "@storybook/storybook-deployer": "^2.8.6", 69 | "@testing-library/jest-dom": "^4.2.4", 70 | "@testing-library/react": "^9.3.2", 71 | "@testing-library/user-event": "^7.1.2", 72 | "babel-loader": "^8.1.0", 73 | "enzyme": "^3.11.0", 74 | "enzyme-adapter-react-16": "^1.15.3", 75 | "eslint-plugin-react-hooks": "^4.1.2", 76 | "react-test-renderer": "^16.13.1" 77 | }, 78 | "babel": { 79 | "presets": [ 80 | "@babel/preset-env", 81 | "@babel/preset-react" 82 | ], 83 | "plugins": [ 84 | "@babel/plugin-proposal-class-properties" 85 | ] 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datopian/datapub/d0662ab261e849ae47a45043a53eaa0689bd737a/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datopian/datapub/d0662ab261e849ae47a45043a53eaa0689bd737a/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datopian/datapub/d0662ab261e849ae47a45043a53eaa0689bd737a/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 12 | monospace; 13 | } 14 | 15 | .App { 16 | text-align: center; 17 | min-height: 100vh; 18 | color: #222; 19 | } 20 | 21 | .upload-wrapper { 22 | width: 100%; 23 | min-width: 360px; 24 | max-width: 960px; 25 | margin: 0 auto; 26 | background-color: #fff; 27 | padding: 32px 24px; 28 | border-radius: 12px; 29 | min-height: 60%; 30 | display: grid; 31 | grid-template-columns: 1fr 1fr; 32 | grid-template-rows: auto 300px auto auto; 33 | grid-template-areas: 34 | "header header" 35 | "uploadArea uploadArea" 36 | "editArea editArea"; 37 | row-gap: 20px; 38 | column-gap: 20px; 39 | } 40 | 41 | .upload-header { 42 | grid-area: header; 43 | border-bottom: 1px solid #e3ebff; 44 | } 45 | 46 | .upload-header__title { 47 | color: #3f4656; 48 | font-weight: 400; 49 | } 50 | 51 | .upload-area { 52 | grid-area: uploadArea; 53 | display: grid; 54 | grid-template-columns: 40% auto; 55 | grid-template-rows: 1fr 1fr; 56 | grid-template-areas: "drop info"; 57 | align-items: center; 58 | } 59 | 60 | .upload-area__info { 61 | grid-area: info; 62 | padding-top: 20px; 63 | } 64 | 65 | .upload-list { 66 | padding: 0; 67 | } 68 | 69 | .list-item { 70 | display: flex; 71 | } 72 | 73 | .upload-list-item { 74 | margin: 0 auto; 75 | background-color: #fff; 76 | padding: 8px 24px; 77 | list-style: none; 78 | max-width: 480px; 79 | width: 85%; 80 | border-top-left-radius: 8px; 81 | border-bottom-left-radius: 8px; 82 | color: #222; 83 | display: flex; 84 | align-items: center; 85 | justify-content: space-between; 86 | box-shadow: 0 4px 11px 3px rgba(18, 22, 33, 0.05); 87 | } 88 | 89 | .upload-file-name { 90 | max-width: 19ch; 91 | white-space: nowrap; 92 | overflow: hidden; 93 | text-overflow: ellipsis; 94 | color: #3f4656; 95 | } 96 | 97 | .upload-file-size { 98 | text-align: left; 99 | font-size: 0.75rem; 100 | color: lightslategrey; 101 | } 102 | 103 | .upload-message { 104 | color: #3f4656; 105 | } 106 | 107 | .upload-switcher { 108 | grid-area: switcher; 109 | } 110 | 111 | .upload-edit-area { 112 | grid-area: editArea; 113 | } 114 | 115 | .btn { 116 | max-width: 220px; 117 | margin: 2rem; 118 | border-radius: 5px; 119 | padding: 10px 25px; 120 | height: 45px; 121 | font-size: 1.2rem; 122 | color: #fff; 123 | background-color: #00A7E1; 124 | outline: none; 125 | border: none; 126 | cursor: pointer; 127 | box-shadow: 0 4px 11px 3px rgba(18, 22, 33, 0.05); 128 | } 129 | 130 | .btn:disabled { 131 | background: #e3e9ed !important; 132 | cursor: default; 133 | } 134 | 135 | .btn:hover { 136 | background-color: #009ace; 137 | } 138 | 139 | .btn-delete { 140 | margin: 2rem; 141 | border-radius: 5px; 142 | padding: 10px 25px; 143 | height: 45px; 144 | font-size: 1.2rem; 145 | color: #fff; 146 | outline: none; 147 | border: none; 148 | cursor: pointer; 149 | box-shadow: 0 4px 11px 3px rgba(18, 22, 33, 0.05); 150 | background-color: #F11301; 151 | } 152 | 153 | .btn-delete :disabled { 154 | background: #e3e9ed !important; 155 | cursor: default; 156 | } 157 | 158 | .btn-delete:hover { 159 | background-color: #C70E00; 160 | } 161 | 162 | .resource-edit-actions { 163 | display: flex; 164 | justify-content: space-between; 165 | } -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Client } from "ckan-client"; 3 | import PropTypes from "prop-types"; 4 | import frictionlessCkanMapper from "frictionless-ckan-mapper-js"; 5 | import { v4 as uuidv4 } from "uuid"; 6 | 7 | import Metadata from "./components/Metadata"; 8 | import TableSchema from "./components/TableSchema"; 9 | import Upload from "./components/Upload"; 10 | import "./App.css"; 11 | import { removeHyphen } from "./utils"; 12 | 13 | export class ResourceEditor extends React.Component { 14 | constructor(props) { 15 | super(props); 16 | this.state = { 17 | datasetId: this.props.config.datasetId, 18 | resourceId: "", 19 | resource: this.props.resource || {}, 20 | ui: { 21 | fileOrLink: "", 22 | uploadComplete: undefined, 23 | success: false, 24 | error: false, 25 | loading: false, 26 | }, 27 | client: null, 28 | isResourceEdit: false, 29 | }; 30 | this.metadataHandler = this.metadataHandler.bind(this); 31 | } 32 | 33 | async componentDidMount() { 34 | const { config } = this.props; 35 | const { 36 | authToken, 37 | api, 38 | lfs, 39 | organizationId, 40 | datasetId, 41 | resourceId, 42 | } = config; 43 | 44 | const client = new Client( 45 | `${authToken}`, 46 | `${organizationId}`, 47 | `${datasetId}`, 48 | `${api}`, 49 | `${lfs}` 50 | ); 51 | 52 | //Check if the user is editing resource 53 | if (resourceId) { 54 | const resource = await client.action("resource_show", { id: resourceId }); 55 | const resourceSchema = await client.action("resource_schema_show", { 56 | id: resourceId, 57 | }); 58 | const resourceSample = await client.action("resource_sample_show", { 59 | id: resourceId, 60 | }); 61 | 62 | let resourceCopy = resource.result; 63 | let sampleCopy = []; 64 | 65 | try { 66 | // push the values to an array 67 | for (const property in resourceSample.result) { 68 | sampleCopy.push(resourceSample.result[property]); 69 | } 70 | // push sample as an array to be able to render in tableschema component 71 | resourceCopy.sample = sampleCopy; 72 | resourceCopy.schema = resourceSchema.result; 73 | } catch (e) { 74 | console.error(e); 75 | //generate empty values not to break the tableschema component 76 | resourceCopy.schema = { fields: [] }; 77 | resourceCopy.sample = []; 78 | } 79 | 80 | return this.setState({ 81 | client, 82 | resourceId, 83 | resource: resourceCopy, 84 | isResourceEdit: true, 85 | }); 86 | } 87 | 88 | this.setState({ client }); 89 | } 90 | 91 | metadataHandler(resource) { 92 | this.setState({ 93 | resource, 94 | }); 95 | } 96 | 97 | handleChangeMetadata = (event) => { 98 | const target = event.target; 99 | const value = target.value; 100 | const name = target.name; 101 | let resourceCopy = this.state.resource; 102 | resourceCopy[name] = value; 103 | 104 | this.setState({ 105 | resource: resourceCopy, 106 | }); 107 | }; 108 | 109 | handleSubmitMetadata = async () => { 110 | const { resource, client } = this.state; 111 | 112 | await this.createResource(resource); 113 | 114 | // Change state of dataset to active if draft atm 115 | // this relates to how CKAN v2 has a phased dataset creation. See e.g. 116 | // https://github.com/ckan/ckan/blob/master/ckan/controllers/package.py#L917 117 | 118 | // only need to do this test if in resource create mode if editing a 119 | // resource this is unnecessary 120 | // TODO: update this in future to check for edit mode 121 | const isResourceCreate = true; 122 | if (isResourceCreate) { 123 | const datasetMetadata = await client.action("package_show", { 124 | id: this.state.datasetId, 125 | }); 126 | let result = datasetMetadata.result; 127 | 128 | if (result.state == "draft") { 129 | result.state = "active"; 130 | await client.action("package_update", result); 131 | } 132 | } 133 | 134 | // Redirect to dataset page 135 | return (window.location.href = `/dataset/${this.state.datasetId}`); 136 | }; 137 | 138 | createResource = async (resource) => { 139 | const { client } = this.state; 140 | const { config } = this.props; 141 | const { organizationId, datasetId, resourceId } = config; 142 | 143 | const ckanResource = frictionlessCkanMapper.resourceFrictionlessToCkan( 144 | resource 145 | ); 146 | 147 | //create a valid format from sample 148 | let data = { ...ckanResource.sample }; 149 | //delete sample because is an invalid format 150 | delete ckanResource.sample; 151 | //generate an unique id for bq_table_name property 152 | let bqTableName = ckanResource.bq_table_name 153 | ? ckanResource.bq_table_name 154 | : uuidv4(); 155 | // create a copy from ckanResource to add package_id, name, url, sha256,size, lfs_prefix, url, url_type 156 | // without this properties ckan-blob-storage doesn't work properly 157 | let ckanResourceCopy = { 158 | ...ckanResource, 159 | package_id: this.state.datasetId, 160 | name: resource.name || resource.title, 161 | sha256: resource.hash, 162 | size: resource.size, 163 | lfs_prefix: `${organizationId}/${datasetId}`, 164 | url: resource.name, 165 | url_type: "upload", 166 | bq_table_name: removeHyphen(bqTableName), 167 | sample: data, 168 | }; 169 | 170 | //Check if the user is editing resource, call resource_update and redirect to the dataset page 171 | if (resourceId) { 172 | ckanResourceCopy = { 173 | ...ckanResourceCopy, 174 | id: resourceId, 175 | }; 176 | await client.action("resource_update", ckanResourceCopy); 177 | 178 | return (window.location.href = `/dataset/${datasetId}`); 179 | } 180 | await client 181 | .action("resource_create", ckanResourceCopy) 182 | .then((response) => { 183 | this.onChangeResourceId(response.result.id); 184 | }); 185 | }; 186 | 187 | deleteResource = async () => { 188 | const { resource, client, datasetId } = this.state; 189 | if (window.confirm("Are you sure to delete this resource?")) { 190 | await client.action("resource_delete", { id: resource.id }); 191 | 192 | return (window.location.href = `/dataset/${datasetId}`); 193 | } 194 | }; 195 | 196 | handleUploadStatus = (status) => { 197 | const { ui } = this.state; 198 | const newUiState = { 199 | ...ui, 200 | success: status.success, 201 | error: status.error, 202 | loading: status.loading, 203 | }; 204 | 205 | this.setState({ ui: newUiState }); 206 | }; 207 | 208 | onChangeResourceId = (resourceId) => { 209 | this.setState({ resourceId }); 210 | }; 211 | 212 | render() { 213 | const { loading, success } = this.state.ui; 214 | 215 | return ( 216 |
217 |
{ 220 | event.preventDefault(); 221 | if (this.state.isResourceEdit) { 222 | return this.createResource(this.state.resource); 223 | } 224 | return this.handleSubmitMetadata(); 225 | }} 226 | > 227 |
228 |

Resource Editor

229 |
230 | 231 | 239 | 240 |
241 | 245 | {this.state.resource.schema && ( 246 | 250 | )} 251 | {!this.state.isResourceEdit ? ( 252 | 255 | ) : ( 256 |
257 | 264 | 265 |
266 | )} 267 |
268 | 269 |
270 | ); 271 | } 272 | } 273 | 274 | /** 275 | * If the parent component doesn't specify a `config` and scope prop, then 276 | * the default values will be used. 277 | * */ 278 | ResourceEditor.defaultProps = { 279 | config: { 280 | authToken: "be270cae-1c77-4853-b8c1-30b6cf5e9878", 281 | api: "http://localhost:5000", 282 | lfs: "http://localhost:5001", // Feel free to modify this 283 | organizationId: "myorg", 284 | datasetId: "data-test-2", 285 | }, 286 | }; 287 | 288 | ResourceEditor.propTypes = { 289 | config: PropTypes.object.isRequired, 290 | }; 291 | 292 | export default ResourceEditor; 293 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { shallow } from "enzyme"; 3 | 4 | import App from "./App"; 5 | 6 | describe("", () => { 7 | 8 | it("should render in add a resource", () => { 9 | const wrapper = shallow(); 10 | 11 | expect(wrapper.find(".btn")).toHaveLength(1) 12 | expect(wrapper.find(".btn-delete")).toHaveLength(0) 13 | }); 14 | }) -------------------------------------------------------------------------------- /src/assets/images/computing-cloud.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/components/Choose/Choose.css: -------------------------------------------------------------------------------- 1 | .upload-choose { 2 | 3 | } 4 | 5 | .choose-btn { 6 | padding: 10px; 7 | width: 90%; 8 | background-color: #4FA8FD; 9 | border: none; 10 | border-radius: 3px; 11 | color: #fff; 12 | font-weight: 500; 13 | cursor: pointer; 14 | outline: 0; 15 | font-weight: bold; 16 | } 17 | .choose-text { 18 | font-size: 1rem; 19 | font-weight: 300; 20 | } 21 | -------------------------------------------------------------------------------- /src/components/Choose/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import PropTypes from "prop-types"; 3 | import InputFile from "../InputFile"; 4 | import InputUrl from "../InputUrl"; 5 | import './Choose.css' 6 | 7 | const Choose = ({ onChangeUrl, onChangeHandler }) => { 8 | const [uploadOption, setUploadOption] = useState(false); 9 | 10 | return ( 11 |
12 | {uploadOption ? ( 13 | <> 14 | {uploadOption === "file" && ( 15 | 16 | )} 17 | {uploadOption === "url" && } 18 | 19 | ) : ( 20 |
21 | 22 |

OR

23 | 24 |
25 | )} 26 |
27 | ); 28 | }; 29 | 30 | Choose.propTypes = { 31 | onChangeUrl: PropTypes.func.isRequired, 32 | onChangeHandler: PropTypes.func.isRequired, 33 | }; 34 | 35 | export default Choose; 36 | -------------------------------------------------------------------------------- /src/components/InputFile/InputFile.css: -------------------------------------------------------------------------------- 1 | 2 | .upload-area__drop { 3 | position: relative; 4 | text-align: center; 5 | display: flex; 6 | justify-content: center; 7 | margin-top: 20px; 8 | height: 220px; 9 | } 10 | 11 | .upload-area__drop__input { 12 | grid-area: drop; 13 | padding: 1rem 1rem; 14 | position: relative; 15 | display: grid; 16 | grid-template-columns: auto auto; 17 | align-items: center; 18 | min-height: 200px; 19 | border: 1px dashed #3f4c6b; 20 | margin: 0px auto; 21 | width: 95%; 22 | border-radius: 8px; 23 | cursor: pointer; 24 | outline: 0; 25 | box-sizing: border-box; 26 | z-index: 10; 27 | color: transparent; 28 | } 29 | 30 | .upload-area__drop__input::-webkit-file-upload-button { 31 | flex-shrink: 0; 32 | font-size: 0.65rem; 33 | color: #ffffff; 34 | border-radius: 3px; 35 | padding: 8px 15px; 36 | margin: 0; 37 | text-transform: uppercase; 38 | text-align: center; 39 | border: none; 40 | outline: 0; 41 | margin: 0 auto; 42 | visibility: hidden; 43 | } 44 | 45 | .upload-area__drop__icon { 46 | width: 80px; 47 | position: absolute; 48 | top: 20%; 49 | left: 40%; 50 | cursor: pointer; 51 | fill: #e3ebff; 52 | } 53 | 54 | .upload-area__drop__text { 55 | color: lightslategrey; 56 | opacity: 0.7; 57 | font-weight: 400; 58 | position: absolute; 59 | top: 55%; 60 | font-size: 0.78rem; 61 | cursor: pointer; 62 | } -------------------------------------------------------------------------------- /src/components/InputFile/InputFile.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { shallow } from "enzyme"; 3 | 4 | import InputFile from "."; 5 | 6 | describe("", () => { 7 | const onChangeHandler = jest.fn(() => "sample"); 8 | 9 | it("render InputFile without crashing", () => { 10 | const wrapper = shallow(); 11 | expect(wrapper.contains("Drag and drop your files")).toEqual(true); 12 | }); 13 | 14 | it("can select a file", () => { 15 | const wrapper = shallow(); 16 | const input = wrapper.find("input"); 17 | 18 | input.simulate("change", { 19 | target: { 20 | files: ["sample.csv"], 21 | }, 22 | }); 23 | 24 | expect(onChangeHandler).toHaveBeenCalled(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/components/InputFile/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import './InputFile.css' 4 | import UploadIcon from "../../assets/images/computing-cloud.svg"; 5 | 6 | const InputFile = ({ onChangeHandler }) => { 7 | 8 | return ( 9 |
10 | 16 | upload-icon 21 | 22 | Drag and drop your files 23 |
24 | or
25 | click to select 26 |
27 |
28 | ) 29 | } 30 | 31 | InputFile.propTypes = { 32 | onChangeHandler: PropTypes.func.isRequired 33 | }; 34 | 35 | export default InputFile; 36 | -------------------------------------------------------------------------------- /src/components/InputUrl/InputUrl.css: -------------------------------------------------------------------------------- 1 | .upload-area__url { 2 | text-align: left; 3 | width: 95%; 4 | margin: 0 auto; 5 | } 6 | 7 | .upload-area__url__label { 8 | display: block; 9 | padding: 4px; 10 | } 11 | 12 | .upload-area__url__input { 13 | width: 100%; 14 | height: 40px; 15 | border-radius: 5px; 16 | box-shadow: none; 17 | border: 1px solid #ced6e0; 18 | transition: all 0.3s ease-in-out; 19 | font-size: 18px; 20 | padding: 5px 15px; 21 | background: none; 22 | color: #1a3b5d; 23 | font-family: "Source Sans Pro", sans-serif; 24 | box-sizing: border-box; 25 | font-size: 1rem; 26 | } 27 | -------------------------------------------------------------------------------- /src/components/InputUrl/InputUrl.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { shallow } from "enzyme"; 3 | 4 | import InputUrl from "."; 5 | 6 | describe("", () => { 7 | const onChangeUrl = jest.fn(() => "https://www.datopian.com"); 8 | 9 | it("render InputUrl without crashing", () => { 10 | const wrapper = shallow(); 11 | expect(wrapper.contains("URL:")).toEqual(true); 12 | }); 13 | 14 | it("can edit url", () => { 15 | const wrapper = shallow(); 16 | const input = wrapper.find("input"); 17 | 18 | input.simulate("blur", { 19 | target: { 20 | value: "https://www.datopian.com", 21 | }, 22 | }); 23 | 24 | expect(onChangeUrl).toHaveBeenCalled(); 25 | }); 26 | }); -------------------------------------------------------------------------------- /src/components/InputUrl/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import "./InputUrl.css"; 4 | 5 | const InputUrl = ({ onChangeUrl }) => { 6 | const handleKeyPress = (event) => { 7 | if (event.keyCode === 13) { 8 | event.target.blur(); 9 | } 10 | }; 11 | 12 | return ( 13 |
14 | 17 | handleKeyPress(event)} 24 | placeholder="https://www.data.com/sample.csv" 25 | /> 26 |
27 | ); 28 | }; 29 | 30 | InputUrl.propTypes = { 31 | onChangeUrl: PropTypes.func.isRequired, 32 | }; 33 | 34 | export default InputUrl; 35 | -------------------------------------------------------------------------------- /src/components/Metadata/Metadata.css: -------------------------------------------------------------------------------- 1 | .metadata-form { 2 | display: grid; 3 | grid-template-columns: 1fr 1fr; 4 | column-gap: 20px; 5 | } 6 | 7 | .metadata-name { 8 | color: #1a3b5d; 9 | text-align: left; 10 | grid-area: name; 11 | } 12 | 13 | .metadata-label { 14 | font-size: 14px; 15 | margin-bottom: 5px; 16 | margin-left: 5px; 17 | text-align: left; 18 | font-weight: 500; 19 | color: #1a3b5d; 20 | width: 100%; 21 | display: block; 22 | user-select: none; 23 | } 24 | 25 | .metadata-input { 26 | margin-bottom: 20px; 27 | } 28 | 29 | .metadata-input__input { 30 | width: 100%; 31 | height: 40px; 32 | border-radius: 5px; 33 | box-shadow: none; 34 | border: 1px solid #ced6e0; 35 | transition: all 0.3s ease-in-out; 36 | font-size: 18px; 37 | padding: 5px 15px; 38 | background: none; 39 | color: #1a3b5d; 40 | font-family: "Source Sans Pro", sans-serif; 41 | box-sizing: border-box; 42 | } 43 | 44 | .btn { 45 | margin-top: 17px; 46 | border-radius: 5px; 47 | padding: 10px; 48 | width: 50%; 49 | height: 45px; 50 | font-size: 1.2rem; 51 | color: #fff; 52 | background-color: #00A7E1; 53 | outline: none; 54 | border: none; 55 | cursor: pointer; 56 | box-shadow: 0 4px 11px 3px rgba(18, 22, 33, 0.05); 57 | } 58 | 59 | .btn:disabled { 60 | background: #e3e9ed !important; 61 | cursor: default; 62 | } 63 | 64 | .btn:hover { 65 | background-color: #009ace; 66 | } -------------------------------------------------------------------------------- /src/components/Metadata/Metadata.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { shallow } from "enzyme"; 4 | 5 | import Metadata from "."; 6 | 7 | describe("", () => { 8 | const handleChange = jest.fn(2); 9 | const handleSubmit = jest.fn(); 10 | const deleteResource = jest.fn(); 11 | const updateResource = jest.fn(); 12 | 13 | it("render Metadata without crashing", () => { 14 | const div = document.createElement("div"); 15 | ReactDOM.render( 16 | , 25 | div 26 | ); 27 | }); 28 | 29 | it("auto-populate input fields", () => { 30 | const wrapper = shallow( 31 | 46 | ); 47 | const inputTitle = wrapper.find("#title"); 48 | const inputFormat = wrapper.find("#format"); 49 | const inputDescription = wrapper.find("#description"); 50 | const inputRestricted = wrapper.find("#restricted"); 51 | const inputEncoding = wrapper.find("#encoding"); 52 | 53 | expect(inputTitle.props().value).toEqual("sample"); 54 | expect(inputFormat.props().value).toEqual("csv"); 55 | expect(inputDescription.props().value).toEqual("Lorem ..."); 56 | expect(inputRestricted.props().value).toEqual("private"); 57 | expect(inputEncoding.props().value).toEqual("utf-8"); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /src/components/Metadata/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import "./Metadata.css"; 5 | import encodeData from "../../db/encode.json"; 6 | import formatData from "../../db/resource_formats.json"; 7 | 8 | //TODO: add the custom fields as a props and render it in metadata component 9 | const customFields = [ 10 | { 11 | label: "Access Restriction", 12 | name: "restricted", 13 | input_type: "select", 14 | values: ['{"level": "public"}', '{"level": "private"}'], 15 | options: ["Public", "Private"], 16 | }, 17 | ]; 18 | 19 | const Metadata = ({ metadata, handleChange}) => { 20 | return ( 21 | <> 22 |

{metadata.path}

23 |
24 |
25 | 28 | 36 |
37 |
38 | 41 | 49 |
50 |
51 | 54 | 71 |
72 |
73 | 76 | 93 |
94 | {customFields && 95 | customFields.map((item) => ( 96 |
97 | 100 | 120 |
121 | ))} 122 |
123 | 124 | ); 125 | }; 126 | 127 | Metadata.propTypes = { 128 | metadata: PropTypes.object.isRequired, 129 | handleChange: PropTypes.func.isRequired, 130 | }; 131 | 132 | export default Metadata; 133 | -------------------------------------------------------------------------------- /src/components/ProgressBar/ProgressBar.css: -------------------------------------------------------------------------------- 1 | .svg{ 2 | display: block; 3 | margin: 20px auto; 4 | max-width: 100%; 5 | } 6 | 7 | .svg-circle-bg { 8 | fill: none; 9 | } 10 | 11 | .svg-circle { 12 | fill: none; 13 | } 14 | 15 | .svg-circle-text { 16 | font-size: 0.68rem; 17 | text-anchor: middle; 18 | margin-top: 30px; 19 | fill:#3F4656; 20 | font-weight: bold; 21 | } 22 | 23 | .time-remaining { 24 | font-size: 0.55rem; 25 | float: left; 26 | margin-top: -13px; 27 | color: #3F4656; 28 | } -------------------------------------------------------------------------------- /src/components/ProgressBar/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useRef } from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import "./ProgressBar.css"; 5 | 6 | const ProgressBar = (props) => { 7 | const [offset, setOffset] = useState(0); 8 | const circleRef = useRef(null); 9 | const { 10 | size, 11 | progress, 12 | strokeWidth, 13 | circleOneStroke, 14 | circleTwoStroke, 15 | timeRemaining, 16 | } = props; 17 | 18 | const center = size / 2; 19 | const radius = size / 2 - strokeWidth / 2; 20 | const circumference = 2 * Math.PI * radius; 21 | 22 | useEffect(() => { 23 | const progressOffset = ((100 - progress) / 100) * circumference; 24 | setOffset(progressOffset); 25 | 26 | circleRef.current.style = "transition: stroke-dashoffset 850ms ease-in-out"; 27 | }, [setOffset, progress, circumference, offset]); 28 | 29 | return ( 30 | <> 31 | 32 | 40 | 51 | 52 | {progress}% 53 | 54 | 55 | {timeRemaining > 0 && {timeRemaining > 60 ? `${Math.floor(timeRemaining /60)} minute${Math.floor(timeRemaining /60) > 1 ? 's' : ''}` : `${Math.floor(timeRemaining)} second${timeRemaining > 1 ? 's' : ''}`} left} 56 | 57 | ); 58 | }; 59 | 60 | ProgressBar.propTypes = { 61 | size: PropTypes.number.isRequired, 62 | progress: PropTypes.number.isRequired, 63 | strokeWidth: PropTypes.number.isRequired, 64 | circleOneStroke: PropTypes.string.isRequired, 65 | circleTwoStroke: PropTypes.string.isRequired, 66 | }; 67 | 68 | export default ProgressBar; 69 | -------------------------------------------------------------------------------- /src/components/Switcher/Switcher.css: -------------------------------------------------------------------------------- 1 | .switch-wrapper { 2 | color: #222222; 3 | display: grid; 4 | grid-template-columns: 160px 200px 160px; 5 | grid-template-areas: "switchOne divLine switchTwo"; 6 | width: 80%; 7 | align-items: center; 8 | justify-items: center; 9 | align-content: center; 10 | justify-content: center; 11 | margin: 1rem auto; 12 | } 13 | 14 | .switch-number { 15 | border-radius: 50%; 16 | width: 20px; 17 | height: 20px; 18 | padding: 3px; 19 | text-align: center; 20 | margin-bottom: 1.2rem; 21 | margin: 0 auto; 22 | cursor: pointer; 23 | } 24 | 25 | .switch-one { 26 | grid-area: switchOne; 27 | } 28 | 29 | .switch-two { 30 | grid-area: switchTwo; 31 | } 32 | 33 | .switch-number-selected { 34 | background-color: #00A7E1; 35 | color: #ffffff; 36 | } 37 | 38 | .switch-number-disabled { 39 | border: 1px solid #e3ebff; 40 | } 41 | 42 | .switch-description { 43 | margin-top: 1rem; 44 | width: 100%; 45 | cursor: pointer; 46 | } 47 | 48 | .switch-description-active { 49 | font-weight: bold; 50 | } 51 | 52 | .divider-line { 53 | grid-area: divLine; 54 | height: 2px; 55 | background-color: #e3ebff; 56 | width: 100%; 57 | margin-bottom: 3rem; 58 | } 59 | -------------------------------------------------------------------------------- /src/components/Switcher/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import './Switcher.css' 4 | 5 | const Switcher = (props) => { 6 | 7 | return ( 8 |
9 |
props.switcher('metadata')}> 10 |
11 | 1 12 |
13 |

14 | Edit Metadata 15 |

16 |
17 |
props.switcher('schema')}> 18 |
19 | 2 20 |
21 |

22 | Edit Schema 23 |

24 |
25 |
26 |
27 | ) 28 | } 29 | 30 | Switcher.propTypes = { 31 | metadataOrSchema: PropTypes.string.isRequired 32 | }; 33 | 34 | export default Switcher; 35 | -------------------------------------------------------------------------------- /src/components/TableSchema/TableSchema.css: -------------------------------------------------------------------------------- 1 | .table-container { 2 | margin-top: 2.6rem; 3 | margin-bottom: 2rem; 4 | display: flex; 5 | } 6 | 7 | .table-schema-info_container { 8 | overflow: overlay; 9 | } 10 | 11 | .table-schema-info_container::-webkit-scrollbar-track { 12 | -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.2); 13 | border-radius: 10px; 14 | background-color: #fff; 15 | } 16 | 17 | .table-schema-info_container::-webkit-scrollbar { 18 | width: 8px; 19 | height: 8px; 20 | background-color: #fff; 21 | } 22 | 23 | .table-schema-info_container::-webkit-scrollbar-thumb { 24 | border-radius: 10px; 25 | -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.2); 26 | background-color: #e3e9ed; 27 | } 28 | 29 | .table-schema-info_table { 30 | width: auto !important; 31 | } 32 | 33 | .table-schema-help { 34 | vertical-align: middle; 35 | } 36 | 37 | .table-schema-help_row { 38 | padding: 1.3rem; 39 | } 40 | 41 | .table-thead-tr { 42 | height: 50px; 43 | border-radius: 8px; 44 | } 45 | 46 | .table-thead-th { 47 | background: #f1f1f1; 48 | color: #363844; 49 | font-weight: bold; 50 | text-align: center; 51 | vertical-align: middle; 52 | position: sticky; 53 | top: 0; 54 | z-index: 10; 55 | } 56 | 57 | .table-tbody-help-tr { 58 | color: #363844; 59 | height: 40px; 60 | } 61 | 62 | .table-tbody-help-td { 63 | vertical-align: middle; 64 | font-weight: bold; 65 | } 66 | 67 | .table-tbody-help-td-empty { 68 | height: 50px; 69 | } 70 | 71 | .table-tbody-td { 72 | padding: 5px; 73 | border: solid 1px #f1f1f1; 74 | background: #fff; 75 | max-width: 100px; 76 | overflow: hidden; 77 | text-overflow: ellipsis; 78 | white-space: nowrap; 79 | font-size: 14px; 80 | } 81 | 82 | .table-tbody-input { 83 | border: none; 84 | padding: 0 10px 0 10px; 85 | height: 40px; 86 | } 87 | 88 | .table-tbody-select { 89 | position: relative; 90 | display: inline-block; 91 | vertical-align: middle; 92 | background-color: #fafafa; 93 | width: 100%; 94 | border: none; 95 | padding: 10px; 96 | height: 40px; 97 | } 98 | 99 | .table-btn-save { 100 | padding: 10px; 101 | width: 90%; 102 | background-color: #4fa8fd; 103 | border: none; 104 | border-radius: 3px; 105 | color: #fff; 106 | font-weight: 500; 107 | cursor: pointer; 108 | outline: 0; 109 | } 110 | 111 | .table-btn-save:disabled { 112 | background: #e3e9ed !important; 113 | cursor: default; 114 | } 115 | -------------------------------------------------------------------------------- /src/components/TableSchema/TableSchema.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { shallow } from "enzyme"; 3 | 4 | import TableSchema from "."; 5 | 6 | describe("", () => { 7 | const onChange = jest.fn(() => "https://www.datopian.com"); 8 | 9 | it("render TableSchema without crashing", () => { 10 | const wrapper = shallow(); 11 | 12 | 13 | expect(wrapper.contains("Title")).toEqual(true); 14 | expect(wrapper.contains("Description")).toEqual(true); 15 | expect(wrapper.contains("Type")).toEqual(true); 16 | expect(wrapper.contains("Format")).toEqual(true); 17 | }); 18 | 19 | }); -------------------------------------------------------------------------------- /src/components/TableSchema/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { useTable } from "react-table"; 4 | import types from "../../db/types.json"; 5 | 6 | import "./TableSchema.css"; 7 | 8 | const TableSchema = (props) => { 9 | const [schema, setSchema] = useState(props.schema); 10 | // eslint-disable-next-line react-hooks/exhaustive-deps 11 | const data = React.useMemo(() => [...props.data], [schema]); 12 | 13 | const columnsSchema = schema.fields.map((item, index) => { 14 | return { 15 | Header: item.name ? item.name : `column_${index + 1}`, 16 | accessor: item.name ? item.name : `column_${index + 1}`, 17 | }; 18 | }); 19 | // eslint-disable-next-line react-hooks/exhaustive-deps 20 | const columns = React.useMemo(() => [...columnsSchema], [schema]); 21 | const { 22 | getTableProps, 23 | getTableBodyProps, 24 | headerGroups, 25 | rows, 26 | prepareRow, 27 | } = useTable({ columns, data }); 28 | 29 | const handleChange = (event, key, index) => { 30 | const newSchema = { ...schema }; 31 | newSchema.fields[index][key] = event.target.value; 32 | setSchema(newSchema); 33 | }; 34 | 35 | //if the the user upload a new file, will update the state 36 | //and render with the new values 37 | useEffect(() => { 38 | setSchema(props.schema); 39 | }, [props.schema]); 40 | 41 | const renderEditSchemaField = (key) => { 42 | if (key === "type") { 43 | return schema.fields.map((item, index) => ( 44 | 45 | 61 | 62 | )); 63 | } 64 | return schema.fields.map((item, index) => ( 65 | 66 | handleChange(event, key, index)} 71 | /> 72 | 73 | )); 74 | }; 75 | 76 | return ( 77 | <> 78 |
79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 |
Title
Description
Type
Format
98 |
99 | 100 | 101 | {headerGroups.map((headerGroup) => ( 102 | 106 | {headerGroup.headers.map((column) => ( 107 | 110 | ))} 111 | 112 | ))} 113 | 114 | 115 | 116 | {renderEditSchemaField("title")} 117 | 118 | 119 | {renderEditSchemaField("description")} 120 | 121 | {renderEditSchemaField("type")} 122 | {renderEditSchemaField("format")} 123 | {rows.map((row) => { 124 | prepareRow(row); 125 | return ( 126 | 127 | {row.cells.map((cell) => { 128 | return ( 129 | 132 | ); 133 | })} 134 | 135 | ); 136 | })} 137 | 138 |
108 | {column.render("Header")} 109 |
130 | {cell.render("Cell")} 131 |
139 |
140 |
141 | 142 | ); 143 | }; 144 | 145 | TableSchema.propTypes = { 146 | schema: PropTypes.object.isRequired, 147 | data: PropTypes.array.isRequired, 148 | }; 149 | 150 | export default TableSchema; 151 | -------------------------------------------------------------------------------- /src/components/Upload/Upload.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, fireEvent } from "@testing-library/react"; 3 | import ReactDOM from "react-dom"; 4 | import { shallow } from "enzyme"; 5 | 6 | import Upload from "."; 7 | 8 | describe("", () => { 9 | it("render Upload without crashing", () => { 10 | const div = document.createElement("div"); 11 | ReactDOM.render(, div); 12 | }); 13 | 14 | it.skip('can select a file', () => { 15 | const wrapper = shallow() 16 | const input = wrapper.find('input'); 17 | 18 | input.simulate('change', { 19 | target: { 20 | files: [ 21 | 'sample.csv' 22 | ] 23 | } 24 | }); 25 | 26 | expect(wrapper.state().selectedFile).toEqual('sample.csv'); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/components/Upload/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as data from "frictionless.js"; 3 | import toArray from "stream-to-array"; 4 | import ProgressBar from "../ProgressBar"; 5 | import { onFormatBytes } from "../../utils"; 6 | import Choose from "../Choose"; 7 | 8 | class Upload extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | this.state = { 12 | datasetId: props.datasetId, 13 | selectedFile: null, 14 | fileSize: 0, 15 | formattedSize: "0 KB", 16 | start: "", 17 | loaded: 0, 18 | success: false, 19 | error: false, 20 | fileExists: false, 21 | loading: false, 22 | timeRemaining: 0, 23 | }; 24 | } 25 | 26 | onChangeHandler = async (event) => { 27 | let { formattedSize, selectedFile } = this.state; 28 | 29 | if (event.target.files.length > 0) { 30 | selectedFile = event.target.files[0]; 31 | const file = data.open(selectedFile); 32 | try { 33 | await file.addSchema(); 34 | } catch (e) { 35 | console.warn(e); 36 | } 37 | formattedSize = onFormatBytes(file.size); 38 | const hash = await file.hash('sha256'); 39 | this.props.metadataHandler(Object.assign(file.descriptor, { hash })); 40 | } 41 | 42 | this.setState({ 43 | selectedFile, 44 | loaded: 0, 45 | success: false, 46 | fileExists: false, 47 | error: false, 48 | formattedSize, 49 | }); 50 | 51 | await this.onClickHandler(); 52 | }; 53 | 54 | onUploadProgress = (progressEvent) => { 55 | this.onTimeRemaining(progressEvent.loaded); 56 | this.setState({ 57 | loaded: (progressEvent.loaded / progressEvent.total) * 100, 58 | }); 59 | }; 60 | 61 | onTimeRemaining = (progressLoaded) => { 62 | const end = new Date().getTime(); 63 | const duration = (end - this.state.start) / 1000; 64 | const bps = progressLoaded / duration; 65 | const kbps = bps / 1024; 66 | const timeRemaining = (this.state.fileSize - progressLoaded) / kbps; 67 | 68 | this.setState({ 69 | timeRemaining: timeRemaining / 1000, 70 | }); 71 | }; 72 | 73 | onClickHandler = async () => { 74 | const start = new Date().getTime(); 75 | const { selectedFile } = this.state; 76 | const { client } = this.props; 77 | 78 | const resource = data.open(selectedFile); 79 | 80 | this.setState({ 81 | fileSize: resource.size, 82 | start, 83 | loading: true, 84 | }); 85 | 86 | this.props.handleUploadStatus({ 87 | loading: true, 88 | error: false, 89 | success: false, 90 | }); 91 | 92 | // Use client to upload file to the storage and track the progress 93 | client 94 | .pushBlob(resource, this.onUploadProgress) 95 | .then((response) => { 96 | this.setState({ 97 | success: true, 98 | loading: false, 99 | fileExists: ! response, 100 | loaded: 100 101 | }); 102 | this.props.handleUploadStatus({ 103 | loading: false, 104 | success: true, 105 | }); 106 | }) 107 | .catch((error) => { 108 | console.error("Upload failed with error: " + error); 109 | this.setState({ error: true, loading: false }); 110 | this.props.handleUploadStatus({ 111 | loading: false, 112 | success: false, 113 | error: true, 114 | }); 115 | }); 116 | }; 117 | 118 | render() { 119 | const { 120 | success, 121 | fileExists, 122 | error, 123 | timeRemaining, 124 | selectedFile, 125 | formattedSize, 126 | } = this.state; 127 | return ( 128 |
129 | console.log("Get url:", event.target.value)} 132 | /> 133 |
134 | {selectedFile && ( 135 | <> 136 |
    137 |
  • 138 |
    139 |
    140 |

    {selectedFile.name}

    141 |

    {formattedSize}

    142 |
    143 |
    144 | 152 |
    153 |
    154 |
  • 155 |
156 |

157 | {success && !fileExists && !error && "File uploaded successfully"} 158 | {fileExists && "File uploaded successfully"} 159 | {error && "Upload failed"} 160 |

161 | 162 | )} 163 |
164 |
165 | ); 166 | } 167 | } 168 | 169 | export default Upload; 170 | -------------------------------------------------------------------------------- /src/core.js: -------------------------------------------------------------------------------- 1 | import Choose from "./components/Choose"; 2 | import InputFile from "./components/InputFile"; 3 | import InputUrl from "./components/InputUrl"; 4 | import Metadata from "./components/Metadata"; 5 | import ProgressBar from "./components/ProgressBar"; 6 | import Switcher from "./components/Switcher"; 7 | import TableSchema from "./components/TableSchema"; 8 | import Upload from "./components/Upload"; 9 | import encodeData from "./db/encode.json"; 10 | import formatData from "./db/resource_formats.json"; 11 | import licenses from "./db/licenses.json"; 12 | import types from "./db/types.json"; 13 | 14 | export { 15 | Upload, 16 | Choose, 17 | InputFile, 18 | InputUrl, 19 | Metadata, 20 | ProgressBar, 21 | Switcher, 22 | TableSchema, 23 | encodeData, 24 | formatData, 25 | licenses, 26 | types, 27 | }; 28 | -------------------------------------------------------------------------------- /src/db/encode.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "value": "utf_8", 4 | "label": "UTF-8" 5 | }, 6 | { 7 | "value": "iso_8859_1", 8 | "label": "ISO-8859-1" 9 | }, 10 | { 11 | "value": "windows_1251", 12 | "label": "Windows-1251" 13 | }, 14 | { 15 | "value": "windows_1252", 16 | "label": "Windows-1252" 17 | }, 18 | { 19 | "value": "shift_jis", 20 | "label": "Shift JIS" 21 | }, 22 | { 23 | "value": "gb2312", 24 | "label": "GB2312" 25 | }, 26 | { 27 | "value": "euc_kr", 28 | "label": "EUC-KR" 29 | }, 30 | { 31 | "value": "iso_8859_2", 32 | "label": "ISO-8859-2" 33 | }, 34 | { 35 | "value": "windows_1250", 36 | "label": "Windows-1250" 37 | }, 38 | { 39 | "value": "euc_jp", 40 | "label": "EUC-JP" 41 | }, 42 | { 43 | "value": "gbk", 44 | "label": "GBK" 45 | }, 46 | { 47 | "value": "big5", 48 | "label": "Big5" 49 | }, 50 | { 51 | "value": "iso_8859_15", 52 | "label": "ISO-8859-15" 53 | }, 54 | { 55 | "value": "windows_1256", 56 | "label": "Windows-1256" 57 | }, 58 | { 59 | "value": "iso_8859_9", 60 | "label": "ISO-8859-9" 61 | } 62 | ] -------------------------------------------------------------------------------- /src/db/licenses.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "value": "cc-by", 4 | "label": "Creative Commons Attribution" 5 | }, 6 | { 7 | "value": "cc-by-sa", 8 | "label": "Creative Commons Attribution Share-Alike" 9 | }, 10 | { 11 | "value": "cc-zero", 12 | "label": "Creative Commons CCZero" 13 | }, 14 | { 15 | "value": "cc-nc", 16 | "label": "Creative Commons Non-Commercial (Any)" 17 | }, 18 | { 19 | "value": "gfdl", 20 | "label": "GNU Free Documentation License" 21 | }, 22 | { 23 | "value": "notspecified", 24 | "label": "License not specified" 25 | }, 26 | { 27 | "value": "odc-by", 28 | "label": "Open Data Commons Attribution License" 29 | }, 30 | { 31 | "value": "odc-odbl", 32 | "label": "Open Data Commons Open Database License (ODbL)" 33 | }, 34 | { 35 | "value": "odc-pddl", 36 | "label": "Open Data Commons Public Domain Dedication and License (PDDL)" 37 | }, 38 | { 39 | "value": "other-at", 40 | "label": "Other (Attribution)" 41 | }, 42 | { 43 | "value": "other-nc", 44 | "label": "Other (Non-Commercial)" 45 | }, 46 | { 47 | "value": "other-closed", 48 | "label": "Other (Not Open)" 49 | }, 50 | { 51 | "value": "other-open", 52 | "label": "Other (Open)" 53 | }, 54 | { 55 | "value": "other-pd", 56 | "label": "Other (Public Domain)" 57 | }, 58 | { 59 | "value": "uk-ogl", 60 | "label": "UK Open Government Licence (OGL)" 61 | } 62 | ] -------------------------------------------------------------------------------- /src/db/resource_formats.json: -------------------------------------------------------------------------------- 1 | [ 2 | ["PPTX", "Powerpoint OOXML Presentation", "application/vnd.openxmlformats-officedocument.presentationml.presentation", []], 3 | ["EXE", "Windows Executable Program", "application/x-msdownload", []], 4 | ["DOC", "Word Document", "application/msword", []], 5 | ["KML", "KML File", "application/vnd.google-earth.kml+xml", []], 6 | ["XLS", "Excel Document", "application/vnd.ms-excel", ["Excel", "application/msexcel", "application/x-msexcel", "application/x-ms-excel", "application/x-excel", "application/x-dos_ms_excel", "application/xls", "application/x-xls"]], 7 | ["WCS", "Web Coverage Service", "wcs", []], 8 | ["JS", "JavaScript", "application/x-javascript", []], 9 | ["MDB", "Access Database", "application/x-msaccess", []], 10 | ["NetCDF", "NetCDF File", "application/netcdf", []], 11 | ["ArcGIS Map Service", "ArcGIS Map Service", "ArcGIS Map Service", ["arcgis map service"]], 12 | ["TSV", "Tab Separated Values File", "text/tab-separated-values", ["text/tsv"]], 13 | ["WFS", "Web Feature Service", null, []], 14 | ["WMTS", "Web Map Tile Service", null, []], 15 | ["ArcGIS Online Map", "ArcGIS Online Map", "ArcGIS Online Map", ["web map application"]], 16 | ["Perl", "Perl Script", "text/x-perl", []], 17 | ["KMZ", "KMZ File", "application/vnd.google-earth.kmz+xml", ["application/vnd.google-earth.kmz"]], 18 | ["OWL", "Web Ontology Language", "application/owl+xml", []], 19 | ["N3", "N3 Triples", "application/x-n3", []], 20 | ["ZIP", "Zip File", "application/zip", ["zip", "http://purl.org/NET/mediatypes/application/zip"]], 21 | ["GZ", "Gzip File", "application/gzip", ["application/x-gzip"]], 22 | ["QGIS", "QGIS File", "application/x-qgis", []], 23 | ["ODS", "OpenDocument Spreadsheet", "application/vnd.oasis.opendocument.spreadsheet", []], 24 | ["ODT", "OpenDocument Text", "application/vnd.oasis.opendocument.text", []], 25 | ["JSON", "JavaScript Object Notation", "application/json", []], 26 | ["BMP", "Bitmap Image File", "image/x-ms-bmp", []], 27 | ["HTML", "Web Page", "text/html", ["htm", "http://purl.org/net/mediatypes/text/html"]], 28 | ["RAR", "RAR Compressed File", "application/rar", []], 29 | ["TIFF", "TIFF Image File", "image/tiff", []], 30 | ["ODB", "OpenDocument Database", "application/vnd.oasis.opendocument.database", []], 31 | ["TXT", "Text File", "text/plain", []], 32 | ["DCR", "Adobe Shockwave format", "application/x-director", []], 33 | ["ODF", "OpenDocument Math Formula", "application/vnd.oasis.opendocument.formula", []], 34 | ["ODG", "OpenDocument Image", "application/vnd.oasis.opendocument.graphics", []], 35 | ["XML", "XML File", "application/xml", ["text/xml", "http://purl.org/net/mediatypes/application/xml"]], 36 | ["XLSX", "Excel OOXML Spreadsheet", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", []], 37 | ["DOCX", "Word OOXML Document", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", []], 38 | ["BIN", "Binary Data", "application/octet-stream", ["bin"]], 39 | ["XSLT", "Extensible Stylesheet Language Transformations", "application/xslt+xml", []], 40 | ["WMS", "Web Mapping Service", "WMS", ["wms"]], 41 | ["SVG", "SVG vector image", "image/svg+xml", ["svg"]], 42 | ["PPT", "Powerpoint Presentation", "application/vnd.ms-powerpoint", []], 43 | ["ODP", "OpenDocument Presentation", "application/vnd.oasis.opendocument.presentation", []], 44 | ["JPEG", "JPG Image File", "image/jpeg", ["jpeg", "jpg"]], 45 | ["SPARQL", "SPARQL end-point", "application/sparql-results+xml", []], 46 | ["GIF", "GIF Image File", "image/gif", []], 47 | ["RDF", "RDF", "application/rdf+xml", ["rdf/xml"]], 48 | ["E00", " ARC/INFO interchange file format", "application/x-e00", []], 49 | ["PDF", "PDF File", "application/pdf", []], 50 | ["CSV", "Comma Separated Values File", "text/csv", ["text/comma-separated-values"]], 51 | ["ODC", "OpenDocument Chart", "application/vnd.oasis.opendocument.chart", []], 52 | ["Atom Feed", "Atom Feed", "application/atom+xml", []], 53 | ["MrSID", "MrSID", "image/x-mrsid", []], 54 | ["ArcGIS Map Preview", "ArcGIS Map Preview", "ArcGIS Map Preview", ["arcgis map preview"]], 55 | ["XYZ", "XYZ Chemical File", "chemical/x-xyz", []], 56 | ["MOP", "MOPAC Input format", "chemical/x-mopac-input", []], 57 | ["Esri REST", "Esri Rest API Endpoint", "Esri REST", ["arcgis_rest"]], 58 | ["dBase", "dBase Database", "application/x-dbf", ["dbf"]], 59 | ["MXD", "ESRI ArcGIS project file", "application/x-mxd", []], 60 | ["TAR", "TAR Compressed File", "application/x-tar", []], 61 | ["PNG", "PNG Image File", "image/png", []], 62 | ["RSS", "RSS feed", "application/rss+xml", []], 63 | ["GeoJSON", "Geographic JavaScript Object Notation", "application/geo+json", ["geojson"]], 64 | ["SHP", "Shapefile", null, ["esri shapefile"]], 65 | ["TORRENT", "Torrent", "application/x-bittorrent", ["bittorrent"]], 66 | ["ICS", "iCalendar", "text/calendar", ["ifb", "iCal"]], 67 | ["GTFS", "General Transit Feed Specification", null, []], 68 | ["XLSB", "Excel OOXML Spreadsheet - Binary Workbook", "application/vnd.ms-excel.sheet.binary.macroEnabled.12", []], 69 | ["XLSM", "Excel OOXML Spreadsheet - Macro-Enabled", "application/vnd.ms-excel.sheet.macroEnabled.12", []], 70 | ["XLTM", "Excel OOXML Spreadsheet - Macro-Enabled Template", "application/vnd.ms-excel.template.macroEnabled.12", []], 71 | ["XLAM", "Excel OOXML Spreadsheet - Macro-Enabled Add-In", "application/vnd.ms-excel.addin.macroEnabled.12", []], 72 | ["DOTX", "Word OOXML - Template", "application/vnd.openxmlformats-officedocument.wordprocessingml.template", []], 73 | ["DOCM", "Word OOXML - Macro Enabled", "application/vnd.ms-word.document.macroEnabled.12", []], 74 | ["DOTM", "Word OOXML Template - Macro Enabled", "application/vnd.ms-word.template.macroEnabled.12", []], 75 | ["XLTX", "Excel OOXML Spreadsheet - Template", "application/vnd.openxmlformats-officedocument.spreadsheetml.template", []], 76 | ["POTX", "PowerPoint OOXML Presentation - Template", "application/vnd.openxmlformats-officedocument.presentationml.template", []], 77 | ["PPSX", "PowerPoint Open XML - Slide Show", "application/vnd.openxmlformats-officedocument.presentationml.slideshow", []], 78 | ["PPAM", "PowerPoint Open XML - Macro-Enabled Add-In", "application/vnd.ms-powerpoint.addin.macroEnabled.12", []], 79 | ["PPTM", "PowerPoint Open XML - Macro-Enabled", "application/vnd.ms-powerpoint.presentation.macroEnabled.12", []], 80 | ["POTM", "PowerPoint Open XML - Macro-Enabled Template", "application/vnd.ms-powerpoint.template.macroEnabled.12", []], 81 | ["PPSM", "PowerPoint Open XML - Macro-Enabled Slide Show", "application/vnd.ms-powerpoint.slideshow.macroEnabled.12", []] 82 | ] -------------------------------------------------------------------------------- /src/db/types.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": [ 3 | "string", 4 | "number", 5 | "integer", 6 | "boolean", 7 | "object", 8 | "array", 9 | "date", 10 | "time", 11 | "datetime", 12 | "year", 13 | "yearmonth", 14 | "duration", 15 | "geopoint", 16 | "geojson", 17 | "any" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | // Mount the ResourceEditor app explicitly 6 | // (make sure you call the function after it's loaded) 7 | function mountResourceEditorApp([elementId, config, resource] = ['root', { 8 | authToken: null, 9 | api: null, 10 | lfs: null, 11 | organizationId: null, 12 | datasetId: null, 13 | resourceId: null, 14 | }, {}]) { 15 | ReactDOM.render( 16 | 17 | 18 | , 19 | document.getElementById(elementId) 20 | ); 21 | }; 22 | 23 | 24 | // Automatically mount the app if an element with id='ResourceEditor' exists 25 | const element = document.getElementById('ResourceEditor'); 26 | if (element) { 27 | const config = { 28 | datasetId: element.getAttribute('data-dataset-id'), 29 | api: element.getAttribute('data-api'), 30 | lfs: element.getAttribute('data-lfs'), 31 | authToken: element.getAttribute('data-auth-token'), 32 | organizationId: element.getAttribute('data-organization-id'), 33 | resourceId: element.getAttribute('data-resource-id') 34 | } 35 | 36 | ReactDOM.render( 37 | 38 | 42 | , 43 | element 44 | ); 45 | } 46 | 47 | 48 | export { ResourceEditor } from './App'; 49 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | import { configure } from 'enzyme'; 7 | import Adapter from 'enzyme-adapter-react-16'; 8 | configure({ adapter: new Adapter() }); 9 | -------------------------------------------------------------------------------- /src/utils/Utils.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { getFileExtension, onFormatTitle, onFormatName, onFormatBytes, removeHyphen } from './index' 4 | 5 | describe("Utils", () => { 6 | 7 | it('format size in Bytes, KB, MB, GB', () => { 8 | 9 | expect(onFormatBytes(100)).toEqual('100 Bytes'); 10 | expect(onFormatBytes(1000)).toEqual('1 KB'); 11 | expect(onFormatBytes(1222222)).toEqual('1.2 MB'); 12 | expect(onFormatBytes(1222222222)).toEqual('1.2 GB'); 13 | }); 14 | 15 | it('format title', () => { 16 | 17 | expect(onFormatTitle("sample-data.csv")).toEqual('sample data'); 18 | expect(onFormatTitle("sample_data.csv")).toEqual('sample data'); 19 | expect(onFormatTitle("sample-data_csv.csv")).toEqual('sample data csv'); 20 | expect(onFormatTitle("sampleData.csv")).toEqual('sampleData'); 21 | }); 22 | 23 | it('get file extension', () => { 24 | 25 | expect(getFileExtension("sample.csv")).toEqual('csv'); 26 | expect(getFileExtension("sample.html")).toEqual('html'); 27 | expect(getFileExtension("sample.xls")).toEqual('xls'); 28 | expect(getFileExtension("sampleData.doc")).toEqual('doc'); 29 | }); 30 | 31 | it('format name', () => { 32 | 33 | expect(onFormatName("sample.csv")).toEqual('sample'); 34 | expect(onFormatName("sample_data.html")).toEqual('sample_data'); 35 | expect(onFormatName("sample-data.xls")).toEqual('sample-data'); 36 | expect(onFormatName("sample-Data.doc")).toEqual('sample-Data'); 37 | }); 38 | 39 | it('remove hyphen from uuid', () => { 40 | 41 | expect(removeHyphen("fd77e419-32ae-4025-8d14-890343b605a3")).toEqual('fd77e41932ae40258d14890343b605a3'); 42 | expect(removeHyphen("9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d")).toEqual('9b1deb4d3b7d4bad9bdd2b0d7b3dcb6d'); 43 | expect(removeHyphen("should not broken without hyphen")).toEqual('should not broken without hyphen'); 44 | }); 45 | }); -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | const getFileExtension = (filename) => { 2 | return /[.]/.exec(filename) ? /[^.]+$/.exec(filename)[0] : undefined; 3 | }; 4 | 5 | const onFormatTitle = (name) => { 6 | return name 7 | .replace(/\.[^/.]+$/, "") 8 | .replace(/_/g, " ") 9 | .replace(/-/g, " "); 10 | }; 11 | 12 | const onFormatName = (name) => { 13 | return name.replace(/\.[^/.]+$/, ""); 14 | }; 15 | 16 | const onFormatBytes = (bytes, decimals = 1) => { 17 | if (bytes === 0) return "0 Bytes"; 18 | 19 | const k = 1000; 20 | const dm = decimals < 0 ? 0 : decimals; 21 | const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; 22 | 23 | const i = Math.floor(Math.log(bytes) / Math.log(k)); 24 | 25 | return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i]; 26 | }; 27 | 28 | const removeHyphen = (id) => { 29 | return id.replace(/-/g, ""); 30 | }; 31 | 32 | export { 33 | getFileExtension, 34 | onFormatTitle, 35 | onFormatName, 36 | onFormatBytes, 37 | removeHyphen, 38 | } -------------------------------------------------------------------------------- /stories/1-app..stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import App from '../src/App'; 3 | 4 | export default { 5 | title: 'Templates', 6 | component: App, 7 | }; 8 | 9 | export const AppTemplate = () => ; 10 | -------------------------------------------------------------------------------- /stories/2-uploader.stories.js: -------------------------------------------------------------------------------- 1 | 2 | import React from "react"; 3 | 4 | import Upload from '../src/components/Upload' 5 | import '../src/App.css'; 6 | 7 | export default { 8 | title: 'Components /Upload', 9 | component: Upload, 10 | }; 11 | 12 | 13 | export const Idle = () =>
; 14 | 15 | -------------------------------------------------------------------------------- /stories/3-metadata.stories.js: -------------------------------------------------------------------------------- 1 | 2 | import React from "react"; 3 | import { action } from '@storybook/addon-actions'; 4 | 5 | import Metadata from '../src/components/Metadata' 6 | 7 | export default { 8 | title: 'Components /Metadata', 9 | component: Metadata, 10 | argTypes: { 11 | loading: { control: 'boolean' } 12 | } 13 | } 14 | 15 | const Template = (args) => (); 16 | 17 | export const Idle = Template.bind({}); 18 | 19 | Idle.args = { 20 | metadata: { 21 | title: "", 22 | description: "", 23 | encoding: "", 24 | path: "", 25 | format: "", 26 | license: "" 27 | }, 28 | handleChange: action('change value'), 29 | handleSubmit: action('submit form'), 30 | uploadSuccess: false 31 | }; 32 | 33 | 34 | export const Loading = Template.bind({}); 35 | 36 | Loading.args = { 37 | ...Idle.args, 38 | }; 39 | 40 | export const Success = Template.bind({}); 41 | 42 | Success.args = { 43 | ...Idle.args, 44 | uploadSuccess: true 45 | }; 46 | 47 | -------------------------------------------------------------------------------- /stories/4-TableSchema.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { action } from "@storybook/addon-actions"; 3 | 4 | import TableSchema from "../src/components/TableSchema"; 5 | 6 | export default { 7 | title: "Components /TableSchema", 8 | component: TableSchema, 9 | }; 10 | 11 | const Template = (args) => ; 12 | 13 | export const Idle = Template.bind({}); 14 | 15 | Idle.args = { 16 | schema: { 17 | fields: [ 18 | { 19 | title: "", 20 | name: "name", 21 | type: "string", 22 | description: "", 23 | format: "", 24 | }, 25 | { 26 | title: "", 27 | name: "age", 28 | type: "integer", 29 | description: "", 30 | format: "", 31 | }, 32 | { 33 | title: "", 34 | name: "address", 35 | type: "string", 36 | description: "", 37 | format: "", 38 | }, 39 | ], 40 | }, 41 | data: [ 42 | { name: "Eathan Pritchard", age: 25, address: "1201 Tompkins Dr Madison" }, 43 | { name: "Zidan Berg", age: 22, address: "1309 Tompkins Dr Madison" }, 44 | { name: "Raisa Kinney", age: 32, address: "1497 Tompkins Dr Madison" }, 45 | { name: "Cara Woodley", age: 30, address: "1197 Buckeye Rd Madison" }, 46 | { name: "Komal Robbins", age: 42, address: "1192 Buckeye Rd Madison" }, 47 | { name: "Deacon Childs", age: 28, address: "1027 Tompkins Dr Madison" }, 48 | { name: "Ayse Shaw", age: 21, address: "1233 Buckeye Rd Madison" }, 49 | ], 50 | }; 51 | 52 | 53 | export const Loading = Template.bind({}); 54 | 55 | Loading.args = { 56 | ...Idle.args, 57 | }; 58 | 59 | export const Success = Template.bind({}); 60 | 61 | Success.args = { 62 | ...Idle.args, 63 | uploadSuccess: true 64 | }; 65 | -------------------------------------------------------------------------------- /stories/5-InputFile.stories.js: -------------------------------------------------------------------------------- 1 | 2 | import React from "react"; 3 | import { action } from '@storybook/addon-actions'; 4 | 5 | import InputFile from '../src/components/InputFile' 6 | 7 | export default { 8 | title: 'Components /Input/File', 9 | component: InputFile, 10 | } 11 | 12 | const Template = (args) => ( 13 |
14 | 15 |
16 | ); 17 | 18 | export const DragAndDrop = Template.bind({}); 19 | 20 | DragAndDrop.args = { 21 | onChangeHandler: action('change value'), 22 | }; 23 | -------------------------------------------------------------------------------- /stories/6-InputUrl.stories.js: -------------------------------------------------------------------------------- 1 | 2 | import React from "react"; 3 | import { action } from '@storybook/addon-actions'; 4 | 5 | import InputUrl from '../src/components/InputUrl' 6 | 7 | export default { 8 | title: 'Components /Input/Url', 9 | component: InputUrl, 10 | } 11 | 12 | const Template = (args) => ( 13 |
14 | 15 |
16 | ); 17 | 18 | export const Default = Template.bind({}); 19 | 20 | Default.args = { 21 | onChangeUrl: action('change value'), 22 | }; 23 | -------------------------------------------------------------------------------- /stories/7-Choose.stories.js: -------------------------------------------------------------------------------- 1 | 2 | import React from "react"; 3 | import { action } from '@storybook/addon-actions'; 4 | 5 | import Choose from '../src/components/Choose' 6 | 7 | export default { 8 | title: 'Components /Choose ', 9 | component: Choose, 10 | } 11 | 12 | const Template = (args) => ( 13 |
14 | 15 |
16 | ); 17 | 18 | export const Default = Template.bind({}); 19 | 20 | Default.args = { 21 | onChangeUrl: action('change url'), 22 | onChangeHandler: action('change file'), 23 | }; 24 | --------------------------------------------------------------------------------