├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── .nvmrc ├── LICENSE ├── README.md ├── build ├── .DS_Store └── index.js ├── package-lock.json ├── package.json ├── react-table-example ├── .gitignore ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json ├── src │ ├── App.css │ ├── App.js │ ├── App.test.js │ ├── index.css │ ├── index.js │ ├── logo.svg │ └── registerServiceWorker.js └── yarn.lock ├── screenshots ├── table.png ├── table_json.png ├── table_success.png └── table_w_pagination.png ├── src ├── .DS_Store ├── index.js └── style.css ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/react" 5 | ], 6 | "plugins": [ 7 | "@babel/plugin-proposal-class-properties" 8 | ] 9 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "jasmine": true 6 | }, 7 | "extends": [ 8 | "airbnb-base", 9 | "plugin:react/recommended" 10 | ], 11 | "parser": "babel-eslint", 12 | "settings": { 13 | "import/resolver": { 14 | "node": { 15 | "paths": [ 16 | "src" 17 | ], 18 | "extensions": [ 19 | ".js", 20 | ".jsx", 21 | ".ts", 22 | ".tsx" 23 | ] 24 | } 25 | }, 26 | "react": { 27 | "createClass": "createReactClass", 28 | "pragma": "React", 29 | "version": "detect" 30 | }, 31 | "propWrapperFunctions": [ 32 | "forbidExtraProps", 33 | { 34 | "property": "freeze", 35 | "object": "Object" 36 | }, 37 | { 38 | "property": "myFavoriteWrapper" 39 | } 40 | ], 41 | "linkComponents": [ 42 | "Hyperlink", 43 | { 44 | "name": "Link", 45 | "linkAttribute": "to" 46 | } 47 | ] 48 | } 49 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | react-table-example 2 | screenshots -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v6.10 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Jose 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![npm](https://img.shields.io/npm/dt/react-js-table-with-csv-dl.svg) 3 | ![npm](https://img.shields.io/npm/v/react-js-table-with-csv-dl.svg) 4 | ![npm](https://img.shields.io/npm/l/react-js-table-with-csv-dl.svg) 5 | ![npm](https://img.shields.io/npm/dm/react-js-table-with-csv-dl.svg) 6 | 7 | # React-table-with-csv-download 8 | React JS Table and log viewer with CSV download functionality accepts text, json and JSX for rendering. 9 | 10 | # Description 11 | 12 | React JS Table and log viewer with Search and CSV download functionality. You can display data table information passing JS objects and an array of data you want to show and download the full data into a csv file. You can pick what fields of the object you want to display and download the full objects. Also you can pass a String with a JSON and will be rendered. 13 | 14 | **How to thank me?** 15 | Just click on ⭐️ button :) 16 | 17 | # How it looks 18 | 19 | ![alt text](screenshots/table.png "React JS Table with download button example") 20 | 21 | # Installation 22 | 23 | Install it from npm and include it in your React build process (using Webpack, Browserify, etc). 24 | 25 | ``` 26 | npm i react-js-table-with-csv-dl 27 | ``` 28 | 29 | # Usage 30 | 31 | Import `TableViewer` in your react component. 32 | 33 | ```javascript 34 | import TableViewer from 'react-js-table-with-csv-dl'; 35 | ``` 36 | 37 | Props available: 38 | * `content` - An array of objects. The key will be used for the table headers. 39 | 40 | ```javascript 41 | let table = [ 42 | {number: 12, name: "Del Piero", position: "ST"}, 43 | {number: 21, name: "Pirlo", position: "MC"}, 44 | {number: 1, name: "Buffon", position: "GK"} 45 | ]; 46 | ``` 47 | 48 | In the above example, will create a table with three columns: `number, name, position` 49 | 50 | Optionally, you can add the key `success` to the object. If value is true, the row will be displayed in green, if it is false will be displayed red. 51 | 52 | ```javascript 53 | let table = [ 54 | {number: 12, name: "Del Piero", position: "ST", success: true}, 55 | {number: 21, name: "Pirlo", position: "MC", success: false}, 56 | {number: 10, name: "Ruiz", position: "MDI"}, 57 | ]; 58 | ``` 59 | 60 | The above object will be displayed as follows: 61 | 62 | ![alt text](screenshots/table_success.png "React JS Table with semantic results") 63 | 64 | * `headers` - An array of strings with the headers you want to display 65 | 66 | `["number", "name"]` 67 | 68 | Use the same names as the object you are passing as prop. 69 | In this case, the table will show only `name` and `number`. In case of downloading data, will download the full object including the `position`. This gives you the ability of showing some fields and being able to download full data rows. 70 | 71 | * `minHeight`, `maxHeight` - Min and Max height dimensions for the table 72 | 73 | * `activateDownloadButton` - (Boolean) if you want to have a download button 74 | 75 | 76 | For example: 77 | ```javascript 78 | 86 | ``` 87 | 88 | You can send a JSON String to a specific cell and will be rendered correctly with different color scheme as shows in the following image: 89 | 90 | ```javascript 91 | let json = { 92 | "Key1": {"id":10,"values":"1-2"}, 93 | "Key2":{}, 94 | "Key3":"(Zone 1)", 95 | "description":"","array":[100], 96 | "array2":[], 97 | "Object":[{"id":1000,"values":"K-1"}] 98 | }; 99 | ``` 100 | 101 | Note that you should convert JSON to a String using the function stringify(). Then, you'll have the result: 102 | 103 | ![alt text](screenshots/table_json.png "React JS Table JSON example") 104 | 105 | You can also customise the row's color (red, green or black) by sending the following row to the table with the key `success`: 106 | 107 | true -> green 108 | false -> red 109 | omit the key and the text will be black by default 110 | 111 | ```javascript 112 | { 113 | number: 1, 114 | name:() =>
Buffon
, 115 | position: JSON.stringify(json), 116 | success: true 117 | } 118 | ``` 119 | 120 | 121 | If you have big tables, you can optionally add a pagination for the table using the prop `pagination`. 122 | 123 | 124 | ![alt text](screenshots/table_w_pagination.png "React JS Table with pagination") 125 | 126 | You can also customize the style using the following props: 127 | 128 | * `headerCss` => style for headers e.g passing: {{color: "blue", backgroundColor:"#fff"}} 129 | Changes the header background to white and the text to blue 130 | 131 | * `bodyCss` => style for each row e.g passing: {{color: "blue", backgroundColor:"#fff"}} 132 | Changes the background to white and the text to blue 133 | 134 | # Props 135 | 136 | | Name | Type | Mandatory | Description 137 | | ------------- |:-------------:| :-----:|:-----| 138 | | activateDownloadButton | boolean |Y | Activates download button | 139 | | content | object | Y |Contents to display on tables | 140 | | headers | array (String) | Y | Array of strings, these will be used to choose what to show in the table | 141 | | maxHeight | integer |Y | Max table desired height | 142 | | minHeight | integer | Y| Min table desired height | 143 | | caseInsensitive | boolean |N| do searches without casing| 144 | | encoding | String |N| Data encoding for table and file, UTF-8 by default | 145 | | filename | String |N | Name of the downloaded filename (default is logResults.csv) | 146 | | maxPagesToDisplay| int | N | how many elements will the paginator have. Default 6 | 147 | | pagination| int | N | integer that will indicate the max page size for the table | 148 | | placeholderSearchText| string |N| Placeholder text to appear in Searchbox | 149 | | renderLineNumber| present | N | render row number at the left of the table | 150 | | reverseLineNumber| present | N | reverse line number to start from last (depends on reverseLineNumber) | 151 | |searchEnabled| presence (boolean) |N| Activate search feature| 152 | |sortColumn| string |N| Column that you want to sort Asc. (must be in headers prop)| 153 | | topPagination | boolean |N| show pagination at top of the table| 154 | 155 | # Styling Props 156 | 157 | | Name | Type | Mandatory | Description 158 | | ------------- |:-------------:| -----:|:-----| 159 | | activePageBoxStyle| object | N | customize style of active box | 160 | | bodyCss | object |N | Body customizations | 161 | | downloadButtonStyle| object | N | download button customizations | 162 | | headerCss | object |N | Headers customization | 163 | | pageBoxStyle| object | N | customize style of pagination box objects | 164 | | tableStyle | object |N | Overall table style/size | 165 | | titleStyle | object |N | Overall tile style/size | 166 | | errorColor | string |N | Hex value for text (default: #b30009)| 167 | | successColor | string |N | Hex value for text (default: #0b7012)| 168 | | warningColor | string |N | Hex value for text (default: #ba8722)| 169 | 170 | # What's new 171 | 172 | v0.9.7 173 | * Search bar updated 174 | 175 | v0.9.6 176 | * More size optimizations and paginator dependency updated to 0.2.1 177 | 178 | v0.9.5 179 | * Dependencies updated 180 | * Overall package size optimized 181 | 182 | v0.9.4 183 | * Dependencies updated 184 | 185 | v0.9.1 186 | * Html render -> send html from a func: `htmlTest = () =>
Yep
` 187 | * Dependencies updated 188 | * Overall package size optimized 189 | 190 | v0.8.7 191 | * Added feature for line change, if you send text with the escape char \n, it will be rendered in different lines in the UI, and also escaped in the CSV download. 192 | E.g: let text = '"Gianluigi\nBuffon"'; Check the Example in GitHub. 193 | 194 | v0.8.6 195 | * If there is a tie between an error and a warning, the error will have priority and show the error color 196 | 197 | v0.8.3 198 | * Added feature to generate CSV file with all different columns, even if some rows don't have data for that column. This means that everything will be visible. 199 | 200 | v0.7.10 201 | * Added warning feature. Will color text in yellow row in case the row has the key warning (boolean:true) 202 | * Added props for errorColor, successColor, warningColor for further customization 203 | 204 | v0.7.9 205 | * Dependencies updated 206 | 207 | v0.7.8 208 | * Bug fixing and check title style check improvements. Please update if using v >=0.7.0 209 | 210 | v0.7.5 211 | * Small bug fixes, checks and improvements 212 | 213 | v0.7.3 214 | * Title will only appear if table has any kind of contents 215 | 216 | v0.7.1 217 | * New style prop for title called `titleStyle` 218 | 219 | v0.7.0 220 | * Several CSS changes made to table viewer 221 | * CSS refactored, base name changed 222 | * New CSS prop: `tableStyle` for overall table styling 223 | * Possible breaking changes: if you rely on the old css `tableViewer` prefix, please change it to: `tableWithCSV` 224 | 225 | # License 226 | Licensed under the MIT License © [jciccio](https://www.npmjs.com/~jciccio) 227 | -------------------------------------------------------------------------------- /build/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jciccio/react-table-with-csv-download/ded2b64ca1800c24122e5301fe8d5f74e0fbce4a/build/.DS_Store -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-js-table-with-csv-dl", 3 | "version": "0.9.7", 4 | "description": "React JS tables and log viewer with stats if needed. Has the functionality to dowload table contents in CSV file with data stored in class prop.", 5 | "main": "build/index.js", 6 | "peerDependencies": { 7 | "react": "^16.0.0" 8 | }, 9 | "dependencies": { 10 | "file-saver": "^1.3.8", 11 | "react": "^16.0.0", 12 | "react-html-parser": "^2.0.2", 13 | "react-js-paginator": "^0.2.1", 14 | "react-js-search": "^0.3.0" 15 | }, 16 | "scripts": { 17 | "test": "echo \"Error: no test specified\" && exit 1", 18 | "start": "webpack --watch", 19 | "build": "webpack", 20 | "lint": "eslint --report-unused-disable-directives src", 21 | "fix": "eslint --fix --report-unused-disable-directives src" 22 | }, 23 | "author": { 24 | "name": "Jose Antonio Ciccio", 25 | "email": "jciccio@gmail.com" 26 | }, 27 | "devDependencies": { 28 | "style-loader": "^1.0.1", 29 | "webpack": "^4.41.2", 30 | "@babel/core": "^7.8.7", 31 | "@babel/plugin-proposal-class-properties": "^7.8.3", 32 | "@babel/preset-env": "^7.8.7", 33 | "@babel/preset-react": "^7.8.3", 34 | "babel-loader": "^8.1.0", 35 | "css-loader": "^3.4.2", 36 | "eslint": "^6.8.0", 37 | "eslint-config-airbnb-base": "^14.0.0", 38 | "eslint-plugin-import": "^2.20.1", 39 | "eslint-plugin-react": "^7.19.0", 40 | "react-dom": "^16.6.3", 41 | "react-icons": "^2.2.7", 42 | "webpack-cli": "^3.3.11" 43 | }, 44 | "repository": { 45 | "type": "git", 46 | "url": "https://github.com/jciccio/react-table-with-csv-download.git" 47 | }, 48 | "keywords": [ 49 | "table", 50 | "log viewer", 51 | "log", 52 | "stats", 53 | "csv download", 54 | "html table", 55 | "react table", 56 | "react download csv", 57 | "csv", 58 | "customizable table", 59 | "search table", 60 | "search" 61 | ], 62 | "license": "MIT", 63 | "homepage": "https://github.com/jciccio/react-table-with-csv-download", 64 | "bugs": { 65 | "url": "https://github.com/jciccio/react-table-with-csv-download/issues" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /react-table-example/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | # See https://help.github.com/ignore-files/ for more about ignoring files. 3 | 4 | # dependencies 5 | /node_modules 6 | 7 | # testing 8 | /coverage 9 | 10 | # production 11 | /build 12 | 13 | # misc 14 | .DS_Store 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | -------------------------------------------------------------------------------- /react-table-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-table-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^16.4.0", 7 | "react-dom": "^16.13.1", 8 | "react-scripts": "^3.0.0" 9 | }, 10 | "scripts": { 11 | "start": "react-scripts start", 12 | "build": "react-scripts build", 13 | "test": "react-scripts test --env=jsdom", 14 | "eject": "react-scripts eject" 15 | }, 16 | "browserslist": { 17 | "production": [ 18 | ">0.2%", 19 | "not dead", 20 | "not op_mini all" 21 | ], 22 | "development": [ 23 | "last 1 chrome version", 24 | "last 1 firefox version", 25 | "last 1 safari version" 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /react-table-example/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jciccio/react-table-with-csv-download/ded2b64ca1800c24122e5301fe8d5f74e0fbce4a/react-table-example/public/favicon.ico -------------------------------------------------------------------------------- /react-table-example/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /react-table-example/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 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /react-table-example/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 80px; 8 | } 9 | 10 | .App-header { 11 | background-color: #222; 12 | height: 150px; 13 | padding: 20px; 14 | color: white; 15 | } 16 | 17 | .App-title { 18 | font-size: 1.5em; 19 | } 20 | 21 | .App-intro { 22 | font-size: large; 23 | } 24 | 25 | @keyframes App-logo-spin { 26 | from { transform: rotate(0deg); } 27 | to { transform: rotate(360deg); } 28 | } 29 | 30 | .paginator{ 31 | } 32 | -------------------------------------------------------------------------------- /react-table-example/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import TableViewer from 'react-js-table-with-csv-dl'; 3 | import './App.css'; 4 | 5 | class App extends Component { 6 | 7 | constructor(props) { 8 | super(props); 9 | 10 | const headers = ["number", "position", "name", "metadata"]; 11 | 12 | 13 | const json = { 14 | "Key1": {"id":10,"values":"1-2"}, 15 | "Key2":{}, 16 | "Key3":"(Zone 1)", 17 | "description":"","array":[100], 18 | "array2":[], 19 | "Object":[{"id":1000,"values":"K-1"}]}; 20 | 21 | const text = '"Gianluigi\nBuffon"'; 22 | 23 | const htmlTest = () =>
Yep
24 | 25 | const table = [ 26 | {number: 12, name:text, success: true}, 27 | {number: 21, name: htmlTest, metadata: JSON.stringify(json), success: false}, 28 | {number: 10, name: htmlTest, position: "MDI"}, 29 | {number: 7, name: "Nesta", position: "RB"}, 30 | {number: 4, name: "Cannavaro", metadata: JSON.stringify(json), age: 38}, 31 | {number: 2, name: htmlTest, position: "CB", success: false, foot: "lefty"}, 32 | {number: 15, name: "Bonaventura", position: "MD", warning:true} 33 | ] 34 | 35 | this.state = { 36 | table: table, 37 | headers: headers, 38 | activateDownloadButton:true 39 | }; 40 | 41 | } 42 | 43 | render() { 44 | let overallStyle = {"width":'auto'}; 45 | return ( 46 |
47 |
48 |

Table Component

49 |
50 | 51 |

Table example:

52 | 68 |
69 | ); 70 | } 71 | } 72 | 73 | export default App; 74 | -------------------------------------------------------------------------------- /react-table-example/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /react-table-example/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /react-table-example/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import registerServiceWorker from './registerServiceWorker'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | registerServiceWorker(); 9 | -------------------------------------------------------------------------------- /react-table-example/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /react-table-example/src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | 'This web app is being served cache-first by a service ' + 44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ' 45 | ); 46 | }); 47 | } else { 48 | // Is not local host. Just register service worker 49 | registerValidSW(swUrl); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then(registration => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | installingWorker.onstatechange = () => { 62 | if (installingWorker.state === 'installed') { 63 | if (navigator.serviceWorker.controller) { 64 | // At this point, the old content will have been purged and 65 | // the fresh content will have been added to the cache. 66 | // It's the perfect time to display a "New content is 67 | // available; please refresh." message in your web app. 68 | console.log('New content is available; please refresh.'); 69 | } else { 70 | // At this point, everything has been precached. 71 | // It's the perfect time to display a 72 | // "Content is cached for offline use." message. 73 | console.log('Content is cached for offline use.'); 74 | } 75 | } 76 | }; 77 | }; 78 | }) 79 | .catch(error => { 80 | console.error('Error during service worker registration:', error); 81 | }); 82 | } 83 | 84 | function checkValidServiceWorker(swUrl) { 85 | // Check if the service worker can be found. If it can't reload the page. 86 | fetch(swUrl) 87 | .then(response => { 88 | // Ensure service worker exists, and that we really are getting a JS file. 89 | if ( 90 | response.status === 404 || 91 | response.headers.get('content-type').indexOf('javascript') === -1 92 | ) { 93 | // No service worker found. Probably a different app. Reload the page. 94 | navigator.serviceWorker.ready.then(registration => { 95 | registration.unregister().then(() => { 96 | window.location.reload(); 97 | }); 98 | }); 99 | } else { 100 | // Service worker found. Proceed as normal. 101 | registerValidSW(swUrl); 102 | } 103 | }) 104 | .catch(() => { 105 | console.log( 106 | 'No internet connection found. App is running in offline mode.' 107 | ); 108 | }); 109 | } 110 | 111 | export function unregister() { 112 | if ('serviceWorker' in navigator) { 113 | navigator.serviceWorker.ready.then(registration => { 114 | registration.unregister(); 115 | }); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /screenshots/table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jciccio/react-table-with-csv-download/ded2b64ca1800c24122e5301fe8d5f74e0fbce4a/screenshots/table.png -------------------------------------------------------------------------------- /screenshots/table_json.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jciccio/react-table-with-csv-download/ded2b64ca1800c24122e5301fe8d5f74e0fbce4a/screenshots/table_json.png -------------------------------------------------------------------------------- /screenshots/table_success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jciccio/react-table-with-csv-download/ded2b64ca1800c24122e5301fe8d5f74e0fbce4a/screenshots/table_success.png -------------------------------------------------------------------------------- /screenshots/table_w_pagination.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jciccio/react-table-with-csv-download/ded2b64ca1800c24122e5301fe8d5f74e0fbce4a/screenshots/table_w_pagination.png -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jciccio/react-table-with-csv-download/ded2b64ca1800c24122e5301fe8d5f74e0fbce4a/src/.DS_Store -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import MdFileDownload from 'react-icons/lib/md/file-download'; 3 | import Search from 'react-icons/lib/md/search'; 4 | import Paginator from 'react-js-paginator'; 5 | import SearchBar from 'react-js-search'; 6 | import FileSaver from 'file-saver/FileSaver'; 7 | import PropTypes from 'prop-types'; 8 | import ReactHtmlParser from 'react-html-parser'; 9 | import './style.css'; 10 | 11 | 12 | /** 13 | * TableViewer component 14 | * @author [Jose Antonio Ciccio](https://github.com/jciccio) 15 | */ 16 | 17 | class TableViewer extends Component { 18 | constructor(props) { 19 | super(props); 20 | this.generateAndDownloadCSV = this.generateAndDownloadCSV.bind(this); 21 | this.state = { 22 | currentPage: 1, 23 | searchResults: null, 24 | }; 25 | 26 | if (this.props.content && this.props.sortColumn) { 27 | this.sortTable(); 28 | } 29 | } 30 | 31 | highlightSyntax(json) { 32 | if (json) { 33 | json = json 34 | .replace(/&/g, '&') 35 | .replace(//g, '>'); 37 | 38 | return json.replace( 39 | /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g, 40 | (match) => { 41 | let cls = 'hljs-number'; 42 | if (/^"/.test(match)) { 43 | if (/:$/.test(match)) { 44 | cls = 'hljs-key'; 45 | } else { 46 | cls = 'hljs-string'; 47 | } 48 | } else if (/true|false/.test(match)) { 49 | cls = 'hljs-boolean'; 50 | } else if (/null/.test(match)) { 51 | cls = 'hljs-null'; 52 | } 53 | return `${match}`; 54 | }, 55 | ); 56 | } 57 | return ''; 58 | } 59 | 60 | componentDidUpdate(prevProps) { 61 | if (prevProps.content !== this.props.content && this.props.sortColumn) { 62 | this.sortTable(); 63 | } 64 | } 65 | 66 | sortTable() { 67 | const criteria = this.props.sortColumn; 68 | if (criteria) { 69 | this.props.content.sort(this.compare(criteria)); 70 | } 71 | } 72 | 73 | compare(criteria) { 74 | return (a, b) => { 75 | if (a[criteria] < b[criteria]) { return -1; } 76 | if (a[criteria] > b[criteria]) { return 1; } 77 | return 0; 78 | }; 79 | } 80 | 81 | generateAndDownloadCSV() { 82 | const encoding = this.props.encoding ? this.props.encoding : 'UTF-8'; 83 | const csvType = { encoding, type: `text/plain;charset=${encoding}` }; 84 | const filename = this.props.filename ? this.props.filename : 'logResults.csv'; 85 | let csvContent = ''; 86 | const data = this.props.content; 87 | const headers = []; 88 | this.props.content.forEach((rowObj) => { 89 | if (headers === undefined || headers.length === 0) { 90 | for (const property in rowObj) { 91 | if (rowObj.hasOwnProperty(property)) { 92 | headers.push(property); 93 | } 94 | } 95 | } else { 96 | for (const property in rowObj) { 97 | if (rowObj.hasOwnProperty(property)) { 98 | if (headers.indexOf(property) == -1) { 99 | headers.push(property); 100 | } 101 | } 102 | } 103 | } 104 | const rowData = []; 105 | for (const i in headers) { 106 | let data = rowObj[headers[i]]; 107 | if (data && typeof data === 'string' && data.indexOf(',') >= 0) { 108 | data = `"${data.replace(/"/g, '""')}"`; 109 | } 110 | rowData.push(data); 111 | } 112 | const row = rowData.join(','); 113 | csvContent += `${row}\r\n`; 114 | }); 115 | const row = headers.join(','); 116 | csvContent = `${row}\r\n${csvContent}`; 117 | const blob = new Blob([csvContent], csvType); 118 | FileSaver.saveAs(blob, filename); 119 | } 120 | 121 | renderDownload() { 122 | if (this.props.activateDownloadButton) { 123 | const buttonStyle = this.props.downloadButtonStyle ? this.props.downloadButtonStyle : {}; 124 | return ( 125 |
126 | 136 |
137 | ); 138 | } 139 | 140 | return null; 141 | } 142 | 143 | renderTitle() { 144 | const titleStyle = this.props.titleStyle ? this.props.titleStyle : {}; 145 | if (Array.isArray(this.props.content) && this.props.content.length > 0) { 146 | return ( 147 |

{this.props.title}

148 | ); 149 | } 150 | 151 | return null; 152 | } 153 | 154 | render() { 155 | const tableStyle = this.props.tableStyle ? this.props.tableStyle : {}; 156 | const height = { maxHeight: this.props.maxHeight, ...tableStyle }; 157 | return ( 158 |
159 | {this.renderTitle()} 160 | {this.renderStats()} 161 |
162 | {this.renderDownload()} 163 | {this.renderTopPagination()} 164 |
165 | {this.renderSearch()} 166 |
167 |
168 | 169 |
170 |
171 |
{this.renderHeaders()}
172 |
{this.renderBody()}
173 |
174 |
175 | {this.renderPagination()} 176 |
177 | ); 178 | } 179 | 180 | onSearch(term, elements) { 181 | if (term.length > 0) { 182 | this.setState({ searchResults: elements }); 183 | } else { 184 | this.setState({ searchResults: null }); 185 | } 186 | this.pageChange(1); 187 | } 188 | 189 | renderSearch() { 190 | if (this.props.searchEnabled) { 191 | let search = 'Search...'; 192 | if (this.props.placeholderSearchText) { 193 | search = this.props.placeholderSearchText; 194 | } 195 | const caseInsensitive = !!this.props.caseInsensitive; 196 | return ( 197 | { this.onSearch(b, e); }} 199 | onSearchButtonClick={(b, e) => { this.onSearch(b, e); }} 200 | placeHolderText={search} 201 | data={this.props.content} 202 | caseInsensitive={caseInsensitive} 203 | /> 204 | ); 205 | } 206 | return null; 207 | } 208 | 209 | renderTopPagination() { 210 | if (this.props.topPagination) { 211 | return this.renderPagination(true); 212 | } 213 | return null; 214 | } 215 | 216 | renderPagination(isTop = false) { 217 | if (this.props.pagination || isTop) { 218 | const boxStyle = this.props.pageBoxStyle ? this.props.pageBoxStyle : {}; 219 | const activeStyle = this.props.activePageBoxStyle ? this.props.activePageBoxStyle : {}; 220 | const pagesDisplay = this.props.maxPagesToDisplay ? this.props.maxPagesToDisplay : 5; 221 | if (this.props.content.length <= this.props.pagination) { 222 | return null; 223 | } 224 | 225 | let totalElements = this.props.content.length; 226 | if (this.state.searchResults) { 227 | totalElements = this.state.searchResults.length; 228 | } 229 | return ( 230 | { this.pageChange(e); }} 234 | pageBoxStyle={boxStyle} 235 | activePageBoxStyle={activeStyle} 236 | maxPagesToDisplay={pagesDisplay} 237 | /> 238 | ); 239 | } 240 | return null; 241 | } 242 | 243 | pageChange(page) { 244 | this.setState({ currentPage: page }); 245 | } 246 | 247 | renderAllRows() { 248 | const rows = this.props.content; 249 | const { headers } = this.props; 250 | return rows.map((row, i) => this.getRow(row, i)); 251 | } 252 | 253 | renderRowPage(rows) { 254 | const rowsContent = []; 255 | const pageStart = (this.state.currentPage - 1) * this.props.pagination; 256 | const rowQty = rows.length; 257 | const { headers } = this.props; 258 | for (let i = pageStart; i < pageStart + this.props.pagination && rows[i]; i++) { 259 | rowsContent.push(this.getRow(rows[i], i)); 260 | } 261 | return rowsContent; 262 | } 263 | 264 | renderBody() { 265 | const rows = this.state.searchResults || this.props.content; 266 | if (rows !== null) { 267 | if (this.props.pagination) { 268 | return this.renderRowPage(rows); 269 | } 270 | return this.renderAllRows(rows); 271 | } 272 | 273 | return (null); 274 | } 275 | 276 | getRow(row, i) { 277 | const isWarning = row.warning || false; 278 | const isSucccess = row.success; 279 | let fontColor = '#000000'; 280 | 281 | if (isSucccess === false) { // because can be undefined 282 | fontColor = (this.props.errorColor) ? this.props.errorColor : '#b30009'; 283 | } else if (isWarning) { 284 | fontColor = (this.props.warningColor) ? this.props.warningColor : '#ba8722'; 285 | } else if (isSucccess === true) { 286 | fontColor = (this.props.successColor) ? this.props.successColor : '#0b7012'; 287 | } 288 | 289 | return ( 290 |
295 | {this.renderRow(row, i)} 296 |
297 | ); 298 | } 299 | 300 | renderLineNumber(i) { 301 | return ( 302 |
305 | {i} 306 |
307 | ); 308 | } 309 | 310 | renderNumberHeader(headerCss) { 311 | if (this.props.renderLineNumber) { 312 | return ( 313 |
314 | Line 315 |
316 | ); 317 | } 318 | 319 | return null; 320 | } 321 | 322 | isFunction(functionToCheck) { 323 | return functionToCheck && {}.toString.call(functionToCheck) === '[object Function]'; 324 | } 325 | 326 | renderRow(row, i) { 327 | const { headers } = this.props; 328 | if (row) { 329 | const rowData = []; 330 | // Render line number 331 | if (this.props.renderLineNumber) { 332 | let number = i + 1; 333 | if (this.props.reverseLineNumber) { 334 | number = this.props.content.length - i; 335 | } 336 | rowData.push(this.renderLineNumber(number)); 337 | } 338 | // Create content 339 | const rowContent = headers.map((header, element) => { 340 | let content = row[header]; 341 | let isJson = false; 342 | try { 343 | if (isNaN(content)) { 344 | content = JSON.parse(content); 345 | isJson = true; 346 | } 347 | } catch (e) { 348 | content = row[header]; 349 | isJson = false; 350 | try{ 351 | if (this.isFunction(content)){ 352 | content = content(); 353 | } 354 | } 355 | catch (e){ 356 | if (content) { 357 | content = content.split('\n').map((item, i) => (
{item}
)); 358 | } 359 | } 360 | } 361 | if (isJson) { 362 | return this.renderJsonContent(content,i, element); 363 | } 364 | 365 | 366 | return ( 367 |
370 | { content } 371 |
372 | ); 373 | }); 374 | return [...rowData, ...rowContent]; 375 | } 376 | 377 | return null; 378 | } 379 | 380 | renderJsonContent(content,i,element){ 381 | const jsonText = JSON.stringify(content, undefined, 2); 382 | const highlight = this.highlightSyntax(jsonText); 383 | const parsedHtml = ReactHtmlParser(highlight, true); 384 | return ( 385 |
388 |
389 |           {parsedHtml}
390 |         
391 |
392 | ); 393 | } 394 | 395 | renderHeaders() { 396 | const { headers } = this.props; 397 | const { headerCss } = this.props; 398 | if (headers) { 399 | return ( 400 |
401 | {this.renderNumberHeader(headerCss)} 402 | {headers.map((header, index) => ( 403 |
404 | {header} 405 |
406 | ))} 407 |
408 | ); 409 | } 410 | 411 | return null; 412 | } 413 | 414 | renderStats() { 415 | if (this.props.renderStats) { 416 | return ( 417 |
418 | 419 |
420 | 423 |
424 | 427 |
428 | 429 |
430 | 431 |
432 |
433 | ); 434 | } 435 | 436 | return null; 437 | } 438 | } 439 | 440 | TableViewer.propTypes = { 441 | content: PropTypes.array.isRequired, 442 | headers: PropTypes.array.isRequired, 443 | minHeight: PropTypes.number.isRequired, 444 | maxHeight: PropTypes.number.isRequired, 445 | activateDownloadButton: PropTypes.bool, 446 | topPaginator: PropTypes.bool, 447 | headerCss: PropTypes.object, 448 | titleStyle: PropTypes.object, 449 | bodyCss: PropTypes.object, 450 | filename: PropTypes.string, 451 | renderLineNumber: PropTypes.bool, 452 | reverseLineNumber: PropTypes.bool, 453 | pagination: PropTypes.number, 454 | pageBoxStyle: PropTypes.object, 455 | activePageBoxStyle: PropTypes.object, 456 | maxPagesToDisplay: PropTypes.number, 457 | downloadButtonStyle: PropTypes.object, 458 | sortColumn: PropTypes.string, 459 | encoding: PropTypes.string, 460 | successColor: PropTypes.string, 461 | warningColor: PropTypes.string, 462 | errorColor: PropTypes.string, 463 | }; 464 | 465 | export default TableViewer; 466 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | .tableWithCSV{ 2 | margin: 0 auto; 3 | font-family: Helvetica; 4 | font-size: 14px; 5 | padding: 0px 0px; 6 | overflow: auto; 7 | text-align: center; 8 | font-family: "Trebuchet MS", Arial, Helvetica, sans-serif; 9 | margin: 10px 1%; 10 | } 11 | 12 | .tableWithCSVTable{ 13 | text-align: center; 14 | font-family: "Trebuchet MS", Arial, Helvetica, sans-serif; 15 | margin: 10px 0; 16 | width: 100%; 17 | } 18 | 19 | .tableWithCSV .header{ 20 | /*width: inherit;*/ 21 | /* padding: 0 8px;*/ 22 | } 23 | 24 | .tableWithCSV .header1{ 25 | width: inherit; 26 | padding: 0 8px; 27 | } 28 | 29 | .tableWithCSV .header2{ 30 | width: inherit; 31 | padding: 0 8px; 32 | } 33 | 34 | .tableWithCSV .tableWithCSVTableHeader{ 35 | border: 2px solid; 36 | text-align: center; 37 | color: #000; 38 | font-weight: bold; 39 | font-family: sans-serif; 40 | border: 1px solid #fff; 41 | background-color: #0F991B; 42 | color: white; 43 | width: inline-block; 44 | padding: 4px 8px; 45 | font-weight: bold; 46 | 47 | /* width: 100%;*/ 48 | table-layout: fixed; 49 | border-collapse: collapse; 50 | } 51 | 52 | .tableWithCSV .tableWithCSVBody:nth-child(even){ 53 | background-color: #f2f2f2; 54 | } 55 | 56 | .tableWithCSV .tableWithCSVTableBody td { 57 | padding: 0px; 58 | border: 1px solid #ddd; 59 | } 60 | 61 | 62 | 63 | .tableWithCSV .divTableHeading .divTableRow .divTableCell:first-child{ 64 | border-left: 0px; 65 | } 66 | 67 | .tableWithCSV .divTableHeading .divTableRow .divTableCell:last-child{ 68 | border-right: 0px; 69 | } 70 | 71 | .tableWithCSV .fixed_headers { 72 | /*width: 100%;*/ 73 | table-layout: fixed; 74 | border-collapse: collapse; 75 | } 76 | 77 | .tableWithCSV th, td { 78 | padding: 5px; 79 | text-align: left; 80 | } 81 | 82 | .tableWithCSV thead tr { 83 | /* display: block; 84 | position: relative;*/ 85 | } 86 | 87 | .tableWithCSV tbody { 88 | /* display: block;*/ 89 | overflow: auto; 90 | width: 100%; 91 | height: 300px; 92 | } 93 | 94 | .tableWithCSV .divTableContainer{ 95 | width:100%; 96 | float: left; 97 | } 98 | 99 | .tableWithCSV .divTableContainer .divTable{ 100 | display: table; 101 | overflow: auto; 102 | text-align: center; 103 | font-family: "Trebuchet MS", Arial, Helvetica, sans-serif; 104 | width:100%; 105 | margin-bottom: 10px; 106 | } 107 | 108 | .tableWithCSV .divTableRow { 109 | display: table-row; 110 | } 111 | 112 | .tableWithCSV .divTableHeading { 113 | padding: 5px; 114 | background-color: #38bcc3; 115 | color: white; 116 | display: table-header-group; 117 | font-weight: bold; 118 | } 119 | .tableWithCSV .divTableCell, .divTableHead { 120 | border: 1px solid #ddd; 121 | display: table-cell; 122 | padding: 4px 10px; 123 | } 124 | 125 | .tableWithCSV .divTableFoot { 126 | background-color: #EEE; 127 | display: table-footer-group; 128 | font-weight: bold; 129 | } 130 | 131 | .tableWithCSV .divTableBody { 132 | display: table-row-group; 133 | } 134 | 135 | .tableWithCSV .divTableRow:nth-child(even){ 136 | background-color: #f2f2f2; 137 | } 138 | 139 | .tableWithCSV .divTableBody div { 140 | padding: 4px; 141 | /*border: 1px solid #ddd;*/ 142 | } 143 | 144 | .tableWithCSV .loader { 145 | border: 3px solid #f3f3f3; 146 | border-top: 3px solid #3498db; 147 | border-radius: 50%; 148 | width: 20px; 149 | height: 20px; 150 | -webkit-animation: spin 1s linear infinite; 151 | animation: spin 1s linear infinite; 152 | margin: 0 5px; 153 | display: inline-flex; 154 | } 155 | 156 | @keyframes spin { 157 | 0% { transform: rotate(0deg); } 158 | 100% { transform: rotate(360deg); } 159 | } 160 | 161 | .tableWithCSV .csvFileDownloader{ 162 | margin-bottom: 10px; 163 | 164 | text-align: left; 165 | } 166 | 167 | 168 | 169 | .tableWithCSV .csvFileDownloader button{ 170 | border: 0px solid #e7e7e7; 171 | background-color: #e7e7e7; 172 | color: #000; 173 | padding: 1px 8px; 174 | text-align: center; 175 | text-decoration: none; 176 | display: inline-block; 177 | font-size: 14px; 178 | -webkit-transition-duration: 0.4s; 179 | transition-duration: 0.4s; 180 | margin-top: 8px; 181 | } 182 | 183 | .tableWithCSV .csvFileDownloader button:hover { 184 | background-color: #adadad; /* Green */ 185 | color: #FFF; 186 | } 187 | 188 | .tableWithCSV .tableTitle{ 189 | font-weight: 600; 190 | font-size: 35px; 191 | } 192 | 193 | .tableWithCSV a { 194 | /*float: left;*/ 195 | display: block; 196 | color: black; 197 | text-align: center; 198 | /*padding: 14px 16px;*/ 199 | text-decoration: none; 200 | font-size: 17px; 201 | } 202 | 203 | .tableWithCSV a:hover { 204 | background-color: #ddd; 205 | color: black; 206 | } 207 | 208 | .tableWithCSV a.active { 209 | background-color: #2196F3; 210 | color: white; 211 | } 212 | 213 | .tableWithCSV .search-container { 214 | float: right; 215 | margin-bottom: 5px; 216 | } 217 | 218 | 219 | .tableWithCSV .search-container button { 220 | float: right; 221 | padding: 6px 10px; 222 | margin-top: 8px; 223 | margin-right: 16px; 224 | background: #ddd; 225 | font-size: 17px; 226 | border: none; 227 | cursor: pointer; 228 | } 229 | 230 | .tableWithCSV .search-container button:hover { 231 | background: #ccc; 232 | } 233 | 234 | @media screen and (max-width: 600px) { 235 | .search-container { 236 | float: none; 237 | } 238 | .search-container .a, .search-container input[type=text], .search-container button { 239 | float: none; 240 | display: block; 241 | text-align: left; 242 | width: 100%; 243 | margin: 0; 244 | padding: 14px; 245 | } 246 | .search-container input[type=text] { 247 | border: 1px solid #ccc; 248 | } 249 | } 250 | 251 | .tableWithCSV .search-container { 252 | float: right; 253 | padding-right: 0px; 254 | } 255 | 256 | .hljs-string { color: #0052cc; } 257 | .hljs-number { color: #009933; } 258 | .hljs-boolean { color: blue; } 259 | .hljs-null { color: magenta; } 260 | .hljs-key { color: #000; } 261 | 262 | .jsonviewer_json pre{ 263 | height: 746px; 264 | } 265 | 266 | pre { 267 | text-align: left; 268 | } 269 | 270 | .paginator{ 271 | margin-top: 10px; 272 | width: fit-content; 273 | } 274 | 275 | .searchComponent{ 276 | overflow: hidden; 277 | } 278 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | module.exports = { 3 | entry: './src/index.js', 4 | 5 | output: { 6 | path: path.resolve(__dirname, 'build'), 7 | filename: 'index.js', 8 | libraryTarget: 'commonjs2', 9 | 10 | }, 11 | 12 | module: { 13 | 14 | 15 | rules: [ 16 | { 17 | test: /\.js$/, 18 | include: path.resolve(__dirname, 'src'), 19 | exclude: /(node_modules|bower_components|build)/, 20 | use: { 21 | loader: 'babel-loader' 22 | 23 | } 24 | }, 25 | { 26 | test: /\.css$/, 27 | include: path.resolve(__dirname, 'src'), 28 | exclude: /(node_modules|bower_components|build)/, 29 | use:['style-loader','css-loader'] 30 | } 31 | ] 32 | }, 33 | externals: { 34 | 'react': 'commonjs react' // this line is just to use the React dependency of our parent-testing-project instead of using our own React. 35 | } 36 | }; --------------------------------------------------------------------------------