├── .gitattributes ├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── README.md ├── build.bat ├── build.sh ├── build_dev.bat ├── build_dev.sh ├── build_release ├── build_release.sh ├── start.bat └── start.sh ├── config ├── poli.docker.properties └── poli.properties ├── db ├── poli.db ├── schema-postgresql.sql └── schema-sqlite.sql ├── docs ├── .nojekyll ├── _coverpage.md ├── _images │ └── screenshots │ │ ├── apikey.jpg │ │ ├── apikey2.jpg │ │ ├── autorefresh.jpg │ │ ├── canned_report.jpg │ │ ├── component_card.jpg │ │ ├── component_funnel.jpg │ │ ├── component_heatmap.jpg │ │ ├── component_treemap.jpg │ │ ├── date_picker.jpg │ │ ├── drillthrough1.jpg │ │ ├── drillthrough2.jpg │ │ ├── drillthrough3.jpg │ │ ├── drillthrough4.jpg │ │ ├── ds1.jpg │ │ ├── ds2.jpg │ │ ├── export_csv.jpg │ │ ├── export_to_pdf_button.jpg │ │ ├── export_to_pdf_dialog.jpg │ │ ├── favourite.jpg │ │ ├── fullscreen1.jpg │ │ ├── fullscreen2.jpg │ │ ├── group1.jpg │ │ ├── group2.jpg │ │ ├── login.jpg │ │ ├── login2.jpg │ │ ├── login3.jpg │ │ ├── more_options_button.jpg │ │ ├── report1.jpg │ │ ├── report10.jpg │ │ ├── report11.jpg │ │ ├── report12.jpg │ │ ├── report13.jpg │ │ ├── report14.jpg │ │ ├── report2.jpg │ │ ├── report3.jpg │ │ ├── report4.jpg │ │ ├── report4_1.jpg │ │ ├── report5.jpg │ │ ├── report6.jpg │ │ ├── report7.jpg │ │ ├── report8.jpg │ │ ├── report9.jpg │ │ ├── rls_table.jpg │ │ ├── rls_user_attributes.jpg │ │ ├── share_button.jpg │ │ ├── share_dialog.jpg │ │ ├── share_event.jpg │ │ ├── slicer.jpg │ │ ├── style1.jpg │ │ ├── style2.jpg │ │ ├── user1.jpg │ │ ├── user2.jpg │ │ ├── user3.jpg │ │ └── user4.jpg ├── _media │ ├── favicon.ico │ └── style.css ├── _sidebar.md ├── build.md ├── change-logs.md ├── configuration.md ├── data-source.md ├── index.html ├── installation.md ├── quick-start.md ├── report-component.md ├── report.md ├── upgrade.md └── user-management.md ├── export-server ├── .gitignore ├── package-lock.json ├── package.json └── poli-export-server.js ├── pom.xml ├── src ├── main │ ├── java │ │ └── com │ │ │ └── shzlw │ │ │ └── poli │ │ │ ├── App.java │ │ │ ├── AppStartUpListener.java │ │ │ ├── config │ │ │ ├── AppConfig.java │ │ │ └── AppProperties.java │ │ │ ├── dao │ │ │ ├── AuditLogDao.java │ │ │ ├── CannedReportDao.java │ │ │ ├── ComponentDao.java │ │ │ ├── DaoHelper.java │ │ │ ├── GroupDao.java │ │ │ ├── JdbcDataSourceDao.java │ │ │ ├── ReportDao.java │ │ │ ├── SavedQueryDao.java │ │ │ ├── SharedReportDao.java │ │ │ ├── UserDao.java │ │ │ └── UserFavouriteDao.java │ │ │ ├── dto │ │ │ ├── AppInfo.java │ │ │ ├── Column.java │ │ │ ├── ExportRequest.java │ │ │ ├── FilterParameter.java │ │ │ ├── LoginResponse.java │ │ │ ├── QueryRequest.java │ │ │ ├── QueryResult.java │ │ │ ├── SharedLinkInfo.java │ │ │ ├── SharedReportRow.java │ │ │ └── Table.java │ │ │ ├── filter │ │ │ ├── AuthFilter.java │ │ │ └── AuthFilterHelper.java │ │ │ ├── model │ │ │ ├── AuditLog.java │ │ │ ├── CannedReport.java │ │ │ ├── Component.java │ │ │ ├── Group.java │ │ │ ├── JdbcDataSource.java │ │ │ ├── Report.java │ │ │ ├── SavedQuery.java │ │ │ ├── SharedReport.java │ │ │ ├── User.java │ │ │ └── UserAttribute.java │ │ │ ├── rest │ │ │ ├── AuditLogWs.java │ │ │ ├── AuthWs.java │ │ │ ├── CannedReportWs.java │ │ │ ├── ComponentWs.java │ │ │ ├── GroupWs.java │ │ │ ├── InfoWs.java │ │ │ ├── JdbcDataSourceWs.java │ │ │ ├── JdbcQueryWs.java │ │ │ ├── ReportWs.java │ │ │ ├── SavedQueryWs.java │ │ │ ├── SharedReportWs.java │ │ │ ├── UserWs.java │ │ │ └── api │ │ │ │ └── SavedQueryEndpointWs.java │ │ │ ├── service │ │ │ ├── AuditLogService.java │ │ │ ├── HttpClient.java │ │ │ ├── JdbcDataSourceService.java │ │ │ ├── JdbcQueryService.java │ │ │ ├── JdbcQueryServiceHelper.java │ │ │ ├── ReportService.java │ │ │ ├── SharedReportService.java │ │ │ └── UserService.java │ │ │ └── util │ │ │ ├── CommonUtils.java │ │ │ ├── Constants.java │ │ │ ├── HttpUtils.java │ │ │ ├── PasswordUtils.java │ │ │ └── RawStringDeserialzier.java │ └── resources │ │ ├── application.properties │ │ ├── loader.properties │ │ └── logback-spring.xml └── test │ ├── java │ └── com │ │ └── shzlw │ │ └── poli │ │ ├── dao │ │ ├── AuditLogDaoTest.java │ │ └── DaoHelperTest.java │ │ ├── filter │ │ └── AuthFilterTest.java │ │ ├── rest │ │ ├── AbstractWsTest.java │ │ ├── AuthWsTest.java │ │ ├── CannedReportWsTest.java │ │ ├── ComponentWsTest.java │ │ ├── GroupWsTest.java │ │ ├── InfoWsTest.java │ │ ├── JdbcDataSourceWsTest.java │ │ ├── JdbcQueryWsTest.java │ │ ├── ReportWsTest.java │ │ ├── SavedQueryWsTest.java │ │ ├── SharedReportWsTest.java │ │ └── UserWsTest.java │ │ ├── service │ │ └── JdbcQueryServiceHelperTest.java │ │ └── util │ │ ├── CommonUtilsTest.java │ │ └── PasswordUtilsTest.java │ └── resources │ ├── application-test.properties │ ├── logback-test.xml │ └── schema-sqlite.sql ├── third-party-license ├── license-HikariCP ├── license-SQLite JDBC ├── license-axios ├── license-echarts ├── license-echarts-for-react ├── license-fontawesome ├── license-guava ├── license-okhttp ├── license-puppeteer ├── license-react ├── license-react-ace ├── license-react-beautiful-dnd ├── license-react-color ├── license-react-i18next ├── license-react-router ├── license-react-select ├── license-react-table ├── license-react-toastify └── license-spring boot ├── upgrade ├── poli_upgrade_v0.10.0.sql ├── poli_upgrade_v0.12.0.sql ├── poli_upgrade_v0.7.0.sql └── poli_upgrade_v0.9.0.sql ├── version └── web-app ├── .gitignore ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json └── src ├── App.css ├── App.js ├── App.test.js ├── api ├── ApiService.js ├── Constants.js ├── EchartsApi.js └── Util.js ├── components ├── Checkbox │ ├── Checkbox.css │ └── Checkbox.js ├── ColorPicker │ ├── ColorPicker.css │ └── ColorPicker.js ├── DropdownDialog │ ├── DropdownDialog.css │ └── DropdownDialog.js ├── GridDraggable.js ├── GridItem.js ├── GridLayout.css ├── GridLayout.js ├── GridResizable.js ├── Kanban │ ├── Kanban.css │ └── Kanban.js ├── Modal │ ├── Modal.css │ └── Modal.js ├── SearchInput │ ├── SearchInput.css │ └── SearchInput.js ├── Select.js ├── SelectButtons │ ├── SelectButtons.css │ └── SelectButtons.js ├── Tabs │ ├── Tabs.css │ └── Tabs.js ├── Toast │ ├── Toast.css │ └── Toast.js ├── filters │ ├── DatePicker.css │ ├── DatePicker.js │ ├── InputRange.css │ ├── InputRange.js │ ├── Paginator.css │ ├── Paginator.js │ ├── Slicer.css │ └── Slicer.js ├── processbar │ ├── Processbar.css │ └── Processbar.js ├── table │ ├── Table.css │ └── Table.js └── widgets │ ├── Card.css │ ├── Card.js │ ├── Iframe.js │ ├── ImageBox.css │ ├── ImageBox.js │ ├── InnerHtml.js │ ├── TextBox.css │ └── TextBox.js ├── i18n.js ├── index.css ├── index.js ├── locales ├── en │ └── translation.json ├── es │ └── translation.json ├── fr │ └── translation.json └── zh │ └── translation.json └── views ├── Account.js ├── DataSource.js ├── Event ├── AuditLog.js └── SharedReportView.js ├── Login ├── ChangeTempPassword.js ├── Login.css └── Login.js ├── PageNotFound.js ├── Report ├── ComponentEditPanel.css ├── ComponentEditPanel.js ├── ComponentViewPanel.css ├── ComponentViewPanel.js ├── Report.css ├── Report.js └── ReportEditView.js ├── ReportFullScreenView.js ├── Studio ├── ReactSelectHelper.js ├── SchemaPanel.css ├── SchemaPanel.js ├── ScrollTabPanel.css ├── ScrollTabPanel.js ├── Studio.css └── Studio.js ├── UserManagement ├── Group.js └── User.js ├── Workspace.css └── Workspace.js /.gitattributes: -------------------------------------------------------------------------------- 1 | * linguist-vendored 2 | *.java linguist-vendored=false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.iml 3 | target/ 4 | .settings/org.eclipse.jdt.core.prefs 5 | .classpath 6 | .project 7 | log/ 8 | jdbc-drivers/* 9 | 10 | # Ignore the files copied from web-app build 11 | src/main/resources/static 12 | 13 | release/ 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - openjdk8 4 | 5 | after_success: 6 | - bash <(curl -s https://codecov.io/bash) -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build 2 | FROM openjdk:8-jdk-alpine as builder 3 | 4 | WORKDIR /app/src 5 | COPY . . 6 | 7 | # Install node and npm 8 | RUN apk add --update nodejs nodejs-npm 9 | 10 | ENV MAVEN_VERSION 3.5.4 11 | ENV MAVEN_HOME /usr/lib/mvn 12 | ENV PATH $MAVEN_HOME/bin:$PATH 13 | 14 | # Install maven 15 | RUN wget http://archive.apache.org/dist/maven/maven-3/$MAVEN_VERSION/binaries/apache-maven-$MAVEN_VERSION-bin.tar.gz && \ 16 | tar -zxvf apache-maven-$MAVEN_VERSION-bin.tar.gz && \ 17 | rm apache-maven-$MAVEN_VERSION-bin.tar.gz && \ 18 | mv apache-maven-$MAVEN_VERSION /usr/lib/mvn 19 | 20 | # Build the jar 21 | RUN ./build.sh 22 | 23 | # Deploy 24 | FROM openjdk:8-jre-alpine 25 | 26 | WORKDIR /app 27 | 28 | COPY --from=builder /app/src/target/poli-0.12.2.jar /app/poli-0.12.2.jar 29 | COPY --from=builder /app/src/db/poli.db /app/db/poli.db 30 | COPY --from=builder /app/src/build_release/start.sh /app/start.sh 31 | COPY --from=builder /app/src/config/poli.docker.properties /app/config/poli.properties 32 | 33 | EXPOSE 6688 34 | 35 | RUN mkdir /app/jdbc-drivers 36 | RUN chmod +x /app/start.sh 37 | 38 | ENTRYPOINT ["/app/start.sh"] 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Zhonglu Wang 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 | # **Poli(魄力)** 2 | 3 | [![Version](https://img.shields.io/badge/Version-0.12.2-0065FF.svg)](#) 4 | [![license: MIT](https://img.shields.io/badge/license-MIT-FF5630.svg)](https://opensource.org/licenses/MIT) 5 | [![Download](https://img.shields.io/github/downloads/shzlw/poli/total.svg?color=6554C0)](https://github.com/shzlw/poli/releases) 6 | [![Docker Pulls](https://img.shields.io/docker/pulls/zhonglu/poli.svg)](https://cloud.docker.com/u/zhonglu/repository/docker/zhonglu/poli) 7 | [![Build Status](https://travis-ci.org/shzlw/poli.svg?branch=master)](https://travis-ci.org/shzlw/poli) 8 | [![codecov](https://codecov.io/gh/shzlw/poli/branch/master/graph/badge.svg)](https://codecov.io/gh/shzlw/poli) 9 | 10 | Poli is an easy-to-use SQL reporting application built for SQL lovers! 11 | 12 | ## Why Poli 13 | 14 | #### :zap: Self-hosted & easy setup 15 | Platform independent web application. Single JAR file + Single SQLite DB file. Get up and running in 5 minutes. 16 | #### :muscle: Connect to any database supporting JDBC drivers 17 | PostgreSQL, Oracle, SQL Server, MySQL, Elasticsearch... You name it. 18 | #### :bulb: SQL editor & schema viewer 19 | No ETLs, no generated SQL, polish your own SQL query to transform data. 20 | #### :fire: Rich and flexible styling 21 | Pixel-perfect positioning + Drag'n'Drop support to customize the reports and charts in your own way. 22 | #### :bookmark_tabs: Interactive Adhoc report 23 | Utilize the power of dynamic SQL with query variables to connect Filters and Charts. 24 | #### :hourglass: Canned report 25 | Capture the snapshot of historical data. Free up space in your own database. 26 | #### :santa: User management 27 | Three system level role configurations + Group based report access control. 28 | #### :earth_americas: Internationalization 29 | Custom the language pack and translations just for your audience. 30 | #### :moneybag: MIT license 31 | Open and free for all usages. 32 | #### :gem: Is that all? 33 | Auto refresh, drill through, fullscreen, embeds, color themes + more features in development. 34 | 35 | ## What's New ([latest](https://shzlw.github.io/poli/#/change-logs)) 36 | 37 | 38 | ![poli v0.8.0](http://66.228.42.235:8080/image-0.8.0/bob_glass_en.jpg) 39 | 40 | ## Gallery 41 | 42 | #### Slicer & Charts 43 | 44 | ![poli v0.5.0](http://66.228.42.235:8080/slicer.gif) 45 | 46 | #### Move & Resize 47 | 48 | ![poli component reposition](http://66.228.42.235:8080/move.gif) 49 | 50 | #### Color palette switch & export CSV 51 | 52 | ![poli v0.6.0](http://66.228.42.235:8080/v0.6.0_new.gif) 53 | 54 | ## Quick Installation 55 | 56 | Windows/Linux 57 | 58 | ```sh 59 | java -jar poli-0.12.2.jar 60 | ``` 61 | 62 | Docker 63 | 64 | ```sh 65 | docker run -d -p 6688:6688 --name poli zhonglu/poli:0.12.2 66 | ``` 67 | 68 | Check [installation guide](https://shzlw.github.io/poli/#/installation) for more details. 69 | 70 | ## Download 71 | 72 | [Download](https://github.com/shzlw/poli/releases) the latest version of Poli via the github release page. 73 | 74 | ## Documentation 75 | 76 | Poli's documentation and other information can be found at [here](https://shzlw.github.io/poli/). 77 | 78 | ## Run on GCP 79 | 80 | [![Run on Google Cloud](https://deploy.cloud.run/button.svg)](https://deploy.cloud.run) 81 | 82 | ## License 83 | 84 | MIT License 85 | 86 | Copyright (c) Zhonglu Wang -------------------------------------------------------------------------------- /build.bat: -------------------------------------------------------------------------------- 1 | rmdir /S /Q src\main\resources\static\static 2 | del /F /Q src\main\resources\static\favicon.ico 3 | del /F /Q src\main\resources\static\index.html 4 | cd web-app 5 | call npm install 6 | call npm run build 7 | cd .. 8 | xcopy web-app\build\static src\main\resources\static\static /s /i 9 | xcopy web-app\build\favicon.ico src\main\resources\static 10 | xcopy web-app\build\index.html src\main\resources\static 11 | call mvn clean package -DskipTests 12 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | rm -rf src/main/resources/static 2 | cd web-app 3 | npm install 4 | npm run build 5 | mkdir ../src/main/resources/static 6 | cp -r build/* ../src/main/resources/static/ 7 | 8 | cd .. 9 | mvn clean package -DskipTests 10 | -------------------------------------------------------------------------------- /build_dev.bat: -------------------------------------------------------------------------------- 1 | rmdir /S /Q src\main\resources\static\static 2 | del /F /Q src\main\resources\static\favicon.ico 3 | del /F /Q src\main\resources\static\index.html 4 | cd web-app 5 | call npm run build 6 | cd .. 7 | xcopy web-app\build\static src\main\resources\static\static /s /i 8 | xcopy web-app\build\favicon.ico src\main\resources\static 9 | xcopy web-app\build\index.html src\main\resources\static 10 | call mvn clean package -DskipTests 11 | -------------------------------------------------------------------------------- /build_dev.sh: -------------------------------------------------------------------------------- 1 | rm -rf src/main/resources/static/* 2 | cd web-app 3 | npm run build 4 | cp -r build/* ../src/main/resources/static/ 5 | cd .. 6 | mvn clean package -DskipTests 7 | -------------------------------------------------------------------------------- /build_release/build_release.sh: -------------------------------------------------------------------------------- 1 | VERSION=$(./version) 2 | if [ $# -eq 0 ]; then 3 | echo "No arguments specified. Use default version: $VERSION" 4 | else 5 | VERSION=$1 6 | echo "Version entered: $VERSION" 7 | fi 8 | 9 | release=poli-$VERSION 10 | mkdir $release 11 | 12 | rm -rf src/main/resources/static 13 | cd web-app 14 | npm install 15 | npm run build 16 | mkdir ../src/main/resources/static 17 | cp -r build/* ../src/main/resources/static/ 18 | cd .. 19 | mvn clean package -DskipTests 20 | 21 | cp -r export-server $release 22 | cp target/poli-*.jar $release 23 | cp -r docs $release 24 | cp LICENSE $release 25 | cp -r third-party-license $release 26 | cp build_release/start.sh $release 27 | cp build_release/start.bat $release 28 | chmod 755 $release/start.sh 29 | chmod 755 $release/start.bat 30 | cp README.md $release 31 | cp -r upgrade $release 32 | mkdir $release/jdbc-drivers 33 | mkdir $release/config 34 | cp config/poli.properties $release/config/poli.properties 35 | mkdir $release/db 36 | cd $release/db 37 | sqlite3 poli.db < ../../db/schema-sqlite.sql 38 | chmod 755 poli.db 39 | cd ../.. 40 | mv $release release 41 | 42 | cd release 43 | zip -r $release.zip $release/ 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /build_release/start.bat: -------------------------------------------------------------------------------- 1 | java -jar poli-0.12.2.jar --spring.config.name=application,poli -------------------------------------------------------------------------------- /build_release/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | java -jar poli-0.12.2.jar --spring.config.name=application,poli 5 | -------------------------------------------------------------------------------- /config/poli.docker.properties: -------------------------------------------------------------------------------- 1 | 2 | # Config the absolute path to poli.db 3 | # For example, jdbc:sqlite:/home/poli/poli.db or jdbc:sqlite:c:/poli/poli.db 4 | spring.datasource.url=jdbc:sqlite:/app/db/poli.db 5 | 6 | # The maximum number of data source pool size 7 | # poli.datasource-maximum-pool-size=50 8 | 9 | # The maximum number of records returned in the JDBC result set. Default value is -1 which returns unlimited rows. 10 | # poli.maximum-query-records=-1 11 | 12 | # The display language. 13 | # poli.locale-language=en 14 | 15 | # Allow to run multiple SQL query statements in the query editor. If there are multiple SQL statements, only the 16 | # last one will return query results. 17 | # poli.allow-multiple-query-statements=false 18 | 19 | # PDF export server URL. Check export-server/poli-export-server.js for more details. 20 | # poli.export-server-url=http://127.0.0.1:6689/pdf 21 | -------------------------------------------------------------------------------- /config/poli.properties: -------------------------------------------------------------------------------- 1 | 2 | # Config the absolute path to poli.db 3 | # For example, jdbc:sqlite:/home/poli/poli.db or jdbc:sqlite:c:/poli/poli.db 4 | # spring.datasource.url= 5 | 6 | # The maximum number of data source pool size 7 | # poli.datasource-maximum-pool-size=50 8 | 9 | # The maximum number of records returned in the JDBC result set. Default value is -1 which returns unlimited rows. 10 | # poli.maximum-query-records=-1 11 | 12 | # The display language. 13 | # poli.locale-language=en 14 | 15 | # Allow to run multiple SQL query statements in the query editor. If there are multiple SQL statements, only the 16 | # last one will return query results. 17 | # poli.allow-multiple-query-statements=false 18 | 19 | # PDF export server URL. Check export-server/poli-export-server.js for more details. 20 | # poli.export-server-url=http://127.0.0.1:6689/pdf -------------------------------------------------------------------------------- /db/poli.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/db/poli.db -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/.nojekyll -------------------------------------------------------------------------------- /docs/_coverpage.md: -------------------------------------------------------------------------------- 1 | #

Poli

2 | 3 | > An easy-to-use SQL reporting application built for SQL lovers 4 | 5 | [GitHub](https://github.com/shzlw/poli) 6 | [Get Started](#Poli) 7 | 8 | ![color](#f6f9fc) -------------------------------------------------------------------------------- /docs/_images/screenshots/apikey.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/apikey.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/apikey2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/apikey2.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/autorefresh.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/autorefresh.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/canned_report.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/canned_report.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/component_card.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/component_card.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/component_funnel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/component_funnel.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/component_heatmap.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/component_heatmap.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/component_treemap.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/component_treemap.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/date_picker.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/date_picker.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/drillthrough1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/drillthrough1.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/drillthrough2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/drillthrough2.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/drillthrough3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/drillthrough3.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/drillthrough4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/drillthrough4.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/ds1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/ds1.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/ds2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/ds2.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/export_csv.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/export_csv.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/export_to_pdf_button.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/export_to_pdf_button.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/export_to_pdf_dialog.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/export_to_pdf_dialog.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/favourite.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/favourite.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/fullscreen1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/fullscreen1.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/fullscreen2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/fullscreen2.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/group1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/group1.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/group2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/group2.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/login.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/login.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/login2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/login2.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/login3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/login3.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/more_options_button.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/more_options_button.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/report1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/report1.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/report10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/report10.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/report11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/report11.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/report12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/report12.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/report13.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/report13.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/report14.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/report14.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/report2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/report2.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/report3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/report3.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/report4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/report4.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/report4_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/report4_1.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/report5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/report5.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/report6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/report6.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/report7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/report7.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/report8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/report8.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/report9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/report9.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/rls_table.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/rls_table.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/rls_user_attributes.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/rls_user_attributes.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/share_button.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/share_button.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/share_dialog.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/share_dialog.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/share_event.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/share_event.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/slicer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/slicer.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/style1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/style1.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/style2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/style2.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/user1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/user1.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/user2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/user2.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/user3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/user3.jpg -------------------------------------------------------------------------------- /docs/_images/screenshots/user4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_images/screenshots/user4.jpg -------------------------------------------------------------------------------- /docs/_media/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/docs/_media/favicon.ico -------------------------------------------------------------------------------- /docs/_media/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --theme-color: #0065FF; 3 | --theme-color-light: #2684FF; 4 | --theme-color-dark: #0052CC; 5 | 6 | --text-color-base: #091E42; 7 | --text-color-secondary: #42526E; 8 | --text-color-tertiary: #7A869A; 9 | } 10 | 11 | body { 12 | font-family: Tahoma, Geneva, sans-serif; 13 | color: #172B4D; 14 | font-size: 16px; 15 | } 16 | 17 | * { 18 | text-decoration: none !important; 19 | } 20 | 21 | a { 22 | transition: all 0.2s linear; 23 | } 24 | 25 | .sidebar .app-name { 26 | font-weight: 700; 27 | } 28 | 29 | .sidebar li { 30 | margin: 5px 0px; 31 | } 32 | 33 | .sidebar li>p { 34 | font-size: 16px; 35 | } 36 | 37 | .sidebar ul li a { 38 | color: #42526E; 39 | font-size: 15px; 40 | } 41 | 42 | .markdown-section pre, 43 | .markdown-section pre > code { 44 | background-color: #253858; 45 | color: #EBECF0; 46 | font-size: 15px; 47 | } 48 | 49 | .markdown-section code { 50 | display: inline-block; 51 | } 52 | 53 | .markdown-section pre { 54 | padding: 6px 12px; 55 | } 56 | 57 | .markdown-section pre > code { 58 | padding: 0; 59 | } -------------------------------------------------------------------------------- /docs/_sidebar.md: -------------------------------------------------------------------------------- 1 | - Getting Started 2 | 3 | - [Installation](installation.md) 4 | - [Quick Start](quick-start.md) 5 | - [Configuration](configuration.md) 6 | - [Upgrade](upgrade.md) 7 | 8 | - Basic Concepts 9 | 10 | - [Data Source](data-source.md) 11 | - [Report](report.md) 12 | - [Report Component](report-component.md) 13 | - [User Management](user-management.md) 14 | 15 | - Development 16 | 17 | - [Build](build.md) 18 | 19 | - Release 20 | 21 | - [Change logs](change-logs.md) -------------------------------------------------------------------------------- /docs/build.md: -------------------------------------------------------------------------------- 1 | # Build 2 | 3 | ## Main tech stack 4 | 5 | * Spring Boot 2 6 | * React 16 7 | * SQLite 3 8 | 9 | ## Prerequisite 10 | 11 | * Node.js 12 | * Maven 13 | * SQLite 3 CLI 14 | * JDK 1.8+ 15 | 16 | ## Building 17 | 18 | 1. Clone or download the [repository](https://github.com/shzlw/poli). 19 | 20 | 2. Run the build script. 21 | 22 | ``` 23 | # Windows 24 | build.bat 25 | 26 | # Linux 27 | ./build.sh 28 | ``` 29 | 30 | 3. The java executable poli-x.y.z.jar is generated under the target folder. Run this to start the server. 31 | 32 | ``` 33 | java -jar target\poli-x.y.z.jar 34 | ``` 35 | 36 | ## Source code structure 37 | 38 | ``` 39 | |-- Repository 40 | |-- config 41 | |-- poli.properties 42 | |-- db 43 | |-- schema-sqlite.sql 44 | |-- src 45 | |-- web-app 46 | |-- pom.xml 47 | |-- build.sh 48 | |-- build.bat 49 | |... 50 | ``` 51 | 52 | * Src folder contains the java source code. 53 | * Web-app folder contains the react code. 54 | * Schema-sqlite.sql contains the database schema for SQLite 3. The poli.db file in the release is loaded from this schema. 55 | * The build scripts build the code in web-app first, copy the generated files to src ~/static folder and finally run maven to build the java project. 56 | 57 | -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | Application configurations can be adjusted in poli.properties. 4 | 5 | ## Data source path 6 | 7 | Absolute path to access poli.db in your local environment. 8 | 9 | ``` 10 | # Linux 11 | spring.datasource.url=jdbc:sqlite:/home/poli/poli.db 12 | 13 | # Windows 14 | spring.datasource.url=jdbc:sqlite:c:/poli/poli.db 15 | ``` 16 | 17 | ## Data source pool size 18 | 19 | The maximum number of data source pool size. 20 | 21 | ``` 22 | poli.datasource-maximum-pool-size=50 23 | ``` 24 | 25 | ## Number of query results 26 | 27 | The maximum number of records returned in the JDBC result set. Default value is -1 which returns unlimited rows 28 | 29 | ``` 30 | poli.maximum-query-records=1000 31 | ``` 32 | 33 | ## All multiple SQL statements 34 | 35 | Allow to run multiple SQL query statements in the query editor. If there are multiple SQL statements, only the last one will return query results. 36 | 37 | ``` 38 | poli.allow-multiple-query-statements=true 39 | ``` 40 | 41 | For example, when query against PostgreSQL, set the search_path in the first statement and query in the second statement. 42 | 43 | ```sql 44 | set search_path to MY_SCHEMA; 45 | select * from MY_TABLE; 46 | ``` 47 | 48 | ## Internationalization (i18n) 49 | 50 | Currently Poli supports four languages: 51 | 52 | * English 53 | * Chinese 54 | * Spanish 55 | * French 56 | 57 | The default language can be switched via changing the poli.properties 58 | 59 | ``` 60 | # English 61 | poli.locale-language=en 62 | 63 | # Chinese 64 | poli.locale-language=zh 65 | 66 | # Spanish 67 | poli.locale-language=es 68 | 69 | # French 70 | poli.locale-language=fr 71 | ``` 72 | 73 | ## Steps to add your own translations 74 | 75 | For instance, add Spanish language: es 76 | 77 | 1. Use poli/web-app/src/locales/en/translation.json as a base template. Copy the content and create a new file at poli/web-app/src/locales/es/translation.json. 78 | 79 | 2. Modify the translations in poli/web-app/src/locales/es/translation.json. 80 | 81 | 3. Modify poli/web-app/src/i18n.js 82 | 83 | ```javascript 84 | import translationES from './locales/es/translation'; 85 | 86 | const resources = { 87 | es: { 88 | translation: translationES 89 | } 90 | } 91 | ``` 92 | 93 | 4. Modify poli.properties 94 | 95 | ``` 96 | poli.locale-language=es 97 | ``` 98 | 99 | 5. Rebuild the project 100 | 101 | Check [Build](build) for more details. 102 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /docs/data-source.md: -------------------------------------------------------------------------------- 1 | # Data Source 2 | 3 | Poli connects to database through the JDBC interface. You can try connect to any databases that supports JDBC drivers. To configure Poli to connect to different databases, two steps are required. 4 | 5 | ## Download the JDBC jar file 6 | 7 | Download the JDBC jar files based on the database you'd like to connect to and put those JDBC jar files under /jdbc-drivers. 8 | 9 | For example: 10 | ```sh 11 | |-- jdbc-drivers 12 | |-- postgresql-42.2.5.jar 13 | |-- mysql-connector-java-8.0.12.jar 14 | |-- mssql-jdbc-7.2.0.jre8.jar 15 | |... 16 | ``` 17 | 18 | > There are no JDBC drivers included in the release except the JDBC driver for SQLite. 19 | 20 | ## Create a Data Source 21 | 22 | For instance, information needed to connect to a PostgreSQL database 23 | 24 | ``` 25 | Connection Url: jdbc:postgresql://127.0.0.1:5432/testdb 26 | Driver Class Name: org.postgresql.Driver 27 | Username: postgres 28 | Password: test 29 | Ping: SELECT 1 30 | ``` 31 | 32 | MySQL 33 | ``` 34 | Connection Url: jdbc:mysql://127.0.0.1:3306/testdb 35 | Driver Class Name: com.mysql.jdbc.Driver 36 | Ping: SELECT 1 37 | 38 | -- If use MySQL Connector/J 8.0 or higher 39 | Driver Class Name: com.mysql.cj.jdbc.Driver 40 | ``` 41 | 42 | SQL Server 43 | 44 | ``` 45 | Connection Url: jdbc:sqlserver://127.0.0.1:1433;databaseName=testdb 46 | Driver Class Name: com.microsoft.sqlserver.jdbc.SQLServerDriver 47 | Ping: SELECT 1 48 | ``` 49 | 50 | > Use the ping button to test the connection. -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Poli 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /docs/upgrade.md: -------------------------------------------------------------------------------- 1 | # Upgrade 2 | 3 | ## Windows/Linux 4 | 5 | 1. Download and unzip poli-x.y.z.zip. 6 | 2. Stop the Poli server. 7 | 3. Backup the old poli.db. (Simply copying and saving the poli.db as a new file should work) 8 | 4. Apply the upgrade sql if needed. 9 | ``` 10 | ./sqlite3 poli.db 11 | sqlite> .read poli_upgrade_x.y.z.sql 12 | ``` 13 | 5. Add the new configuration values if needed. 14 | 6. Replace the old poli-old-version.jar with the new poli-new-version.jar. 15 | 7. Start the Poli server. 16 | -------------------------------------------------------------------------------- /docs/user-management.md: -------------------------------------------------------------------------------- 1 | # User Management 2 | 3 | ## System role 4 | 5 | There are three types of system roles in Poli. Each user account requires to be assigned a system role when the account is created. 6 | 7 | #### Admin 8 | 9 | * Only one per instance. Cannot be created. 10 | * Manage reports 11 | * Manage datasources 12 | * Manage users 13 | 14 | #### Developer 15 | * Manage reports 16 | * Manage datasources 17 | * Manage users in Viewer Role 18 | 19 | #### Viewer 20 | * Access reports 21 | 22 | ## Group 23 | 24 | Group is used to control the access level to reports for Viewer user. Reports can be assigned to Group so the users in Viewer Role who belong to that group can have access. 25 | 26 | For example: 27 | 28 | ``` 29 | * User u1 belongs to Group g1 30 | * User u2 belongs to Group g2 31 | * Report r1 is assigned to Group g1 32 | * Report r1, r2 and r3 are assigned to Group g2 33 | * User u1 has access to Report r1 only 34 | * user u2 has access to Report r1, r2 and r3 35 | ``` -------------------------------------------------------------------------------- /export-server/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /export-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "poli-export-server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "puppeteer": "^3.0.4" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /export-server/poli-export-server.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const puppeteer = require('puppeteer'); 3 | 4 | const poli = 'http://localhost:6688/poli'; 5 | const hostname = '127.0.0.1'; 6 | const port = 6689; 7 | 8 | const server = http.createServer((request, response) => { 9 | const { 10 | url, 11 | method 12 | } = request; 13 | if (url === '/pdf' && method === 'POST') { 14 | pdfHandler(request).then(pdf => { 15 | response.setHeader('Content-Type', 'application/pdf'); 16 | response.setHeader('Content-Length', pdf.length); 17 | response.end(Buffer.from(pdf)); 18 | }).catch(error => { 19 | console.log(error); 20 | }) 21 | } else { 22 | response.end('Hello Poli!'); 23 | } 24 | }); 25 | 26 | async function pdfHandler(request) { 27 | const body = await getPostBody(request); 28 | const exportRequest = JSON.parse(body); 29 | const pdf = await generatePDF(exportRequest); 30 | return pdf; 31 | } 32 | 33 | function getPostBody(request) { 34 | return new Promise((resolve, reject) => { 35 | let body = ''; 36 | request.on('data', (chunk) => { 37 | body += chunk; 38 | if (body.length > 1e8) { 39 | request.connection.destroy(); 40 | } 41 | }); 42 | 43 | request.on('end', () => { 44 | resolve(body); 45 | }); 46 | 47 | request.on("error", (err) => { 48 | reject(err); 49 | }); 50 | }); 51 | } 52 | 53 | 54 | async function generatePDF(exportRequest) { 55 | const tomorrow = new Date(); 56 | tomorrow.setDate(tomorrow.getDate() + 1); 57 | 58 | const width = parseInt(exportRequest.width, 10); 59 | const height = parseInt(exportRequest.height, 10); 60 | 61 | const browser = await puppeteer.launch(); 62 | const page = await browser.newPage(); 63 | const cookie = { 64 | name: 'pskey', 65 | value: exportRequest.sessionKey, 66 | domain: 'localhost', 67 | path: '/', 68 | expires: Math.floor(tomorrow.getTime() / 1000) 69 | }; 70 | await page.setCookie(cookie); 71 | await page.setViewport({ width: width, height: height }); 72 | await page.goto(poli + '/workspace/report/fullscreen?$showControl=false&$toReport=' + exportRequest.reportName, {waitUntil: 'networkidle2'}); 73 | await page.waitFor(2000); 74 | const pdf = await page.pdf({ 75 | width: width, 76 | height: height, 77 | printBackground: true 78 | }); 79 | await browser.close(); 80 | return pdf; 81 | } 82 | 83 | server.listen(port, hostname, () => { 84 | console.log(`Server running at http://${hostname}:${port}/`); 85 | }); 86 | 87 | 88 | -------------------------------------------------------------------------------- /src/main/java/com/shzlw/poli/App.java: -------------------------------------------------------------------------------- 1 | package com.shzlw.poli; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class App { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(App.class, args); 11 | } 12 | } -------------------------------------------------------------------------------- /src/main/java/com/shzlw/poli/AppStartUpListener.java: -------------------------------------------------------------------------------- 1 | package com.shzlw.poli; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.boot.context.event.ApplicationReadyEvent; 6 | import org.springframework.context.event.EventListener; 7 | import org.springframework.stereotype.Component; 8 | 9 | @Component 10 | public class AppStartUpListener { 11 | 12 | private static final Logger LOGGER = LoggerFactory.getLogger(AppStartUpListener.class); 13 | 14 | @EventListener(ApplicationReadyEvent.class) 15 | public void applicationReadyEvent () { 16 | LOGGER.info("Welcome from Poli!"); 17 | } 18 | } -------------------------------------------------------------------------------- /src/main/java/com/shzlw/poli/config/AppConfig.java: -------------------------------------------------------------------------------- 1 | package com.shzlw.poli.config; 2 | 3 | import com.shzlw.poli.filter.AuthFilter; 4 | import okhttp3.OkHttpClient; 5 | import org.springframework.boot.web.servlet.FilterRegistrationBean; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; 9 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 10 | 11 | import java.util.concurrent.TimeUnit; 12 | 13 | @Configuration 14 | public class AppConfig implements WebMvcConfigurer { 15 | 16 | @Override 17 | public void addViewControllers(ViewControllerRegistry registry) { 18 | // \\w+ regex does not match the hyphen or underscore. Replace \\w for ^[a-zA-Z\d-_] 19 | registry.addViewController("/{spring:\\w+}") 20 | .setViewName("forward:/"); 21 | registry.addViewController("/**/{spring:\\w+}") 22 | .setViewName("forward:/"); 23 | registry.addViewController("/{spring:\\w+}/**{spring:?!(\\.js|\\.css)$}") 24 | .setViewName("forward:/"); 25 | } 26 | 27 | @Bean 28 | public FilterRegistrationBean authFilterRegistry() { 29 | FilterRegistrationBean registration = new FilterRegistrationBean(); 30 | registration.setName("authFilter"); 31 | registration.setFilter(new AuthFilter()); 32 | registration.addUrlPatterns("/*"); 33 | registration.setAsyncSupported(Boolean.TRUE); 34 | registration.setEnabled(Boolean.TRUE); 35 | return registration; 36 | } 37 | 38 | @Bean 39 | public OkHttpClient okHttpClient() { 40 | return new OkHttpClient.Builder() 41 | .connectTimeout(30, TimeUnit.SECONDS) 42 | .writeTimeout(30, TimeUnit.SECONDS) 43 | .readTimeout(30, TimeUnit.SECONDS) 44 | .build(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/shzlw/poli/config/AppProperties.java: -------------------------------------------------------------------------------- 1 | package com.shzlw.poli.config; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | import org.springframework.stereotype.Component; 5 | 6 | @Component 7 | @ConfigurationProperties(prefix = "poli") 8 | public class AppProperties { 9 | 10 | Integer datasourceMaximumPoolSize; 11 | 12 | Integer maximumQueryRecords; 13 | 14 | String localeLanguage; 15 | 16 | Boolean allowMultipleQueryStatements; 17 | 18 | String exportServerUrl; 19 | 20 | public AppProperties() { 21 | datasourceMaximumPoolSize = 50; 22 | maximumQueryRecords = 1000; 23 | localeLanguage = "en"; 24 | allowMultipleQueryStatements = false; 25 | } 26 | 27 | public Integer getDatasourceMaximumPoolSize() { 28 | return datasourceMaximumPoolSize; 29 | } 30 | 31 | public void setDatasourceMaximumPoolSize(Integer datasourceMaximumPoolSize) { 32 | this.datasourceMaximumPoolSize = datasourceMaximumPoolSize; 33 | } 34 | 35 | public Integer getMaximumQueryRecords() { 36 | return maximumQueryRecords; 37 | } 38 | 39 | public void setMaximumQueryRecords(Integer maximumQueryRecords) { 40 | this.maximumQueryRecords = maximumQueryRecords; 41 | } 42 | 43 | public String getLocaleLanguage() { 44 | return localeLanguage; 45 | } 46 | 47 | public void setLocaleLanguage(String localeLanguage) { 48 | this.localeLanguage = localeLanguage; 49 | } 50 | 51 | public Boolean getAllowMultipleQueryStatements() { 52 | return allowMultipleQueryStatements; 53 | } 54 | 55 | public void setAllowMultipleQueryStatements(Boolean allowMultipleQueryStatements) { 56 | this.allowMultipleQueryStatements = allowMultipleQueryStatements; 57 | } 58 | 59 | public String getExportServerUrl() { 60 | return exportServerUrl; 61 | } 62 | 63 | public void setExportServerUrl(String exportServerUrl) { 64 | this.exportServerUrl = exportServerUrl; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/shzlw/poli/dao/AuditLogDao.java: -------------------------------------------------------------------------------- 1 | package com.shzlw.poli.dao; 2 | 3 | import com.shzlw.poli.model.AuditLog; 4 | import com.shzlw.poli.util.CommonUtils; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.jdbc.core.JdbcTemplate; 7 | import org.springframework.jdbc.core.RowMapper; 8 | import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; 9 | import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; 10 | import org.springframework.jdbc.support.GeneratedKeyHolder; 11 | import org.springframework.jdbc.support.KeyHolder; 12 | import org.springframework.stereotype.Repository; 13 | 14 | import java.sql.ResultSet; 15 | import java.sql.SQLException; 16 | import java.util.List; 17 | 18 | @Repository 19 | public class AuditLogDao { 20 | 21 | @Autowired 22 | JdbcTemplate jt; 23 | 24 | @Autowired 25 | NamedParameterJdbcTemplate npjt; 26 | 27 | public List findAll(int page, int pageSize, String searchValue) { 28 | String sql = "SELECT id, created_at, type, data " 29 | + "FROM p_audit_log " 30 | + "WHERE data LIKE :data " 31 | + "OR type LIKE :type " 32 | + "ORDER BY created_at DESC LIMIT :limit OFFSET :offset"; 33 | 34 | MapSqlParameterSource params = new MapSqlParameterSource(); 35 | params.addValue("data", DaoHelper.getLikeParam(searchValue)); 36 | params.addValue("type", DaoHelper.getLikeParam(searchValue)); 37 | params.addValue("offset", DaoHelper.toOffset(page, pageSize)); 38 | params.addValue("limit", DaoHelper.toLimit(pageSize)); 39 | 40 | return npjt.query(sql, params, new AuditLogMapper()); 41 | } 42 | 43 | public long insert(long createdAt, String type, String data) { 44 | String sql = "INSERT INTO p_audit_log(created_at, type, data) " 45 | + "VALUES(:created_at, :type, :data)"; 46 | MapSqlParameterSource params = new MapSqlParameterSource(); 47 | params.addValue(AuditLog.CREATED_AT, createdAt); 48 | params.addValue(AuditLog.TYPE, type); 49 | params.addValue(AuditLog.DATA, data); 50 | 51 | KeyHolder keyHolder = new GeneratedKeyHolder(); 52 | npjt.update(sql, params, keyHolder, new String[] { AuditLog.ID }); 53 | return keyHolder.getKey().longValue(); 54 | } 55 | 56 | private static class AuditLogMapper implements RowMapper { 57 | @Override 58 | public AuditLog mapRow(ResultSet rs, int i) throws SQLException { 59 | AuditLog r = new AuditLog(); 60 | r.setId(rs.getLong(AuditLog.ID)); 61 | r.setType(rs.getString(AuditLog.TYPE)); 62 | long createdAt = rs.getLong(AuditLog.CREATED_AT); 63 | r.setCreatedAt(CommonUtils.toReadableDateTime(CommonUtils.fromEpoch(createdAt))); 64 | r.setData(rs.getString(AuditLog.DATA)); 65 | return r; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/shzlw/poli/dao/DaoHelper.java: -------------------------------------------------------------------------------- 1 | package com.shzlw.poli.dao; 2 | 3 | public final class DaoHelper { 4 | 5 | public static final int DEFAULT_LIMIT = 20; 6 | public static final int DEFAULT_OFFSET = 0; 7 | 8 | private DaoHelper() {} 9 | 10 | public static int toOffset(int page, int pageSize) { 11 | if (page < 1 || pageSize < 1) { 12 | return DEFAULT_OFFSET; 13 | } 14 | 15 | return (page - 1) * pageSize; 16 | } 17 | 18 | public static int toLimit(int pageSize) { 19 | return pageSize > 0 ? pageSize : DEFAULT_LIMIT; 20 | } 21 | 22 | public static String getLikeParam(String val) { 23 | return "%" + val + "%"; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/shzlw/poli/dao/UserFavouriteDao.java: -------------------------------------------------------------------------------- 1 | package com.shzlw.poli.dao; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.jdbc.core.JdbcTemplate; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | public class UserFavouriteDao { 9 | 10 | @Autowired 11 | JdbcTemplate jt; 12 | 13 | public boolean isFavourite(long userId, long reportId) { 14 | String sql = "SELECT COUNT(1) FROM p_user_favourite WHERE user_id=? AND report_id=?"; 15 | int count = jt.queryForObject(sql, new Object[]{ userId, reportId }, Integer.class); 16 | return count != 0; 17 | } 18 | 19 | public int insertFavourite(long userId, long reportId) { 20 | String sql = "INSERT INTO p_user_favourite(user_id, report_id) VALUES(?, ?)"; 21 | return jt.update(sql, new Object[]{ userId, reportId }); 22 | } 23 | 24 | public int deleteFavourite(long userId, long reportId) { 25 | String sql = "DELETE FROM p_user_favourite WHERE user_id=? AND report_id=?"; 26 | return jt.update(sql, new Object[]{ userId, reportId }); 27 | } 28 | 29 | public int deleteByUserId(long userId) { 30 | String sql = "DELETE FROM p_user_favourite WHERE user_id=?"; 31 | return jt.update(sql, new Object[]{ userId }); 32 | } 33 | 34 | public int deleteByReportId(long reportId) { 35 | String sql = "DELETE FROM p_user_favourite WHERE report_id=?"; 36 | return jt.update(sql, new Object[]{ reportId }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/shzlw/poli/dto/AppInfo.java: -------------------------------------------------------------------------------- 1 | package com.shzlw.poli.dto; 2 | 3 | public class AppInfo { 4 | 5 | private String version; 6 | private String localeLanguage; 7 | 8 | public AppInfo(String version, String localeLanguage) { 9 | this.version = version; 10 | this.localeLanguage = localeLanguage; 11 | } 12 | 13 | public String getVersion() { 14 | return version; 15 | } 16 | 17 | public String getLocaleLanguage() { 18 | return localeLanguage; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/shzlw/poli/dto/Column.java: -------------------------------------------------------------------------------- 1 | package com.shzlw.poli.dto; 2 | 3 | public class Column { 4 | 5 | private String name; 6 | private String javaType; 7 | private String dbType; 8 | private int length; 9 | 10 | public Column(String name, String javaType, String dbType, int length) { 11 | this.name = name; 12 | this.javaType = javaType; 13 | this.dbType = dbType; 14 | this.length = length; 15 | } 16 | 17 | public String getName() { 18 | return name; 19 | } 20 | 21 | public String getJavaType() { 22 | return javaType; 23 | } 24 | 25 | public String getDbType() { 26 | return dbType; 27 | } 28 | 29 | public int getLength() { 30 | return length; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/shzlw/poli/dto/ExportRequest.java: -------------------------------------------------------------------------------- 1 | package com.shzlw.poli.dto; 2 | 3 | public class ExportRequest { 4 | 5 | private long reportId; 6 | private String reportName; 7 | private String sessionKey; 8 | private int width; 9 | private int height; 10 | 11 | public ExportRequest(long reportId, String reportName, String sessionKey, int width, int height) { 12 | this.reportId = reportId; 13 | this.reportName = reportName; 14 | this.sessionKey = sessionKey; 15 | this.width = width; 16 | this.height = height; 17 | } 18 | 19 | public long getReportId() { 20 | return reportId; 21 | } 22 | 23 | public void setReportId(long reportId) { 24 | this.reportId = reportId; 25 | } 26 | 27 | public String getReportName() { 28 | return reportName; 29 | } 30 | 31 | public void setSessionKey(String sessionKey) { 32 | this.sessionKey = sessionKey; 33 | } 34 | 35 | public String getSessionKey() { 36 | return sessionKey; 37 | } 38 | 39 | public int getWidth() { 40 | return width; 41 | } 42 | 43 | public int getHeight() { 44 | return height; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/shzlw/poli/dto/FilterParameter.java: -------------------------------------------------------------------------------- 1 | package com.shzlw.poli.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonRawValue; 4 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 5 | import com.shzlw.poli.util.RawStringDeserialzier; 6 | 7 | public class FilterParameter { 8 | 9 | private String type; 10 | private String param; 11 | private String value; 12 | /** 13 | * If the type is: 'slicer' and remark is: 'select all', then no parameters should be applied to the query. 14 | */ 15 | private String remark; 16 | 17 | public String getType() { 18 | return type; 19 | } 20 | 21 | public void setType(String type) { 22 | this.type = type; 23 | } 24 | 25 | public String getParam() { 26 | return param; 27 | } 28 | 29 | public void setParam(String param) { 30 | this.param = param; 31 | } 32 | 33 | @JsonRawValue 34 | public String getValue() { 35 | return value; 36 | } 37 | 38 | @JsonDeserialize(using = RawStringDeserialzier.class) 39 | public void setValue(String value) { 40 | this.value = value; 41 | } 42 | 43 | public String getRemark() { 44 | return remark; 45 | } 46 | 47 | public void setRemark(String remark) { 48 | this.remark = remark; 49 | } 50 | 51 | @Override 52 | public String toString() { 53 | return "FilterParameter{" + 54 | "type='" + type + '\'' + 55 | ", param='" + param + '\'' + 56 | ", value='" + value + '\'' + 57 | '}'; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/shzlw/poli/dto/LoginResponse.java: -------------------------------------------------------------------------------- 1 | package com.shzlw.poli.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | public class LoginResponse { 6 | 7 | private String username; 8 | private String sysRole; 9 | private String error; 10 | 11 | @JsonProperty(value = "isTempPassword") 12 | private boolean isTempPassword; 13 | 14 | public static LoginResponse ofError(String error) { 15 | return new LoginResponse(error); 16 | } 17 | 18 | public static LoginResponse ofSuccess(String username, String sysRole, boolean isTempPassword) { 19 | return new LoginResponse(username, sysRole, isTempPassword); 20 | } 21 | 22 | private LoginResponse() {}; 23 | 24 | private LoginResponse(String error) { 25 | this.error = error; 26 | } 27 | 28 | private LoginResponse(String username, String sysRole, boolean isTempPassword) { 29 | this.username = username; 30 | this.sysRole = sysRole; 31 | this.isTempPassword = isTempPassword; 32 | } 33 | 34 | public String getUsername() { 35 | return username; 36 | } 37 | 38 | public String getSysRole() { 39 | return sysRole; 40 | } 41 | 42 | public String getError() { 43 | return error; 44 | } 45 | 46 | public boolean isTempPassword() { 47 | return isTempPassword; 48 | } 49 | 50 | @Override 51 | public String toString() { 52 | return "LoginResponse{" + 53 | "username='" + username + '\'' + 54 | ", sysRole='" + sysRole + '\'' + 55 | ", error='" + error + '\'' + 56 | ", isTempPassword=" + isTempPassword + 57 | '}'; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/shzlw/poli/dto/QueryRequest.java: -------------------------------------------------------------------------------- 1 | package com.shzlw.poli.dto; 2 | 3 | public class QueryRequest { 4 | 5 | private long jdbcDataSourceId; 6 | private String sqlQuery; 7 | private int resultLimit; 8 | 9 | public QueryRequest() {} 10 | 11 | public long getJdbcDataSourceId() { 12 | return jdbcDataSourceId; 13 | } 14 | 15 | public void setJdbcDataSourceId(long jdbcDataSourceId) { 16 | this.jdbcDataSourceId = jdbcDataSourceId; 17 | } 18 | 19 | public String getSqlQuery() { 20 | return sqlQuery; 21 | } 22 | 23 | public void setSqlQuery(String sqlQuery) { 24 | this.sqlQuery = sqlQuery; 25 | } 26 | 27 | public int getResultLimit() { 28 | return resultLimit; 29 | } 30 | 31 | public void setResultLimit(int resultLimit) { 32 | this.resultLimit = resultLimit; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/shzlw/poli/dto/QueryResult.java: -------------------------------------------------------------------------------- 1 | package com.shzlw.poli.dto; 2 | 3 | import com.shzlw.poli.util.Constants; 4 | import org.springframework.util.StringUtils; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | public class QueryResult { 10 | 11 | private List columns = new ArrayList<>(); 12 | 13 | /** 14 | * JSON array value 15 | */ 16 | private String data; 17 | private String error; 18 | 19 | public static QueryResult ofError(String error) { 20 | return new QueryResult(error); 21 | } 22 | 23 | public static QueryResult ofData(String data, List columns) { 24 | return new QueryResult(data, columns); 25 | } 26 | 27 | private QueryResult() { 28 | } 29 | 30 | private QueryResult(String error) { 31 | this.error = error; 32 | } 33 | 34 | private QueryResult(String data, List columns) { 35 | this.data = data; 36 | this.columns = columns; 37 | } 38 | 39 | public List getColumns() { 40 | return columns; 41 | } 42 | 43 | public String getData() { 44 | if (StringUtils.isEmpty(data)) { 45 | return Constants.EMPTY_JSON_ARRAY; 46 | } 47 | return data; 48 | } 49 | 50 | public String getError() { 51 | return error; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/shzlw/poli/dto/SharedLinkInfo.java: -------------------------------------------------------------------------------- 1 | package com.shzlw.poli.dto; 2 | 3 | import com.shzlw.poli.model.User; 4 | 5 | import java.util.Set; 6 | 7 | public class SharedLinkInfo { 8 | 9 | private User user; 10 | private long reportId; 11 | private Set componentQueryUrls; 12 | 13 | public SharedLinkInfo(User user, long reportId, Set componentQueryUrls) { 14 | this.user = user; 15 | this.reportId = reportId; 16 | this.componentQueryUrls = componentQueryUrls; 17 | } 18 | 19 | public User getUser() { 20 | return user; 21 | } 22 | 23 | public long getReportId() { 24 | return reportId; 25 | } 26 | 27 | public Set getComponentQueryUrls() { 28 | return componentQueryUrls; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/shzlw/poli/dto/SharedReportRow.java: -------------------------------------------------------------------------------- 1 | package com.shzlw.poli.dto; 2 | 3 | import com.shzlw.poli.model.SharedReport; 4 | 5 | public class SharedReportRow extends SharedReport { 6 | 7 | public static final String REPORT_NAME = "report_name"; 8 | public static final String CREATED_BY = "created_by"; 9 | 10 | private String reportName; 11 | private String createdBy; 12 | private String createDateTime; 13 | private String expirationDate; 14 | 15 | public SharedReportRow() {} 16 | 17 | public String getReportName() { 18 | return reportName; 19 | } 20 | 21 | public void setReportName(String reportName) { 22 | this.reportName = reportName; 23 | } 24 | 25 | public String getCreatedBy() { 26 | return createdBy; 27 | } 28 | 29 | public void setCreatedBy(String createdBy) { 30 | this.createdBy = createdBy; 31 | } 32 | 33 | public String getCreateDateTime() { 34 | return createDateTime; 35 | } 36 | 37 | public void setCreateDateTime(String createDateTime) { 38 | this.createDateTime = createDateTime; 39 | } 40 | 41 | public String getExpirationDate() { 42 | return expirationDate; 43 | } 44 | 45 | public void setExpirationDate(String expirationDate) { 46 | this.expirationDate = expirationDate; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/shzlw/poli/dto/Table.java: -------------------------------------------------------------------------------- 1 | package com.shzlw.poli.dto; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | public class Table { 7 | 8 | private String name; 9 | private String type; 10 | private List columns = new ArrayList<>(); 11 | 12 | public Table(String name, String type) { 13 | this.name = name; 14 | this.type = type; 15 | } 16 | 17 | public String getName() { 18 | return name; 19 | } 20 | 21 | public String getType() { 22 | return type; 23 | } 24 | 25 | public List getColumns() { 26 | return columns; 27 | } 28 | 29 | public void setColumns(List columns) { 30 | this.columns = columns; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/shzlw/poli/filter/AuthFilterHelper.java: -------------------------------------------------------------------------------- 1 | package com.shzlw.poli.filter; 2 | 3 | import com.shzlw.poli.util.Constants; 4 | 5 | import java.util.*; 6 | 7 | public final class AuthFilterHelper { 8 | 9 | private AuthFilterHelper() {} 10 | 11 | private static final List VIEWER_GET_PATH = Arrays.asList( 12 | "/ws/reports", 13 | "/ws/cannedreports", 14 | "/ws/components/report/", 15 | "/ws/users/account", 16 | "/ws/sharedreports/generate-sharekey" 17 | ); 18 | 19 | private static final List VIEWER_PUT_PATH = Arrays.asList( 20 | "/ws/users/account" 21 | ); 22 | 23 | private static final List VIEWER_POST_PATH = Arrays.asList( 24 | "/ws/jdbcquery", 25 | "/ws/cannedreports", 26 | "/ws/reports/favourite", 27 | "/ws/reports/pdf" 28 | ); 29 | 30 | private static final List VIEWER_DELETE_PATH = Arrays.asList( 31 | "/ws/cannedreports" 32 | ); 33 | 34 | private static final Map> VIEWER_MAP; 35 | static { 36 | Map> viewerTemp = new HashMap<>(); 37 | viewerTemp.put(Constants.HTTP_METHOD_GET, VIEWER_GET_PATH); 38 | viewerTemp.put(Constants.HTTP_METHOD_PUT, VIEWER_PUT_PATH); 39 | viewerTemp.put(Constants.HTTP_METHOD_POST, VIEWER_POST_PATH); 40 | viewerTemp.put(Constants.HTTP_METHOD_DELETE, VIEWER_DELETE_PATH); 41 | VIEWER_MAP = Collections.unmodifiableMap(viewerTemp); 42 | } 43 | 44 | private static final List APIKEY_GET_PATH = Arrays.asList( 45 | "/ws/reports", 46 | "/ws/cannedreports", 47 | "/ws/components/report/" 48 | ); 49 | private static final List APIKEY_POST_PATH = Arrays.asList( 50 | "/ws/jdbcquery/component" 51 | ); 52 | 53 | private static final Map> APIKEY_MAP; 54 | static { 55 | Map> apikeyTemp = new HashMap<>(); 56 | apikeyTemp.put(Constants.HTTP_METHOD_GET, APIKEY_GET_PATH); 57 | apikeyTemp.put(Constants.HTTP_METHOD_POST, APIKEY_POST_PATH); 58 | APIKEY_MAP = Collections.unmodifiableMap(apikeyTemp); 59 | } 60 | 61 | public static boolean validateViewer(String requestMethod, String path) { 62 | return isPathStartWith(path, VIEWER_MAP.get(requestMethod)); 63 | } 64 | 65 | public static boolean validateByApiKey(String requestMethod, String path) { 66 | return isPathStartWith(path, APIKEY_MAP.get(requestMethod)); 67 | } 68 | 69 | private static boolean isPathStartWith(String path, List startWithList) { 70 | if (path == null || startWithList == null) { 71 | return false; 72 | } 73 | 74 | for (String s : startWithList) { 75 | if (path.startsWith(s)) { 76 | return true; 77 | } 78 | } 79 | return false; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/com/shzlw/poli/model/AuditLog.java: -------------------------------------------------------------------------------- 1 | package com.shzlw.poli.model; 2 | 3 | public class AuditLog { 4 | 5 | public static final String ID = "id"; 6 | public static final String TYPE = "type"; 7 | public static final String CREATED_AT = "created_at"; 8 | public static final String DATA = "data"; 9 | 10 | private long id; 11 | private String createdAt; 12 | private String type; 13 | private String data; 14 | 15 | public AuditLog() {} 16 | 17 | public long getId() { 18 | return id; 19 | } 20 | 21 | public void setId(long id) { 22 | this.id = id; 23 | } 24 | 25 | public String getCreatedAt() { 26 | return createdAt; 27 | } 28 | 29 | public void setCreatedAt(String createdAt) { 30 | this.createdAt = createdAt; 31 | } 32 | 33 | public String getType() { 34 | return type; 35 | } 36 | 37 | public void setType(String type) { 38 | this.type = type; 39 | } 40 | 41 | public String getData() { 42 | return data; 43 | } 44 | 45 | public void setData(String data) { 46 | this.data = data; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/shzlw/poli/model/CannedReport.java: -------------------------------------------------------------------------------- 1 | package com.shzlw.poli.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonRawValue; 4 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 5 | import com.shzlw.poli.util.RawStringDeserialzier; 6 | 7 | public class CannedReport { 8 | 9 | public static final String ID = "id"; 10 | public static final String NAME = "name"; 11 | public static final String CREATED_AT = "created_at"; 12 | public static final String CREATED_BY = "created_by"; 13 | public static final String USER_ID = "user_id"; 14 | public static final String DATA = "data"; 15 | 16 | private long id; 17 | private String name; 18 | private String createdBy; 19 | private long createdAt; 20 | 21 | // Json column 22 | private String data; 23 | 24 | public CannedReport() {} 25 | 26 | public long getId() { 27 | return id; 28 | } 29 | 30 | public void setId(long id) { 31 | this.id = id; 32 | } 33 | 34 | public String getName() { 35 | return name; 36 | } 37 | 38 | public void setName(String name) { 39 | this.name = name; 40 | } 41 | 42 | public String getCreatedBy() { 43 | return createdBy; 44 | } 45 | 46 | public void setCreatedBy(String createdBy) { 47 | this.createdBy = createdBy; 48 | } 49 | 50 | public long getCreatedAt() { 51 | return createdAt; 52 | } 53 | 54 | public void setCreatedAt(long createdAt) { 55 | this.createdAt = createdAt; 56 | } 57 | 58 | @JsonRawValue 59 | public String getData() { 60 | return data; 61 | } 62 | 63 | @JsonDeserialize(using = RawStringDeserialzier.class) 64 | public void setData(String data) { 65 | this.data = data; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/shzlw/poli/model/Group.java: -------------------------------------------------------------------------------- 1 | package com.shzlw.poli.model; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | public class Group { 7 | 8 | public static final String ID = "id"; 9 | public static final String NAME = "name"; 10 | 11 | private long id; 12 | private String name; 13 | 14 | private List groupReports = new ArrayList<>(); 15 | 16 | public Group() {} 17 | 18 | public long getId() { 19 | return id; 20 | } 21 | 22 | public void setId(long id) { 23 | this.id = id; 24 | } 25 | 26 | public String getName() { 27 | return name; 28 | } 29 | 30 | public void setName(String name) { 31 | this.name = name; 32 | } 33 | 34 | public List getGroupReports() { 35 | return groupReports; 36 | } 37 | 38 | public void setGroupReports(List groupReports) { 39 | this.groupReports = groupReports; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/shzlw/poli/model/JdbcDataSource.java: -------------------------------------------------------------------------------- 1 | package com.shzlw.poli.model; 2 | 3 | import java.util.Objects; 4 | 5 | public class JdbcDataSource { 6 | 7 | public static final String ID = "id"; 8 | public static final String NAME = "name"; 9 | public static final String USERNAME = "username"; 10 | public static final String PASSWORD = "password"; 11 | public static final String CONNECTION_URL = "connection_url"; 12 | public static final String DRIVER_CLASS_NAME = "driver_class_name"; 13 | public static final String PING = "ping"; 14 | 15 | private long id; 16 | private String name; 17 | private String connectionUrl; 18 | private String driverClassName; 19 | private String username; 20 | private String password; 21 | private String ping; 22 | 23 | public JdbcDataSource() {} 24 | 25 | public long getId() { 26 | return id; 27 | } 28 | 29 | public void setId(long id) { 30 | this.id = id; 31 | } 32 | 33 | public String getName() { 34 | return name; 35 | } 36 | 37 | public void setName(String name) { 38 | this.name = name; 39 | } 40 | 41 | public String getConnectionUrl() { 42 | return connectionUrl; 43 | } 44 | 45 | public void setConnectionUrl(String connectionUrl) { 46 | this.connectionUrl = connectionUrl; 47 | } 48 | 49 | public String getDriverClassName() { 50 | return driverClassName; 51 | } 52 | 53 | public void setDriverClassName(String driverClassName) { 54 | this.driverClassName = driverClassName; 55 | } 56 | 57 | public String getUsername() { 58 | return username; 59 | } 60 | 61 | public void setUsername(String username) { 62 | this.username = username; 63 | } 64 | 65 | public String getPassword() { 66 | return password; 67 | } 68 | 69 | public void setPassword(String password) { 70 | this.password = password; 71 | } 72 | 73 | public String getPing() { 74 | return ping; 75 | } 76 | 77 | public void setPing(String ping) { 78 | this.ping = ping; 79 | } 80 | 81 | @Override 82 | public boolean equals(Object o) { 83 | if (this == o) return true; 84 | if (o == null || getClass() != o.getClass()) return false; 85 | JdbcDataSource that = (JdbcDataSource) o; 86 | return id == that.id && 87 | Objects.equals(name, that.name) && 88 | Objects.equals(connectionUrl, that.connectionUrl) && 89 | Objects.equals(driverClassName, that.driverClassName); 90 | } 91 | 92 | @Override 93 | public int hashCode() { 94 | return Objects.hash(id, name, connectionUrl, driverClassName); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/com/shzlw/poli/model/Report.java: -------------------------------------------------------------------------------- 1 | package com.shzlw.poli.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import com.fasterxml.jackson.annotation.JsonRawValue; 5 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 6 | import com.shzlw.poli.util.RawStringDeserialzier; 7 | 8 | import java.util.Objects; 9 | 10 | public class Report { 11 | 12 | public static final String TABLE = "p_report"; 13 | public static final String ID = "id"; 14 | public static final String NAME = "name"; 15 | public static final String STYLE = "style"; 16 | public static final String PROJECT = "project"; 17 | 18 | private long id; 19 | private String name; 20 | 21 | /** 22 | * Json field 23 | * 24 | * { 25 | * height: 600, 26 | * backgroundColor: '#123456' 27 | * } 28 | */ 29 | private String style; 30 | 31 | private String project; 32 | 33 | @JsonProperty(value = "isFavourite") 34 | private boolean isFavourite; 35 | 36 | public long getId() { 37 | return id; 38 | } 39 | 40 | public void setId(long id) { 41 | this.id = id; 42 | } 43 | 44 | public String getName() { 45 | return name; 46 | } 47 | 48 | public void setName(String name) { 49 | this.name = name; 50 | } 51 | 52 | public String getProject() { 53 | return project; 54 | } 55 | 56 | public void setProject(String project) { 57 | this.project = project; 58 | } 59 | 60 | public boolean isFavourite() { 61 | return isFavourite; 62 | } 63 | 64 | public void setFavourite(boolean favourite) { 65 | isFavourite = favourite; 66 | } 67 | 68 | @JsonRawValue 69 | public String getStyle() { 70 | return style; 71 | } 72 | 73 | @JsonDeserialize(using = RawStringDeserialzier.class) 74 | public void setStyle(String style) { 75 | this.style = style; 76 | } 77 | 78 | @Override 79 | public boolean equals(Object o) { 80 | if (this == o) return true; 81 | if (o == null || getClass() != o.getClass()) return false; 82 | Report report = (Report) o; 83 | return id == report.id && 84 | name.equals(report.name); 85 | } 86 | 87 | @Override 88 | public int hashCode() { 89 | return Objects.hash(id, name); 90 | } 91 | 92 | @Override 93 | public String toString() { 94 | return "Report{" + 95 | "id=" + id + 96 | ", name='" + name + '\'' + 97 | ", style='" + style + '\'' + 98 | '}'; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/com/shzlw/poli/model/SavedQuery.java: -------------------------------------------------------------------------------- 1 | package com.shzlw.poli.model; 2 | 3 | public class SavedQuery { 4 | 5 | public static final String ID = "id"; 6 | public static final String DATASOURCE_ID = "datasource_id"; 7 | public static final String NAME = "name"; 8 | public static final String SQL_QUERY = "sql_query"; 9 | public static final String ENDPOINT_NAME = "endpoint_name"; 10 | public static final String ENDPOINT_ACCESSCODE = "endpoint_accesscode"; 11 | 12 | private long id; 13 | private String name; 14 | private String sqlQuery; 15 | private long jdbcDataSourceId; 16 | 17 | // Http endpoint 18 | private String endpointName; 19 | private String endpointAccessCode; 20 | 21 | public SavedQuery() {} 22 | 23 | public long getId() { 24 | return id; 25 | } 26 | 27 | public void setId(long id) { 28 | this.id = id; 29 | } 30 | 31 | public String getName() { 32 | return name; 33 | } 34 | 35 | public void setName(String name) { 36 | this.name = name; 37 | } 38 | 39 | public String getSqlQuery() { 40 | return sqlQuery; 41 | } 42 | 43 | public void setSqlQuery(String sqlQuery) { 44 | this.sqlQuery = sqlQuery; 45 | } 46 | 47 | public long getJdbcDataSourceId() { 48 | return jdbcDataSourceId; 49 | } 50 | 51 | public void setJdbcDataSourceId(long jdbcDataSourceId) { 52 | this.jdbcDataSourceId = jdbcDataSourceId; 53 | } 54 | 55 | public String getEndpointName() { 56 | return endpointName; 57 | } 58 | 59 | public void setEndpointName(String endpointName) { 60 | this.endpointName = endpointName; 61 | } 62 | 63 | public String getEndpointAccessCode() { 64 | return endpointAccessCode; 65 | } 66 | 67 | public void setEndpointAccessCode(String endpointAccessCode) { 68 | this.endpointAccessCode = endpointAccessCode; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/shzlw/poli/model/SharedReport.java: -------------------------------------------------------------------------------- 1 | package com.shzlw.poli.model; 2 | 3 | public class SharedReport { 4 | 5 | public static final String ID = "id"; 6 | public static final String SHARE_KEY = "share_key"; 7 | public static final String REPORT_ID = "report_id"; 8 | public static final String REPORT_TYPE = "report_type"; 9 | public static final String USER_ID = "user_id"; 10 | public static final String CREATED_AT = "created_at"; 11 | public static final String EXPIRED_BY = "expired_by"; 12 | 13 | private long id; 14 | private String shareKey; 15 | private long reportId; 16 | private String reportType; 17 | private long userId; 18 | private long createdAt; 19 | private long expiredBy; 20 | 21 | public SharedReport() {} 22 | 23 | public long getId() { 24 | return id; 25 | } 26 | 27 | public void setId(long id) { 28 | this.id = id; 29 | } 30 | 31 | public String getShareKey() { 32 | return shareKey; 33 | } 34 | 35 | public void setShareKey(String shareKey) { 36 | this.shareKey = shareKey; 37 | } 38 | 39 | public long getReportId() { 40 | return reportId; 41 | } 42 | 43 | public void setReportId(long reportId) { 44 | this.reportId = reportId; 45 | } 46 | 47 | public String getReportType() { 48 | return reportType; 49 | } 50 | 51 | public void setReportType(String reportType) { 52 | this.reportType = reportType; 53 | } 54 | 55 | public long getUserId() { 56 | return userId; 57 | } 58 | 59 | public void setUserId(long userId) { 60 | this.userId = userId; 61 | } 62 | 63 | public long getCreatedAt() { 64 | return createdAt; 65 | } 66 | 67 | public void setCreatedAt(long createdAt) { 68 | this.createdAt = createdAt; 69 | } 70 | 71 | public long getExpiredBy() { 72 | return expiredBy; 73 | } 74 | 75 | public void setExpiredBy(long expiredBy) { 76 | this.expiredBy = expiredBy; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/com/shzlw/poli/model/UserAttribute.java: -------------------------------------------------------------------------------- 1 | package com.shzlw.poli.model; 2 | 3 | public class UserAttribute { 4 | 5 | public static final String ATTR_KEY = "attr_key"; 6 | public static final String ATTR_VALUE = "attr_value"; 7 | 8 | private String attrKey; 9 | private String attrValue; 10 | 11 | public UserAttribute() {} 12 | 13 | public String getAttrKey() { 14 | return attrKey; 15 | } 16 | 17 | public void setAttrKey(String attrKey) { 18 | this.attrKey = attrKey; 19 | } 20 | 21 | public String getAttrValue() { 22 | return attrValue; 23 | } 24 | 25 | public void setAttrValue(String attrValue) { 26 | this.attrValue = attrValue; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/shzlw/poli/rest/AuditLogWs.java: -------------------------------------------------------------------------------- 1 | package com.shzlw.poli.rest; 2 | 3 | import com.shzlw.poli.dao.AuditLogDao; 4 | import com.shzlw.poli.model.AuditLog; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.http.MediaType; 7 | import org.springframework.transaction.annotation.Transactional; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import org.springframework.web.bind.annotation.RequestMethod; 10 | import org.springframework.web.bind.annotation.RequestParam; 11 | import org.springframework.web.bind.annotation.RestController; 12 | 13 | import java.util.List; 14 | 15 | @RestController 16 | @RequestMapping("/ws/audit-logs") 17 | public class AuditLogWs { 18 | 19 | @Autowired 20 | AuditLogDao auditLogDao; 21 | 22 | @RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) 23 | @Transactional(readOnly = true) 24 | public List findAll(@RequestParam(name = "search", defaultValue = "", required = false) String searchValue, 25 | @RequestParam(name = "page", defaultValue = "1", required = false) int page, 26 | @RequestParam(name = "size", defaultValue = "20", required = false) int pageSize) { 27 | return auditLogDao.findAll(page, pageSize, searchValue); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/shzlw/poli/rest/CannedReportWs.java: -------------------------------------------------------------------------------- 1 | package com.shzlw.poli.rest; 2 | 3 | import com.shzlw.poli.dao.CannedReportDao; 4 | import com.shzlw.poli.model.CannedReport; 5 | import com.shzlw.poli.model.User; 6 | import com.shzlw.poli.util.CommonUtils; 7 | import com.shzlw.poli.util.Constants; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.http.HttpStatus; 10 | import org.springframework.http.MediaType; 11 | import org.springframework.http.ResponseEntity; 12 | import org.springframework.transaction.annotation.Transactional; 13 | import org.springframework.web.bind.annotation.*; 14 | 15 | import javax.servlet.http.HttpServletRequest; 16 | import java.time.LocalDateTime; 17 | import java.util.List; 18 | 19 | @RestController 20 | @RequestMapping("/ws/cannedreports") 21 | public class CannedReportWs { 22 | 23 | @Autowired 24 | CannedReportDao cannedReportDao; 25 | 26 | @RequestMapping( 27 | value = "/{id}", 28 | method = RequestMethod.GET, 29 | produces = MediaType.APPLICATION_JSON_VALUE) 30 | @Transactional(readOnly = true) 31 | public CannedReport one(@PathVariable("id") long id) { 32 | return cannedReportDao.findById(id); 33 | } 34 | 35 | @RequestMapping( 36 | method = RequestMethod.GET, 37 | produces = MediaType.APPLICATION_JSON_VALUE) 38 | @Transactional(readOnly = true) 39 | public List all() { 40 | return cannedReportDao.findAll(); 41 | } 42 | 43 | @RequestMapping( 44 | value = "/myreport", 45 | method = RequestMethod.GET, 46 | produces = MediaType.APPLICATION_JSON_VALUE) 47 | @Transactional(readOnly = true) 48 | public List getMyReport(HttpServletRequest request) { 49 | User user = (User) request.getAttribute(Constants.HTTP_REQUEST_ATTR_USER); 50 | return cannedReportDao.findByUserId(user.getId()); 51 | } 52 | 53 | @RequestMapping(method = RequestMethod.POST) 54 | @Transactional 55 | public ResponseEntity add(@RequestBody CannedReport cannedReport, 56 | HttpServletRequest request) { 57 | User user = (User) request.getAttribute(Constants.HTTP_REQUEST_ATTR_USER); 58 | long id = cannedReportDao.insert(user.getId(), 59 | CommonUtils.toEpoch(LocalDateTime.now()), 60 | cannedReport.getName(), 61 | cannedReport.getData()); 62 | return new ResponseEntity(id, HttpStatus.CREATED); 63 | } 64 | 65 | @RequestMapping(value = "/{id}", method = RequestMethod.DELETE) 66 | @Transactional 67 | public ResponseEntity delete(@PathVariable("id") long id) { 68 | cannedReportDao.delete(id); 69 | return new ResponseEntity(HttpStatus.NO_CONTENT); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/com/shzlw/poli/rest/InfoWs.java: -------------------------------------------------------------------------------- 1 | package com.shzlw.poli.rest; 2 | 3 | import com.shzlw.poli.config.AppProperties; 4 | import com.shzlw.poli.dto.AppInfo; 5 | import com.shzlw.poli.util.Constants; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | import org.springframework.web.bind.annotation.RequestMethod; 9 | import org.springframework.web.bind.annotation.RestController; 10 | 11 | @RestController 12 | @RequestMapping("/info") 13 | public class InfoWs { 14 | 15 | @Autowired 16 | AppProperties appProperties; 17 | 18 | @RequestMapping(value="/general", method = RequestMethod.GET) 19 | public AppInfo getGeneral() { 20 | return new AppInfo(Constants.CURRENT_VERSION, appProperties.getLocaleLanguage()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/shzlw/poli/rest/SavedQueryWs.java: -------------------------------------------------------------------------------- 1 | package com.shzlw.poli.rest; 2 | 3 | import com.shzlw.poli.dao.SavedQueryDao; 4 | import com.shzlw.poli.model.SavedQuery; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.http.HttpStatus; 7 | import org.springframework.http.MediaType; 8 | import org.springframework.http.ResponseEntity; 9 | import org.springframework.transaction.annotation.Transactional; 10 | import org.springframework.util.StringUtils; 11 | import org.springframework.web.bind.annotation.*; 12 | 13 | import java.util.List; 14 | 15 | @RestController 16 | @RequestMapping("/ws/saved-queries") 17 | public class SavedQueryWs { 18 | 19 | @Autowired 20 | SavedQueryDao savedQueryDao; 21 | 22 | @RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) 23 | @Transactional(readOnly = true) 24 | public List findAll() { 25 | return savedQueryDao.findAll(); 26 | } 27 | 28 | @RequestMapping(value = "/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) 29 | @Transactional(readOnly = true) 30 | public SavedQuery one(@PathVariable("id") long id) { 31 | return savedQueryDao.find(id); 32 | } 33 | 34 | @RequestMapping(method = RequestMethod.POST) 35 | @Transactional 36 | public ResponseEntity add(@RequestBody SavedQuery savedQuery) { 37 | if (StringUtils.isEmpty(savedQuery.getEndpointName())) { 38 | savedQuery.setEndpointName(null); 39 | } 40 | long id = savedQueryDao.insert(savedQuery); 41 | return new ResponseEntity<>(id, HttpStatus.CREATED); 42 | } 43 | 44 | @RequestMapping(method = RequestMethod.PUT) 45 | @Transactional 46 | public ResponseEntity update(@RequestBody SavedQuery savedQuery) { 47 | if (StringUtils.isEmpty(savedQuery.getEndpointName())) { 48 | savedQuery.setEndpointName(null); 49 | } 50 | savedQueryDao.update(savedQuery); 51 | return new ResponseEntity(HttpStatus.OK); 52 | } 53 | 54 | @RequestMapping(value = "/{id}", method = RequestMethod.DELETE) 55 | @Transactional 56 | public ResponseEntity delete(@PathVariable("id") long id) { 57 | savedQueryDao.delete(id); 58 | return new ResponseEntity<>(HttpStatus.NO_CONTENT); 59 | } 60 | 61 | public List findAllByPage(@RequestParam(name = "search", defaultValue = "", required = false) String searchValue, 62 | @RequestParam(name = "page", defaultValue = "1", required = false) int page, 63 | @RequestParam(name = "size", defaultValue = "20", required = false) int pageSize) { 64 | return savedQueryDao.findAllByPage(page, pageSize, searchValue); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/shzlw/poli/rest/SharedReportWs.java: -------------------------------------------------------------------------------- 1 | package com.shzlw.poli.rest; 2 | 3 | import com.shzlw.poli.dao.SharedReportDao; 4 | import com.shzlw.poli.dto.SharedReportRow; 5 | import com.shzlw.poli.model.SharedReport; 6 | import com.shzlw.poli.model.User; 7 | import com.shzlw.poli.service.SharedReportService; 8 | import com.shzlw.poli.util.CommonUtils; 9 | import com.shzlw.poli.util.Constants; 10 | import com.shzlw.poli.util.PasswordUtils; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.http.HttpStatus; 13 | import org.springframework.http.ResponseEntity; 14 | import org.springframework.transaction.annotation.Transactional; 15 | import org.springframework.web.bind.annotation.*; 16 | 17 | import javax.servlet.http.HttpServletRequest; 18 | import java.time.LocalDateTime; 19 | import java.util.List; 20 | 21 | @RestController 22 | @RequestMapping("/ws/sharedreports") 23 | public class SharedReportWs { 24 | 25 | @Autowired 26 | SharedReportDao sharedReportDao; 27 | 28 | @Autowired 29 | SharedReportService sharedReportService; 30 | 31 | @RequestMapping(value="/generate-sharekey", method = RequestMethod.POST) 32 | @Transactional 33 | public String generateSharedReportUrl(@RequestBody SharedReport sharedReport, 34 | HttpServletRequest request) { 35 | // Try reuse the same share key if it's the same report and has the same expiration date. 36 | List sharedReports = sharedReportDao.findByReportId(sharedReport.getReportId()); 37 | String newExpirationDate = CommonUtils.toReadableDateTime(CommonUtils.fromEpoch(sharedReport.getExpiredBy())); 38 | for (SharedReport sr : sharedReports) { 39 | String expirationDate = CommonUtils.toReadableDateTime(CommonUtils.fromEpoch(sr.getExpiredBy())); 40 | if (newExpirationDate.equals(expirationDate)) { 41 | return sr.getShareKey(); 42 | } 43 | } 44 | 45 | // Create a new shared report. 46 | User user = (User) request.getAttribute(Constants.HTTP_REQUEST_ATTR_USER); 47 | String shareKey = Constants.SHARE_KEY_PREFIX + PasswordUtils.getUniqueId(); 48 | long createdAt = CommonUtils.toEpoch(LocalDateTime.now()); 49 | sharedReportDao.insert(shareKey, sharedReport.getReportId(), sharedReport.getReportType(), 50 | user.getId(), createdAt, sharedReport.getExpiredBy()); 51 | return shareKey; 52 | } 53 | 54 | @RequestMapping(method = RequestMethod.GET) 55 | @Transactional(readOnly = true) 56 | public List findAllSharedReports() { 57 | return sharedReportDao.findAll(); 58 | } 59 | 60 | @RequestMapping(value = "/{id}", method = RequestMethod.DELETE) 61 | @Transactional 62 | public ResponseEntity delete(@PathVariable("id") long id) { 63 | SharedReport sharedReport = sharedReportDao.findById(id); 64 | sharedReportService.invalidateSharedLinkInfoCacheByShareKey(sharedReport.getShareKey()); 65 | sharedReportDao.delete(id); 66 | return new ResponseEntity(HttpStatus.NO_CONTENT); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/shzlw/poli/rest/api/SavedQueryEndpointWs.java: -------------------------------------------------------------------------------- 1 | package com.shzlw.poli.rest.api; 2 | 3 | import com.shzlw.poli.dao.SavedQueryDao; 4 | import com.shzlw.poli.dto.QueryResult; 5 | import com.shzlw.poli.model.SavedQuery; 6 | import com.shzlw.poli.service.AuditLogService; 7 | import com.shzlw.poli.service.JdbcDataSourceService; 8 | import com.shzlw.poli.service.JdbcQueryService; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.http.HttpStatus; 11 | import org.springframework.http.ResponseEntity; 12 | import org.springframework.transaction.annotation.Transactional; 13 | import org.springframework.util.StringUtils; 14 | import org.springframework.web.bind.annotation.RequestMapping; 15 | import org.springframework.web.bind.annotation.RequestMethod; 16 | import org.springframework.web.bind.annotation.RequestParam; 17 | import org.springframework.web.bind.annotation.RestController; 18 | 19 | import javax.servlet.http.HttpServletRequest; 20 | import javax.sql.DataSource; 21 | 22 | @RestController 23 | @RequestMapping("/api/v1") 24 | public class SavedQueryEndpointWs { 25 | 26 | @Autowired 27 | SavedQueryDao savedQueryDao; 28 | 29 | @Autowired 30 | JdbcDataSourceService jdbcDataSourceService; 31 | 32 | @Autowired 33 | JdbcQueryService jdbcQueryService; 34 | 35 | @Autowired 36 | AuditLogService auditLogService; 37 | 38 | @RequestMapping(value = "/saved-queries", method = RequestMethod.GET) 39 | @Transactional 40 | public ResponseEntity getSavedQuery(@RequestParam("name") String name, 41 | @RequestParam(name = "accessCode", required = false, defaultValue = "") String accessCode, 42 | @RequestParam(name = "contentType", required = false, defaultValue = "") String contentType, 43 | HttpServletRequest request) { 44 | 45 | auditLogService.logQueryEndpointAccess(request, name); 46 | 47 | SavedQuery savedQuery = savedQueryDao.findByEndpointName(name); 48 | if (savedQuery == null) { 49 | return new ResponseEntity<>(null, HttpStatus.NOT_FOUND); 50 | } 51 | 52 | // Validate access code 53 | String endpointAccessCode = savedQuery.getEndpointAccessCode(); 54 | if (!StringUtils.isEmpty(endpointAccessCode) && !endpointAccessCode.equals(accessCode)) { 55 | return new ResponseEntity<>(null, HttpStatus.UNAUTHORIZED); 56 | } 57 | 58 | DataSource dataSource = jdbcDataSourceService.getDataSource(savedQuery.getJdbcDataSourceId()); 59 | if (dataSource == null) { 60 | return new ResponseEntity<>(null, HttpStatus.NOT_FOUND); 61 | } 62 | 63 | QueryResult queryResult = jdbcQueryService.executeQuery(dataSource, savedQuery.getSqlQuery(), contentType); 64 | if (queryResult.getError() != null) { 65 | return new ResponseEntity<>(queryResult.getError(), HttpStatus.BAD_REQUEST); 66 | } else { 67 | return new ResponseEntity<>(queryResult.getData(), HttpStatus.OK); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/shzlw/poli/service/AuditLogService.java: -------------------------------------------------------------------------------- 1 | package com.shzlw.poli.service; 2 | 3 | import com.shzlw.poli.dao.AuditLogDao; 4 | import com.shzlw.poli.util.CommonUtils; 5 | import com.shzlw.poli.util.HttpUtils; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.stereotype.Service; 8 | 9 | import javax.servlet.http.HttpServletRequest; 10 | import java.time.LocalDateTime; 11 | 12 | @Service 13 | public class AuditLogService { 14 | 15 | @Autowired 16 | AuditLogDao auditLogDao; 17 | 18 | public void logQueryEndpointAccess(HttpServletRequest request, String name) { 19 | long now = CommonUtils.toEpoch(LocalDateTime.now()); 20 | String ip = HttpUtils.getIpAddress(request); 21 | String ipAddress = ip == null ? "" : ip; 22 | String type = "query endpoint"; 23 | String logData = String.format("%s accesses query endpoint: %s", ipAddress, name); 24 | auditLogDao.insert(now, type, logData); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/shzlw/poli/service/HttpClient.java: -------------------------------------------------------------------------------- 1 | package com.shzlw.poli.service; 2 | 3 | import okhttp3.*; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.stereotype.Service; 6 | 7 | import java.io.IOException; 8 | 9 | @Service 10 | public class HttpClient { 11 | 12 | private static final MediaType MEDIA_TYPE_PLAINTEXT = MediaType.parse("text/plain; charset=utf-8"); 13 | private static final MediaType MEDIA_TYPE_JSON = MediaType.parse("application/json; charset=utf-8"); 14 | 15 | @Autowired 16 | OkHttpClient okHttpClient; 17 | 18 | public byte[] get(String url) throws IOException { 19 | Request request = new Request.Builder() 20 | .url(url) 21 | .build(); 22 | try (Response response = okHttpClient.newCall(request).execute()) { 23 | return response.body().bytes(); 24 | } 25 | } 26 | 27 | public byte[] postJson(String url, String data) throws IOException { 28 | RequestBody body = RequestBody.create(data, MEDIA_TYPE_JSON); 29 | Request request = new Request.Builder() 30 | .url(url) 31 | .post(body) 32 | .build(); 33 | try (Response response = okHttpClient.newCall(request).execute()) { 34 | return response.body().bytes(); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/shzlw/poli/service/JdbcDataSourceService.java: -------------------------------------------------------------------------------- 1 | package com.shzlw.poli.service; 2 | 3 | import com.google.common.cache.Cache; 4 | import com.google.common.cache.CacheBuilder; 5 | import com.google.common.cache.CacheLoader; 6 | import com.google.common.cache.RemovalListener; 7 | import com.shzlw.poli.config.AppProperties; 8 | import com.shzlw.poli.dao.JdbcDataSourceDao; 9 | import com.shzlw.poli.model.JdbcDataSource; 10 | import com.zaxxer.hikari.HikariDataSource; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.stereotype.Service; 13 | import org.springframework.util.StringUtils; 14 | 15 | import javax.annotation.PostConstruct; 16 | import javax.annotation.PreDestroy; 17 | import javax.sql.DataSource; 18 | import java.util.concurrent.ExecutionException; 19 | import java.util.concurrent.TimeUnit; 20 | 21 | @Service 22 | public class JdbcDataSourceService { 23 | 24 | // 10 seconds 25 | private static final int LEAK_DETECTION_THRESHOLD = 10000; 26 | 27 | /** 28 | * Key: JdbcDataSource id 29 | * Value: HikariDataSource 30 | */ 31 | private static final Cache DATA_SOURCE_CACHE = CacheBuilder.newBuilder() 32 | .expireAfterWrite(5, TimeUnit.MINUTES) 33 | .removalListener((RemovalListener) removal -> { 34 | HikariDataSource ds = removal.getValue(); 35 | ds.close(); 36 | }) 37 | .build(); 38 | 39 | @Autowired 40 | JdbcDataSourceDao jdbcDataSourceDao; 41 | 42 | @Autowired 43 | AppProperties appProperties; 44 | 45 | @PostConstruct 46 | public void init() { 47 | } 48 | 49 | @PreDestroy 50 | public void shutdown() { 51 | for (HikariDataSource hiDs : DATA_SOURCE_CACHE.asMap().values()) { 52 | hiDs.close(); 53 | } 54 | } 55 | 56 | public void removeFromCache(long dataSourceId) { 57 | DATA_SOURCE_CACHE.invalidate(dataSourceId); 58 | } 59 | 60 | public DataSource getDataSource(long dataSourceId) { 61 | if (dataSourceId == 0) { 62 | return null; 63 | } 64 | 65 | try { 66 | DataSource hiDs = DATA_SOURCE_CACHE.get(dataSourceId, () -> { 67 | JdbcDataSource dataSource = jdbcDataSourceDao.findById(dataSourceId); 68 | if (dataSource == null) { 69 | return null; 70 | } 71 | HikariDataSource newHiDs = new HikariDataSource(); 72 | newHiDs.setJdbcUrl(dataSource.getConnectionUrl()); 73 | newHiDs.setUsername(dataSource.getUsername()); 74 | newHiDs.setPassword(dataSource.getPassword()); 75 | if (!StringUtils.isEmpty(dataSource.getDriverClassName())) { 76 | newHiDs.setDriverClassName(dataSource.getDriverClassName()); 77 | } 78 | newHiDs.setMaximumPoolSize(appProperties.getDatasourceMaximumPoolSize()); 79 | newHiDs.setLeakDetectionThreshold(LEAK_DETECTION_THRESHOLD); 80 | return newHiDs; 81 | }); 82 | return hiDs; 83 | } catch (ExecutionException | CacheLoader.InvalidCacheLoadException e) { 84 | return null; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/com/shzlw/poli/service/ReportService.java: -------------------------------------------------------------------------------- 1 | package com.shzlw.poli.service; 2 | 3 | import com.google.common.cache.Cache; 4 | import com.google.common.cache.CacheBuilder; 5 | import com.google.common.cache.CacheLoader; 6 | import com.shzlw.poli.dao.ReportDao; 7 | import com.shzlw.poli.model.Report; 8 | import com.shzlw.poli.model.User; 9 | import com.shzlw.poli.util.Constants; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.stereotype.Service; 14 | import org.springframework.util.StringUtils; 15 | 16 | import java.util.ArrayList; 17 | import java.util.Collections; 18 | import java.util.List; 19 | import java.util.concurrent.ExecutionException; 20 | import java.util.concurrent.TimeUnit; 21 | 22 | @Service 23 | public class ReportService { 24 | 25 | private static final Logger LOGGER = LoggerFactory.getLogger(ReportService.class); 26 | 27 | /** 28 | * Key: User id 29 | * Value: Report 30 | */ 31 | private static final Cache> USER_REPORT_CACHE = CacheBuilder.newBuilder() 32 | .expireAfterWrite(5, TimeUnit.MINUTES) 33 | .build(); 34 | 35 | @Autowired 36 | ReportDao reportDao; 37 | 38 | public List getReportsByUser(User user) { 39 | if (StringUtils.isEmpty(user)) { 40 | return Collections.emptyList(); 41 | } 42 | 43 | try { 44 | List rt = USER_REPORT_CACHE.get(user.getId(), () -> { 45 | List reports = new ArrayList<>(); 46 | if (Constants.SYS_ROLE_VIEWER.equals(user.getSysRole())) { 47 | reports = reportDao.findByViewer(user.getId()); 48 | } else { 49 | reports = reportDao.findAll(); 50 | } 51 | 52 | return reports; 53 | }); 54 | return rt; 55 | } catch (ExecutionException | CacheLoader.InvalidCacheLoadException e) { 56 | return Collections.emptyList(); 57 | } 58 | } 59 | 60 | public void invalidateCache(long userId) { 61 | USER_REPORT_CACHE.invalidate(userId); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/shzlw/poli/service/UserService.java: -------------------------------------------------------------------------------- 1 | package com.shzlw.poli.service; 2 | 3 | import com.google.common.cache.Cache; 4 | import com.google.common.cache.CacheBuilder; 5 | import com.google.common.cache.CacheLoader; 6 | import com.shzlw.poli.dao.UserDao; 7 | import com.shzlw.poli.model.User; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.stereotype.Service; 12 | import org.springframework.util.StringUtils; 13 | 14 | import java.util.concurrent.ExecutionException; 15 | import java.util.concurrent.TimeUnit; 16 | 17 | @Service 18 | public class UserService { 19 | 20 | private static final Logger LOGGER = LoggerFactory.getLogger(UserService.class); 21 | /** 22 | * Key: Session key 23 | * Value: User 24 | */ 25 | private static final Cache SESSION_USER_CACHE = CacheBuilder.newBuilder() 26 | .expireAfterWrite(5, TimeUnit.MINUTES) 27 | .build(); 28 | 29 | /** 30 | * Key: Api key 31 | * Value: User 32 | */ 33 | private static final Cache API_KEY_USER_CACHE = CacheBuilder.newBuilder() 34 | .expireAfterWrite(5, TimeUnit.MINUTES) 35 | .build(); 36 | 37 | @Autowired 38 | UserDao userDao; 39 | 40 | public User getUserBySessionKey(String sessionKey) { 41 | if (StringUtils.isEmpty(sessionKey)) { 42 | return null; 43 | } 44 | 45 | try { 46 | User user = SESSION_USER_CACHE.get(sessionKey, () -> { 47 | User u = userDao.findBySessionKey(sessionKey); 48 | u.setUserAttributes(userDao.findUserAttributes(u.getId())); 49 | return u; 50 | }); 51 | return user; 52 | } catch (ExecutionException | CacheLoader.InvalidCacheLoadException e) { 53 | return null; 54 | } 55 | } 56 | 57 | public User getUserByApiKey(String apiKey) { 58 | if (StringUtils.isEmpty(apiKey)) { 59 | return null; 60 | } 61 | 62 | try { 63 | User user = API_KEY_USER_CACHE.get(apiKey, () -> { 64 | User u = userDao.findByApiKey(apiKey); 65 | u.setUserAttributes(userDao.findUserAttributes(u.getId())); 66 | return u; 67 | }); 68 | return user; 69 | } catch (ExecutionException | CacheLoader.InvalidCacheLoadException e) { 70 | return null; 71 | } 72 | } 73 | 74 | public void newOrUpdateUser(User user, String oldSessionKey, String newSessionKey) { 75 | invalidateSessionUserCache(oldSessionKey); 76 | SESSION_USER_CACHE.put(newSessionKey, user); 77 | } 78 | 79 | public void invalidateSessionUserCache(String sessionKey) { 80 | if (sessionKey != null) { 81 | SESSION_USER_CACHE.invalidate(sessionKey); 82 | } 83 | } 84 | 85 | public void invalidateApiKeyUserCache(String apiKey) { 86 | if (apiKey != null) { 87 | API_KEY_USER_CACHE.invalidate(apiKey); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/com/shzlw/poli/util/CommonUtils.java: -------------------------------------------------------------------------------- 1 | package com.shzlw.poli.util; 2 | 3 | import org.springframework.lang.Nullable; 4 | 5 | import java.time.Instant; 6 | import java.time.LocalDateTime; 7 | import java.time.ZoneId; 8 | import java.time.format.DateTimeFormatter; 9 | 10 | public final class CommonUtils { 11 | 12 | private CommonUtils() {} 13 | 14 | public static LocalDateTime fromEpoch(long epoch) { 15 | return Instant.ofEpochMilli(epoch).atZone(ZoneId.systemDefault()).toLocalDateTime(); 16 | } 17 | 18 | public static long toEpoch(@Nullable LocalDateTime dateTime) { 19 | return dateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); 20 | } 21 | 22 | public static String toReadableDateTime(LocalDateTime localDateTime) { 23 | DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); 24 | return localDateTime.format(formatter); 25 | } 26 | 27 | public static String toReadableDate(LocalDateTime localDateTime) { 28 | DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); 29 | return localDateTime.format(formatter); 30 | } 31 | 32 | public static String getSimpleError(Exception e) { 33 | return "ERROR: " + e.getClass().getCanonicalName() + ": " + e.getMessage(); 34 | } 35 | 36 | public static String getParamByAttrKey(String attrKey) { 37 | return "$user_attr[" + attrKey + "]"; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/shzlw/poli/util/Constants.java: -------------------------------------------------------------------------------- 1 | package com.shzlw.poli.util; 2 | 3 | public final class Constants { 4 | 5 | private Constants() {} 6 | 7 | public static final String CURRENT_VERSION = "0.12.2"; 8 | 9 | public static final String SUCCESS = "success"; 10 | public static final String GOOD = ""; 11 | public static final String ERROR = ""; 12 | 13 | public static final String SESSION_KEY = "pskey"; 14 | public static final int COOKIE_TIMEOUT = 8640000; 15 | 16 | public static final String API_KEY_PREFIX = "apk_"; 17 | public static final String SESSION_KEY_PREFIX = "sk_"; 18 | public static final String SHARE_KEY_PREFIX = "sha_"; 19 | 20 | public static final String PASSWORD_SALT = "awesome"; 21 | 22 | public static final String ENCRYPT_KEY = "1234salt1234salt"; 23 | public static final String ENCRYPT_IV = "5678salt5678salt"; 24 | 25 | public static final String FILTER_TYPE_SLICER = "slicer"; 26 | public static final String FILTER_TYPE_SINGLE = "single"; 27 | public static final String FILTER_TYPE_DATE_PICKER = "date picker"; 28 | public static final String FILTER_TYPE_USER_ATTRIBUTE = "user attribute"; 29 | 30 | public static final String SLICER_SELECT_ALL = "select all"; 31 | public static final String SLICER_SELECT_NONE = "select none"; 32 | 33 | public static final String SYS_ROLE_VIEWER = "viewer"; 34 | public static final String SYS_ROLE_DEVELOPER = "developer"; 35 | public static final String SYS_ROLE_ADMIN = "admin"; 36 | 37 | public static final String COMPONENT_TYPE_FILTER = "filter"; 38 | public static final String COMPONENT_TYPE_CHART = "chart"; 39 | public static final String COMPONENT_TYPE_STATIC = "static"; 40 | 41 | public static final String EMPTY_JSON_ARRAY = "[]"; 42 | public static final String HTTP_REQUEST_ATTR_USER = "attr_user"; 43 | 44 | public static final String HTTP_METHOD_GET = "GET"; 45 | public static final String HTTP_METHOD_POST = "POST"; 46 | public static final String HTTP_METHOD_PUT = "PUT"; 47 | public static final String HTTP_METHOD_DELETE = "DELETE"; 48 | 49 | public static final String HTTP_HEADER_API_KEY = "Poli-Api-Key"; 50 | public static final String HTTP_HEADER_SHARE_KEY = "Poli-Share-Key"; 51 | 52 | public static final String ERROR_NO_DATA_SOURCE_FOUND = "No data source found"; 53 | public static final String ERROR_EMPTY_SQL_QUERY = "SQL query cannot be empty"; 54 | 55 | public static final int QUERY_RESULT_NOLIMIT = 0; 56 | public static final int MAXIMUM_QUERY_RECORDS_NOLIMIT = -1; 57 | 58 | public static final String CONTENT_TYPE_JSON = "json"; 59 | public static final String CONTENT_TYPE_CSV = "csv"; 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/shzlw/poli/util/HttpUtils.java: -------------------------------------------------------------------------------- 1 | package com.shzlw.poli.util; 2 | 3 | import javax.servlet.http.Cookie; 4 | import javax.servlet.http.HttpServletRequest; 5 | 6 | public final class HttpUtils { 7 | 8 | private HttpUtils() {} 9 | 10 | public static String getSessionKey(HttpServletRequest httpRequest) { 11 | Cookie[] cookies = httpRequest.getCookies(); 12 | if (cookies == null || cookies.length == 0) { 13 | return null; 14 | } 15 | 16 | for (int i = 0; i < cookies.length; i++) { 17 | String name = cookies[i].getName(); 18 | String value = cookies[i].getValue(); 19 | if (Constants.SESSION_KEY.equals(name)) { 20 | return value; 21 | } 22 | } 23 | return null; 24 | } 25 | 26 | public static String getIpAddress(HttpServletRequest httpRequest) { 27 | String ipAddress = httpRequest.getHeader("X-FORWARDED-FOR"); 28 | if (ipAddress == null) { 29 | ipAddress = httpRequest.getRemoteAddr(); 30 | } 31 | return ipAddress; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/shzlw/poli/util/RawStringDeserialzier.java: -------------------------------------------------------------------------------- 1 | package com.shzlw.poli.util; 2 | 3 | import com.fasterxml.jackson.core.JsonParser; 4 | import com.fasterxml.jackson.core.TreeNode; 5 | import com.fasterxml.jackson.databind.DeserializationContext; 6 | import com.fasterxml.jackson.databind.JsonDeserializer; 7 | 8 | import java.io.IOException; 9 | 10 | public class RawStringDeserialzier extends JsonDeserializer { 11 | 12 | @Override 13 | public String deserialize(JsonParser parser, DeserializationContext ctx) throws IOException { 14 | TreeNode tree = parser.getCodec().readTree(parser); 15 | return tree.toString(); 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | #server.servlet.contextPath=/poli 2 | server.port=6688 3 | spring.main.banner_mode=off 4 | 5 | spring.datasource.url=jdbc:sqlite:c:/test/poli.db 6 | spring.datasource.driver-class-name=org.sqlite.JDBC 7 | 8 | # datasource extra settings. 9 | spring.datasource.max-active=32 10 | spring.datasource.max-idle=16 11 | spring.datasource.min-idle=8 12 | spring.datasource.test-while-idle= true 13 | spring.datasource.time-between-eviction-runs-millis=60000 14 | spring.datasource.validation-query=SELECT 1 15 | spring.datasource.tomcat.max-wait=10000 16 | spring.datasource.sql-script-encoding=UTF-8 17 | 18 | # JDBC 19 | spring.jdbc.template.fetch-size=100 20 | 21 | poli.datasource-maximum-pool-size=50 22 | poli.maximum-query-records=-1 23 | poli.locale-language=en 24 | poli.allow-multiple-query-statements=false 25 | poli.export-server-url=http://127.0.0.1:6689/pdf 26 | -------------------------------------------------------------------------------- /src/main/resources/loader.properties: -------------------------------------------------------------------------------- 1 | # loader 2 | loader.path=jdbc-drivers/ -------------------------------------------------------------------------------- /src/main/resources/logback-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | log/server.log 6 | 7 | 8 | 9 | %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n 10 | 11 | 12 | 13 | 14 | log/server-%d{yyyy-MM-dd}.%i.log 15 | 3MB 16 | 5 17 | 15MB 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/test/java/com/shzlw/poli/dao/AuditLogDaoTest.java: -------------------------------------------------------------------------------- 1 | package com.shzlw.poli.dao; 2 | 3 | import com.shzlw.poli.model.AuditLog; 4 | import com.shzlw.poli.util.CommonUtils; 5 | import org.junit.Assert; 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 10 | import org.springframework.boot.test.context.SpringBootTest; 11 | import org.springframework.test.context.TestPropertySource; 12 | import org.springframework.test.context.jdbc.Sql; 13 | import org.springframework.test.context.junit4.SpringRunner; 14 | 15 | import java.time.LocalDateTime; 16 | import java.util.List; 17 | 18 | @RunWith(SpringRunner.class) 19 | @SpringBootTest 20 | @AutoConfigureMockMvc 21 | @TestPropertySource(locations="classpath:application-test.properties") 22 | @Sql(scripts = "classpath:schema-sqlite.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) 23 | public class AuditLogDaoTest { 24 | 25 | @Autowired 26 | AuditLogDao auditLogDao; 27 | 28 | @Test 29 | public void test() { 30 | long epoch = CommonUtils.toEpoch(LocalDateTime.now()); 31 | for (int i = 0; i < 20; i++) { 32 | auditLogDao.insert(epoch, "type", "data" + i); 33 | } 34 | 35 | List list = auditLogDao.findAll(1, 10, ""); 36 | Assert.assertEquals(10, list.size()); 37 | for (int i = 0; i < 10; i++) { 38 | Assert.assertEquals("data" + (19 - i), list.get(i).getData()); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/test/java/com/shzlw/poli/dao/DaoHelperTest.java: -------------------------------------------------------------------------------- 1 | package com.shzlw.poli.dao; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | 6 | public class DaoHelperTest { 7 | 8 | @Test 9 | public void testToOffset() { 10 | Assert.assertEquals(0, DaoHelper.toOffset(1, 10)); 11 | Assert.assertEquals(DaoHelper.DEFAULT_OFFSET, DaoHelper.toOffset(0, 10)); 12 | Assert.assertEquals(DaoHelper.DEFAULT_OFFSET, DaoHelper.toOffset(1, 0)); 13 | Assert.assertEquals(90, DaoHelper.toOffset(10, 10)); 14 | } 15 | 16 | @Test 17 | public void testToLimit() { 18 | Assert.assertEquals(10, DaoHelper.toLimit(10)); 19 | Assert.assertEquals(DaoHelper.DEFAULT_LIMIT, DaoHelper.toLimit(0)); 20 | Assert.assertEquals(DaoHelper.DEFAULT_LIMIT, DaoHelper.toLimit(-1)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/test/java/com/shzlw/poli/rest/InfoWsTest.java: -------------------------------------------------------------------------------- 1 | package com.shzlw.poli.rest; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.shzlw.poli.config.AppProperties; 5 | import com.shzlw.poli.dto.AppInfo; 6 | import com.shzlw.poli.util.Constants; 7 | import org.junit.Assert; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 12 | import org.springframework.boot.test.context.SpringBootTest; 13 | import org.springframework.test.context.TestPropertySource; 14 | import org.springframework.test.context.junit4.SpringRunner; 15 | import org.springframework.test.web.servlet.MockMvc; 16 | import org.springframework.test.web.servlet.MvcResult; 17 | 18 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 19 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 20 | 21 | @RunWith(SpringRunner.class) 22 | @SpringBootTest 23 | @AutoConfigureMockMvc 24 | @TestPropertySource(locations="classpath:application-test.properties") 25 | public class InfoWsTest { 26 | 27 | @Autowired 28 | MockMvc mvc; 29 | 30 | @Autowired 31 | ObjectMapper mapper; 32 | 33 | @Autowired 34 | AppProperties appProperties; 35 | 36 | @Test 37 | public void testGetAppInfo() throws Exception { 38 | MvcResult mvcResult = mvc.perform(get("/info/general")).andExpect(status().isOk()) 39 | .andReturn(); 40 | String responeText = mvcResult.getResponse().getContentAsString(); 41 | AppInfo appInfo = mapper.readValue(responeText, AppInfo.class); 42 | 43 | Assert.assertEquals(Constants.CURRENT_VERSION, appInfo.getVersion()); 44 | Assert.assertEquals(appProperties.getLocaleLanguage(), appInfo.getLocaleLanguage()); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/test/java/com/shzlw/poli/util/CommonUtilsTest.java: -------------------------------------------------------------------------------- 1 | package com.shzlw.poli.util; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | 6 | import java.time.LocalDateTime; 7 | import java.time.format.DateTimeFormatter; 8 | import java.time.temporal.ChronoUnit; 9 | 10 | public class CommonUtilsTest { 11 | 12 | @Test 13 | public void testFromEpoch() { 14 | Assert.assertNotNull(CommonUtils.fromEpoch(0)); 15 | } 16 | 17 | @Test 18 | public void testFromEpoch_toEpoch() { 19 | LocalDateTime dateTime = LocalDateTime.now(); 20 | long epoch = CommonUtils.toEpoch(dateTime); 21 | LocalDateTime newDateTime = CommonUtils.fromEpoch(epoch); 22 | long newEpoch = CommonUtils.toEpoch(newDateTime); 23 | 24 | Assert.assertEquals(0, dateTime.truncatedTo(ChronoUnit.SECONDS).compareTo(newDateTime.truncatedTo(ChronoUnit.SECONDS))); 25 | Assert.assertEquals(epoch, newEpoch); 26 | } 27 | 28 | @Test 29 | public void testToReadableDateTime() { 30 | String date = "2019-01-01 01:00:00"; 31 | DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:s"); 32 | LocalDateTime dateTime = LocalDateTime.parse(date, formatter); 33 | Assert.assertEquals(date, CommonUtils.toReadableDateTime(dateTime)); 34 | } 35 | 36 | @Test 37 | public void testToReadableDate() { 38 | String date = "2019-01-01 01:00:00"; 39 | DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:s"); 40 | LocalDateTime dateTime = LocalDateTime.parse(date, formatter); 41 | Assert.assertEquals("2019-01-01", CommonUtils.toReadableDate(dateTime)); 42 | } 43 | 44 | @Test 45 | public void testGetParamByAttrKey() { 46 | String rt = CommonUtils.getParamByAttrKey("attrKey"); 47 | Assert.assertEquals("$user_attr[attrKey]", rt); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/test/java/com/shzlw/poli/util/PasswordUtilsTest.java: -------------------------------------------------------------------------------- 1 | package com.shzlw.poli.util; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | 6 | public class PasswordUtilsTest { 7 | 8 | @Test 9 | public void testDecryptEncrypt() { 10 | String value = "1234567890"; 11 | String b = PasswordUtils.decrypt(PasswordUtils.encrypt(value)); 12 | Assert.assertEquals(value, b); 13 | } 14 | 15 | @Test 16 | public void testEncryptedPassword_2() { 17 | String password = "1234567890"; 18 | Assert.assertEquals(password, PasswordUtils.getDecryptedPassword(PasswordUtils.getEncryptedPassword(password))); 19 | 20 | password = ""; 21 | Assert.assertEquals(password, PasswordUtils.getDecryptedPassword(PasswordUtils.getEncryptedPassword(password))); 22 | 23 | password = null; 24 | Assert.assertEquals("", PasswordUtils.getDecryptedPassword(PasswordUtils.getEncryptedPassword(password))); 25 | } 26 | 27 | @Test 28 | public void testGetUniqueId() { 29 | Assert.assertEquals(22, PasswordUtils.getUniqueId().length()); 30 | } 31 | 32 | @Test 33 | public void testGenerateAdminPassword() { 34 | String password = "adminadmin"; 35 | Assert.assertNotNull(PasswordUtils.getMd5Hash(password)); 36 | } 37 | 38 | @Test 39 | public void testPadOrTrimTo16() { 40 | Assert.assertEquals("0000000000000000", PasswordUtils.padOrTrimTo16(null)); 41 | Assert.assertEquals("0000000000000000", PasswordUtils.padOrTrimTo16("")); 42 | Assert.assertEquals("0000000000000001", PasswordUtils.padOrTrimTo16("1")); 43 | Assert.assertEquals("1234567812345678", PasswordUtils.padOrTrimTo16("1234567812345678")); 44 | Assert.assertEquals("1234567812345678", PasswordUtils.padOrTrimTo16("12345678123456789")); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/test/resources/application-test.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.url=jdbc:sqlite::memory: 2 | spring.datasource.driverClassName=org.sqlite.JDBC -------------------------------------------------------------------------------- /src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /third-party-license/license-axios: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-present Matt Zabriskie 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /third-party-license/license-echarts-for-react: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) hustcc 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. -------------------------------------------------------------------------------- /third-party-license/license-fontawesome: -------------------------------------------------------------------------------- 1 | Font Awesome Free License 2 | ------------------------- 3 | 4 | Font Awesome Free is free, open source, and GPL friendly. You can use it for 5 | commercial projects, open source projects, or really almost whatever you want. 6 | Full Font Awesome Free license: https://fontawesome.com/license/free. 7 | 8 | # Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/) 9 | In the Font Awesome Free download, the CC BY 4.0 license applies to all icons 10 | packaged as SVG and JS file types. 11 | 12 | # Fonts: SIL OFL 1.1 License (https://scripts.sil.org/OFL) 13 | In the Font Awesome Free download, the SIL OFL license applies to all icons 14 | packaged as web and desktop font files. 15 | 16 | # Code: MIT License (https://opensource.org/licenses/MIT) 17 | In the Font Awesome Free download, the MIT license applies to all non-font and 18 | non-icon files. 19 | 20 | # Attribution 21 | Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font 22 | Awesome Free files already contain embedded comments with sufficient 23 | attribution, so you shouldn't need to do anything additional when using these 24 | files normally. 25 | 26 | We've kept attribution comments terse, so we ask that you do not actively work 27 | to remove them from files, especially code. They're a great way for folks to 28 | learn about Font Awesome. 29 | 30 | # Brand Icons 31 | All brand icons are trademarks of their respective owners. The use of these 32 | trademarks does not indicate endorsement of the trademark holder by Font 33 | Awesome, nor vice versa. **Please do not use brand logos for any purpose except 34 | to represent the company, product, or service to which they refer.** -------------------------------------------------------------------------------- /third-party-license/license-react: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Facebook, Inc. and its affiliates. 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. -------------------------------------------------------------------------------- /third-party-license/license-react-ace: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 James Hrisho 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 | -------------------------------------------------------------------------------- /third-party-license/license-react-beautiful-dnd: -------------------------------------------------------------------------------- 1 | Copyright 2019 Atlassian Pty Ltd 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /third-party-license/license-react-color: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Case Sandberg 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. -------------------------------------------------------------------------------- /third-party-license/license-react-i18next: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 i18next 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 | -------------------------------------------------------------------------------- /third-party-license/license-react-router: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) React Training 2016-2018 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. -------------------------------------------------------------------------------- /third-party-license/license-react-select: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Jed Watson 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. -------------------------------------------------------------------------------- /third-party-license/license-react-table: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Tanner Linsley 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. -------------------------------------------------------------------------------- /third-party-license/license-react-toastify: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Fadi Khadra 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. -------------------------------------------------------------------------------- /upgrade/poli_upgrade_v0.10.0.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE p_report ADD project TEXT; 2 | 3 | CREATE TABLE 4 | IF NOT EXISTS p_user_favourite ( 5 | user_id INTEGER NOT NULL, 6 | report_id INTEGER NOT NULL, 7 | PRIMARY KEY (user_id, report_id), 8 | FOREIGN KEY (user_id) REFERENCES p_user(id), 9 | FOREIGN KEY (report_id) REFERENCES p_report(id) 10 | ); 11 | 12 | CREATE TABLE 13 | IF NOT EXISTS p_shared_report ( 14 | id INTEGER NOT NULL PRIMARY KEY, 15 | share_key VARCHAR NOT NULL, 16 | report_id INTEGER NOT NULL, 17 | report_type VARCHAR NOT NULL, 18 | user_id INTEGER NOT NULL, 19 | created_at INTEGER NOT NULL, 20 | expired_by INTEGER NOT NULL, 21 | FOREIGN KEY (report_id) REFERENCES p_report(id), 22 | FOREIGN KEY (user_id) REFERENCES p_user(id) 23 | ); -------------------------------------------------------------------------------- /upgrade/poli_upgrade_v0.12.0.sql: -------------------------------------------------------------------------------- 1 | -- SQLite 2 | CREATE TABLE 3 | IF NOT EXISTS p_saved_query ( 4 | id INTEGER NOT NULL PRIMARY KEY, 5 | datasource_id INTEGER, 6 | name TEXT NOT NULL UNIQUE, 7 | sql_query TEXT, 8 | endpoint_name TEXT UNIQUE, 9 | endpoint_accesscode TEXT 10 | ); 11 | 12 | CREATE TABLE 13 | IF NOT EXISTS p_audit_log ( 14 | id INTEGER NOT NULL PRIMARY KEY, 15 | created_at INTEGER NOT NULL, 16 | type TEXT NOT NULL, 17 | data TEXT 18 | ); 19 | 20 | CREATE INDEX idx_audit_log_created_at ON p_audit_log (created_at); 21 | -------------------------------------------------------------------------------- /upgrade/poli_upgrade_v0.7.0.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE 2 | IF NOT EXISTS p_canned_report ( 3 | id INTEGER NOT NULL PRIMARY KEY, 4 | user_id INTEGER NOT NULL, 5 | created_at INTEGER NOT NULL, 6 | name TEXT NOT NULL, 7 | data TEXT, 8 | FOREIGN KEY (user_id) REFERENCES p_user(id) 9 | ); -------------------------------------------------------------------------------- /upgrade/poli_upgrade_v0.9.0.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE 2 | IF NOT EXISTS p_user_attribute ( 3 | user_id INTEGER NOT NULL, 4 | attr_key TEXT NOT NULL, 5 | attr_value TEXT, 6 | FOREIGN KEY (user_id) REFERENCES p_user(id) 7 | ); -------------------------------------------------------------------------------- /version: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | VERSION=0.12.2 3 | 4 | echo $VERSION 5 | -------------------------------------------------------------------------------- /web-app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /web-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "poli-web-app", 3 | "version": "0.12.2", 4 | "private": true, 5 | "dependencies": { 6 | "@fortawesome/fontawesome-svg-core": "^1.2.28", 7 | "@fortawesome/free-regular-svg-icons": "^5.13.0", 8 | "@fortawesome/free-solid-svg-icons": "^5.13.0", 9 | "@fortawesome/react-fontawesome": "^0.1.9", 10 | "axios": "^0.18.1", 11 | "echarts": "^4.7.0", 12 | "echarts-for-react": "^2.0.15-beta.1", 13 | "i18next": "^17.3.1", 14 | "react": "^16.13.1", 15 | "react-ace": "^6.6.0", 16 | "react-beautiful-dnd": "^13.0.0", 17 | "react-color": "^2.18.0", 18 | "react-dom": "^16.13.1", 19 | "react-i18next": "^10.13.2", 20 | "react-router-dom": "^5.1.2", 21 | "react-scripts": "3.4.1", 22 | "react-select": "^3.1.0", 23 | "react-table": "^6.11.5", 24 | "react-toastify": "^5.5.0" 25 | }, 26 | "scripts": { 27 | "start": "react-scripts start", 28 | "build": "react-scripts build", 29 | "test": "react-scripts test", 30 | "eject": "react-scripts eject" 31 | }, 32 | "eslintConfig": { 33 | "extends": "react-app" 34 | }, 35 | "browserslist": [] 36 | } 37 | -------------------------------------------------------------------------------- /web-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shzlw/poli/5b90d6adc6f839866cb77854ad33f3cd9e1e26e8/web-app/public/favicon.ico -------------------------------------------------------------------------------- /web-app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Poli 8 | 9 | 10 | 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /web-app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Poli", 3 | "name": "Poli", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /web-app/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 | -------------------------------------------------------------------------------- /web-app/src/api/ApiService.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export const fetchJdbcdatasources = async () => { 4 | return await httpGet('/ws/jdbcdatasources'); 5 | } 6 | 7 | export const fetchDatabaseSchema = async (jdbcDataSourceId) => { 8 | return await httpGet(`/ws/jdbcdatasources/schema/${jdbcDataSourceId}`); 9 | } 10 | 11 | export const runQuery = async (jdbcDataSourceId, sqlQuery, resultLimit = 100) => { 12 | const requestBody = { 13 | jdbcDataSourceId: jdbcDataSourceId, 14 | sqlQuery: sqlQuery, 15 | resultLimit: resultLimit 16 | }; 17 | return await httpPost('/ws/jdbcquery/query', requestBody); 18 | } 19 | 20 | export const fetchAuditLogs = async (page, pageSize, searchValue) => { 21 | const parameters = { 22 | search: searchValue, 23 | page, 24 | size: pageSize 25 | }; 26 | 27 | return await httpGet('/ws/audit-logs', parameters); 28 | } 29 | 30 | export const fetchSavedQueries = async (page, pageSize, searchValue) => { 31 | const parameters = { 32 | search: searchValue, 33 | page, 34 | size: pageSize 35 | }; 36 | 37 | return await httpGet('/ws/saved-queries', parameters); 38 | } 39 | 40 | 41 | export const httpGet = async (url, parameters = {}) => { 42 | try { 43 | const response = await axios.get(url, {params: parameters}); 44 | return response; 45 | } catch(error) { 46 | console.log(error); 47 | return {}; 48 | } 49 | } 50 | 51 | export const httpPost = async (url, requestBody = {}) => { 52 | try { 53 | const response = await axios.post(url, requestBody); 54 | return response; 55 | } catch(error) { 56 | console.log(error); 57 | return {}; 58 | } 59 | } 60 | 61 | export const httpPut = async (url, requestBody = {}) => { 62 | try { 63 | const response = await axios.put(url, requestBody); 64 | return response; 65 | } catch(error) { 66 | console.log(error); 67 | return {}; 68 | } 69 | } 70 | 71 | export const httpDelete = async (url, parameters = {}) => { 72 | try { 73 | const response = await axios.delete(url, {params: parameters}); 74 | return response; 75 | } catch(error) { 76 | console.log(error); 77 | return {}; 78 | } 79 | } 80 | 81 | -------------------------------------------------------------------------------- /web-app/src/api/Constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Constants 3 | */ 4 | export const STATIC = 'static'; 5 | export const CHART = 'chart'; 6 | export const FILTER = 'filter'; 7 | export const COMPONENT_TYPES = [STATIC, CHART, FILTER]; 8 | 9 | export const SLICER = 'slicer'; 10 | export const SINGLE_VALUE= 'single'; 11 | export const DATE_PICKER = 'date picker'; 12 | export const FILTER_TYPES = [SLICER, SINGLE_VALUE, DATE_PICKER]; 13 | 14 | export const PIE = 'pie'; 15 | export const TABLE = 'table'; 16 | export const LINE = 'line'; 17 | export const AREA = 'area'; 18 | export const BAR = 'bar'; 19 | export const HEATMAP = 'heatmap'; 20 | export const TREEMAP = 'treemap'; 21 | export const FUNNEL = 'funnel'; 22 | export const CARD = 'card'; 23 | export const KANBAN = 'kanban'; 24 | export const CHART_TYPES = [TABLE, PIE, LINE, BAR, AREA, CARD, FUNNEL, TREEMAP, HEATMAP, KANBAN]; 25 | 26 | export const IMAGE = 'image'; 27 | export const TEXT = 'text'; 28 | export const HTML = 'html'; 29 | export const IFRAME = 'iframe'; 30 | export const STATIC_TYPES = [TEXT, IMAGE, IFRAME, HTML]; 31 | 32 | 33 | export const DEFAULT_FILTER_VIEW_WIDTH = 200; 34 | 35 | export const SYS_ROLE_ADMIN = 'admin'; 36 | export const SYS_ROLE_DEVELOPER = 'developer'; 37 | export const SYS_ROLE_VIEWER = 'viewer'; 38 | 39 | export const DEFAULT_REPORT_HEIGHT = 600; 40 | export const DEFAULT_REPORT_FIXED_WIDTH = 800; 41 | 42 | export const REMEMBERME = 'rememberMe'; 43 | export const YES = 'yes'; 44 | export const NO = 'no'; 45 | 46 | export const COLOR_SLATE = 'rgba(9, 30, 66, 1)'; 47 | export const COLOR_WHITE = 'rgba(255, 255, 255, 1)'; 48 | 49 | export const DEFAULT = 'default'; 50 | export const VINTAGE = 'vintage'; 51 | export const ROMA = 'roma'; 52 | export const MACARONS = 'macarons'; 53 | export const SHINE = 'shine'; 54 | export const CHART_COLOR_PLATETTES = [DEFAULT, VINTAGE, ROMA, MACARONS, SHINE]; 55 | 56 | export const ADHOC = 'adhoc'; 57 | export const CANNED = 'canned'; 58 | 59 | export const DEFAULT_MIN_COLOR = 'rgba(222, 53, 11, 1)'; 60 | export const DEFAULT_MAX_COLOR = 'rgba(0, 135, 90, 1)'; 61 | 62 | export const REPORT = 'report'; 63 | 64 | -------------------------------------------------------------------------------- /web-app/src/api/Util.js: -------------------------------------------------------------------------------- 1 | 2 | export const isArrayEmpty = (array) => { 3 | return !Array.isArray(array) || !array.length; 4 | }; 5 | 6 | export const jsonToArray = (json) => { 7 | let array; 8 | try { 9 | array = JSON.parse(json); 10 | } catch(e) { 11 | array = []; 12 | } 13 | return array; 14 | } 15 | 16 | export const getReadableDiffTime = (d1, d2) => { 17 | const seconds = Math.abs(d1 - d2) / 1000; 18 | if (seconds <= 5) { 19 | return 'Just now'; 20 | } else if (seconds > 5 && seconds < 60) { 21 | return Math.floor(seconds) + ' seconds ago'; 22 | } 23 | 24 | const minutes = Math.floor(seconds / 60); 25 | if (minutes === 1) { 26 | return '1 minute ago'; 27 | } else if (minutes > 1 && minutes < 60) { 28 | return minutes + " minutes ago"; 29 | } 30 | 31 | const hours = Math.floor(seconds / 3600); 32 | if (hours === 1) { 33 | return '1 hour ago'; 34 | } 35 | return hours + " hours ago"; 36 | } 37 | 38 | export const leftPadZero = (n) => { 39 | return parseInt(n, 10) < 10 ? '0' + n : n; 40 | } 41 | 42 | // Spring rest error 43 | // {"timestamp":"2019-12-18T20:50:29.009+0000", 44 | // "status":405, 45 | // "error":"Method Not Allowed", 46 | // "message":"Request method 'GET' not supported", 47 | // "path":"/auth/signup"} 48 | export const toReadableServerError = (error) => { 49 | const resData = error.response.data || {}; 50 | const serverError = resData.error; 51 | const serverMsg = resData.message; 52 | const displayError = serverError + ": " + serverMsg; 53 | return displayError; 54 | } 55 | -------------------------------------------------------------------------------- /web-app/src/components/Checkbox/Checkbox.css: -------------------------------------------------------------------------------- 1 | .checkbox-container { 2 | display: flex; 3 | flex-flow: row; 4 | height: 20px; 5 | } 6 | 7 | .checkbox-checkmark { 8 | width: 20px; 9 | height: 20px; 10 | flex: 0 1 auto; 11 | cursor: pointer; 12 | } 13 | 14 | .checkbox-label { 15 | flex: 1 1 auto; 16 | white-space: nowrap; 17 | overflow: hidden; 18 | text-overflow: ellipsis; 19 | } -------------------------------------------------------------------------------- /web-app/src/components/Checkbox/Checkbox.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import './Checkbox.css'; 4 | 5 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 6 | 7 | 8 | class Checkbox extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | this.state = { 12 | }; 13 | } 14 | 15 | static propTypes = { 16 | name: PropTypes.string.isRequired, 17 | value: PropTypes.string.isRequired, 18 | checked: PropTypes.bool.isRequired, 19 | onChange: PropTypes.func.isRequired, 20 | readOnly: PropTypes.bool 21 | }; 22 | 23 | toggle = () => { 24 | const { 25 | name, 26 | checked, 27 | readOnly = false 28 | } = this.props; 29 | 30 | if (readOnly) { 31 | return; 32 | } 33 | 34 | this.props.onChange(name, !checked); 35 | } 36 | 37 | render() { 38 | const { 39 | value, 40 | checked 41 | } = this.props; 42 | 43 | return ( 44 |
45 |
46 | { checked ? ( 47 | 48 | ) : ( 49 | 50 | )} 51 |
52 |
{value}
53 |
54 | ); 55 | } 56 | } 57 | 58 | export default Checkbox; 59 | -------------------------------------------------------------------------------- /web-app/src/components/ColorPicker/ColorPicker.css: -------------------------------------------------------------------------------- 1 | .colorpicker-select { 2 | background-color: #373737; 3 | border: 1px solid #00001c; 4 | display: inline-block; 5 | box-shadow: rgba(50, 50, 93, 0.149) 0px 1px 3px, rgba(0, 0, 0, 0.0196) 0px 1px 0px; 6 | border-radius: 4px; 7 | border: 0; 8 | outline: none; 9 | cursor: pointer; 10 | } 11 | 12 | .colorpicker-color { 13 | width: 36px; 14 | height: 20px; 15 | border-radius: 4px; 16 | } 17 | 18 | .colorpicker-popover { 19 | position: fixed; 20 | z-index: 90; 21 | } 22 | 23 | .colorpicker-overlay { 24 | position: fixed; 25 | top: 0; 26 | left: 0; 27 | right: 0; 28 | bottom: 0; 29 | z-index: 89; 30 | } -------------------------------------------------------------------------------- /web-app/src/components/ColorPicker/ColorPicker.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ChromePicker } from 'react-color'; 3 | import './ColorPicker.css' 4 | 5 | class ColorPicker extends React.Component { 6 | 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | showPalette: false 11 | } 12 | } 13 | 14 | handleClick = () => { 15 | this.setState(prevState => ({ 16 | showPalette: !prevState.showPalette 17 | })); 18 | }; 19 | 20 | handleChange = (color) => { 21 | const { name } = this.props; 22 | const rgb = color.rgb; 23 | const rgba = `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${rgb.a})`; 24 | this.props.onChange(name, rgba); 25 | }; 26 | 27 | close = () => { 28 | this.setState({ 29 | showPalette: false 30 | }); 31 | } 32 | 33 | render() { 34 | 35 | const color = { 36 | background: this.props.value 37 | }; 38 | 39 | return ( 40 |
41 |
42 |
43 |
44 | { this.state.showPalette && ( 45 |
46 |
47 |
48 |
49 | 50 |
51 |
52 | )} 53 |
54 | ) 55 | }; 56 | } 57 | 58 | export default ColorPicker; -------------------------------------------------------------------------------- /web-app/src/components/DropdownDialog/DropdownDialog.css: -------------------------------------------------------------------------------- 1 | .dropdown-overlay { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | right: 0; 6 | bottom: 0; 7 | z-index: 150; 8 | } 9 | 10 | .dropdown-panel { 11 | position: fixed; 12 | z-index: 151; 13 | box-shadow: rgba(50, 50, 93, 0.149) 0px 1px 3px, rgba(0, 0, 0, 0.0196) 0px 1px 0px; 14 | border-radius: 4px; 15 | top: 85px; 16 | right: 35px; 17 | background-color: #FFFFFF; 18 | border: 1px solid #e0e0e0; 19 | } 20 | 21 | .dropdown-body { 22 | padding: 8px; 23 | } -------------------------------------------------------------------------------- /web-app/src/components/DropdownDialog/DropdownDialog.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './DropdownDialog.css'; 3 | 4 | class DropdownDialog extends React.Component { 5 | 6 | render() { 7 | const { 8 | show = false, 9 | children 10 | } = this.props; 11 | 12 | const displayClass = show ? 'display-block' : 'display-none'; 13 | return ( 14 |
this.props.onClose()}> 15 |
16 |
17 | {children} 18 |
19 |
20 |
21 | ); 22 | } 23 | } 24 | 25 | export default DropdownDialog; -------------------------------------------------------------------------------- /web-app/src/components/GridLayout.css: -------------------------------------------------------------------------------- 1 | 2 | .grid-layout { 3 | position: relative; 4 | background-color: #FFFFFF; 5 | } 6 | 7 | .grid-box { 8 | position: absolute; 9 | overflow-x: hidden; 10 | overflow-y: hidden; 11 | display: flex; 12 | flex-flow: column; 13 | height: 100%; 14 | } 15 | 16 | .grid-box-title { 17 | padding: 3px 6px; 18 | flex: 0 1 auto; 19 | } 20 | 21 | .grid-box-title-value { 22 | font-size: 18px; 23 | } 24 | 25 | .grid-box-content { 26 | flex: 1 1 auto; 27 | overflow-y: auto; 28 | } 29 | 30 | .grid-box-content-panel { 31 | padding: 0px 5px; 32 | z-index: 1; 33 | } 34 | 35 | .grid-title-button-panel { 36 | position: absolute; 37 | right: 0; 38 | top: 0; 39 | background-color: transparent; 40 | padding: 4px; 41 | z-index: 2; 42 | min-width: 20px; 43 | height: 20px; 44 | } 45 | 46 | .grid-draggable { 47 | cursor: move; 48 | height: 25px; 49 | position: absolute; 50 | left: 0px; 51 | top: 0px; 52 | right: 0px; 53 | background-color: transparent; 54 | z-index: 10; 55 | } 56 | 57 | .grid-resizable { 58 | width: 12px; 59 | height: 12px; 60 | position: absolute; 61 | right: 0; 62 | bottom: 0; 63 | cursor: se-resize; 64 | z-index: 10; 65 | } 66 | 67 | .download-csv-button { 68 | display: none; 69 | z-index: 10; 70 | } 71 | 72 | .grid-title-button-panel:hover .download-csv-button { 73 | display: inline-block; 74 | transition: 0.3s; 75 | } -------------------------------------------------------------------------------- /web-app/src/components/GridLayout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import GridItem from './GridItem'; 4 | import './GridLayout.css'; 5 | 6 | class GridLayout extends React.Component { 7 | 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | }; 12 | } 13 | 14 | render() { 15 | const { 16 | width, 17 | height, 18 | backgroundColor, 19 | components = [], 20 | showGridlines, 21 | isEditMode 22 | } = this.props; 23 | 24 | let style = { 25 | width: width + 'px', 26 | height: height + 'px', 27 | backgroundColor: backgroundColor 28 | }; 29 | 30 | if (showGridlines && isEditMode) { 31 | style.backgroundSize = '15px 15px'; 32 | style.backgroundImage = 'radial-gradient(rgb(23, 43, 77) 10%, transparent 10%)'; 33 | } 34 | 35 | const boxItems = components.map((component, index) => 36 | 50 | ); 51 | 52 | 53 | return ( 54 |
57 | {boxItems} 58 |
59 | ) 60 | } 61 | } 62 | 63 | export default GridLayout; -------------------------------------------------------------------------------- /web-app/src/components/Kanban/Kanban.css: -------------------------------------------------------------------------------- 1 | .kanban-container { 2 | width: inherit; 3 | height: inherit; 4 | 5 | display: flex; 6 | flex-direction: row; 7 | } 8 | 9 | .kanban-column { 10 | flex: 1; 11 | display: flex; 12 | flex-direction: column; 13 | } 14 | 15 | .kanban-group-title { 16 | flex: 0 1 auto; 17 | text-align: center; 18 | font-weight: bold; 19 | font-size: 18px; 20 | } 21 | 22 | .kanban-group-panel { 23 | flex: 1; 24 | overflow-y: auto; 25 | } 26 | 27 | .kanban-block { 28 | box-shadow: rgba(50, 50, 93, 0.149) 0px 1px 3px, rgba(0, 0, 0, 0.0196) 0px 1px 0px; 29 | border: 1px solid #e0e0e0; 30 | outline: 0; 31 | border-radius: 4px; 32 | padding: 5px; 33 | margin: 8px 5px; 34 | background-color: #FFFFFF; 35 | text-align: center; 36 | } 37 | 38 | .kanban-block-title { 39 | font-weight: bold; 40 | font-size: 16px; 41 | border-bottom: 1px solid black; 42 | padding-bottom: 5px; 43 | } 44 | 45 | .kanban-block-body { 46 | padding-top: 5px; 47 | } 48 | 49 | .kanban-block-body-row { 50 | padding-bottom: 5px; 51 | } -------------------------------------------------------------------------------- /web-app/src/components/Kanban/Kanban.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import './Kanban.css'; 4 | 5 | class Kanban extends React.Component { 6 | 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | } 11 | } 12 | 13 | static propTypes = { 14 | groupByField: PropTypes.string.isRequired, 15 | data: PropTypes.array.isRequired, 16 | blockTitleField: PropTypes.string 17 | }; 18 | 19 | componentDidMount() { 20 | } 21 | 22 | render() { 23 | // TODO: allow to setup fixed width for column? 24 | // If the width is defined, the panel should be scrollable. 25 | // TODO: sort the blocks based on orderByField? 26 | const { 27 | groupByField, 28 | blockTitleField, 29 | data = [] 30 | } = this.props; 31 | 32 | const groupData = []; 33 | for (let i = 0; i < data.length; i++) { 34 | const row = data[i]; 35 | const index = groupData.findIndex(g => g.groupBy === row[groupByField]); 36 | if (index === -1) { 37 | groupData.push({ 38 | groupBy: row[groupByField], 39 | blocks: [row] 40 | }); 41 | } else { 42 | groupData[index].blocks.push(row); 43 | } 44 | } 45 | 46 | const groupPanelItems = []; 47 | for (let i = 0; i < groupData.length; i++) { 48 | const { 49 | groupBy, 50 | blocks 51 | } = groupData[i]; 52 | 53 | const blockItems = []; 54 | for (let j = 0; j < blocks.length; j++) { 55 | const blockRowItems = []; 56 | let blockTitleValue = null; 57 | for (let [key, value] of Object.entries(blocks[j])) { 58 | if (key === blockTitleField) { 59 | blockTitleValue = value; 60 | } else if (key !== groupByField) { 61 | blockRowItems.push( 62 |
{value}
63 | ); 64 | } 65 | } 66 | 67 | blockItems.push( 68 |
69 | { blockTitleValue && ( 70 |
{blockTitleValue}
71 | )} 72 |
73 | {blockRowItems} 74 |
75 |
76 | ); 77 | } 78 | 79 | groupPanelItems.push( 80 |
81 |
{groupBy}
82 |
83 | {blockItems} 84 |
85 |
86 | ); 87 | } 88 | 89 | return ( 90 |
91 | {groupPanelItems} 92 |
93 | ); 94 | } 95 | } 96 | 97 | export default Kanban; -------------------------------------------------------------------------------- /web-app/src/components/Modal/Modal.css: -------------------------------------------------------------------------------- 1 | .modal-overlay { 2 | background: rgba(0, 0, 0, 0.6); 3 | position: fixed; 4 | top: 0; 5 | left: 0; 6 | right: 0; 7 | bottom: 0; 8 | z-index: 150; 9 | } 10 | 11 | .model-title { 12 | font-size: 22px; 13 | float: left; 14 | } 15 | 16 | .model-close-button { 17 | color: #212121; 18 | float: right; 19 | cursor: pointer; 20 | } 21 | 22 | .model-close-button:hover { 23 | background-color: #212121; 24 | color: #FFFFFF; 25 | } 26 | 27 | .modal-panel { 28 | background-color: #efefef; 29 | position: fixed; 30 | overflow-y: auto; 31 | padding: 10px; 32 | z-index: 200; 33 | box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); 34 | max-height: calc(100vh - 60px); 35 | } -------------------------------------------------------------------------------- /web-app/src/components/Modal/Modal.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './Modal.css'; 3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 4 | 5 | class Modal extends React.Component { 6 | 7 | render() { 8 | const { 9 | show, 10 | modalClass = '', 11 | title, 12 | children 13 | } = this.props; 14 | 15 | const modalStatus = show ? 'display-block' : 'display-none'; 16 | return ( 17 |
18 |
19 |
20 |
{title}
21 | 24 |
25 |
26 |
27 | {children} 28 |
29 |
30 |
31 | ); 32 | } 33 | } 34 | 35 | export default Modal; -------------------------------------------------------------------------------- /web-app/src/components/SearchInput/SearchInput.css: -------------------------------------------------------------------------------- 1 | .search-input-container { 2 | box-shadow: rgba(50, 50, 93, 0.149) 0px 1px 3px, rgba(0, 0, 0, 0.0196) 0px 1px 0px; 3 | display: flex; 4 | flex-direction: row; 5 | width: inherit; 6 | box-sizing: border-box; 7 | border: 1px solid #e0e0e0; 8 | border-radius: 4px; 9 | } 10 | 11 | .search-input-icon { 12 | width: 32px; 13 | height: 31px; 14 | background-color: #FFFFFF; 15 | border-radius: 4px 0px 0px 4px; 16 | } 17 | 18 | .search-input-input { 19 | -webkit-appearance: none; 20 | -webkit-box-sizing: border-box; 21 | -moz-box-sizing: border-box; 22 | box-sizing: border-box; 23 | width: calc(100% - 64px); 24 | margin: 0px !important; 25 | padding: 5px 0px !important; 26 | box-shadow: none !important; 27 | border-radius: 0 !important; 28 | border: 2px solid #FFFFFF; 29 | outline: 0; 30 | background: #FFFFFF; 31 | font-size: 16px; 32 | height: 31px; 33 | } 34 | 35 | .search-input-input:focus { 36 | border: 2px solid #FFFFFF !important; 37 | } 38 | 39 | .search-input-reset-button { 40 | border-radius: 0px 4px 4px 0px; 41 | width: 32px; 42 | height: 31px; 43 | box-shadow: none; 44 | background-color: #FFFFFF; 45 | color: #212121; 46 | border: 0; 47 | } 48 | 49 | .search-input-reset-button:hover { 50 | background-color: #FFFFFF; 51 | color: #212121; 52 | } 53 | 54 | -------------------------------------------------------------------------------- /web-app/src/components/SearchInput/SearchInput.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 4 | import './SearchInput.css'; 5 | 6 | class SearchInput extends React.Component { 7 | 8 | constructor(props) { 9 | super(props); 10 | this.state = {}; 11 | } 12 | 13 | static propTypes = { 14 | name: PropTypes.string.isRequired, 15 | value: PropTypes.string.isRequired, 16 | onChange: PropTypes.func.isRequired, 17 | inputWidth: PropTypes.number 18 | }; 19 | 20 | handleInputChange = (event) => { 21 | const { name } = this.props; 22 | const value = event.target.value; 23 | this.props.onChange(name, value); 24 | } 25 | 26 | reset = () => { 27 | const { name } = this.props; 28 | this.props.onChange(name, ''); 29 | } 30 | 31 | render() { 32 | const { 33 | name, 34 | value, 35 | inputWidth 36 | } = this.props; 37 | 38 | let inputStyle = {}; 39 | if (inputWidth) { 40 | inputStyle.width = inputWidth + 'px'; 41 | } 42 | 43 | return ( 44 |
45 |
46 | 47 |
48 | 56 | 61 |
62 | ); 63 | } 64 | } 65 | 66 | export default SearchInput; -------------------------------------------------------------------------------- /web-app/src/components/Select.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | class Select extends React.Component { 5 | 6 | constructor(props) { 7 | super(props); 8 | this.state = {}; 9 | } 10 | 11 | static propTypes = { 12 | name: PropTypes.string.isRequired, 13 | value: PropTypes.string.isRequired, 14 | onChange: PropTypes.func.isRequired, 15 | options: PropTypes.array.isRequired, 16 | optionDisplay: PropTypes.string, 17 | optionValue: PropTypes.string, 18 | preloadOneEmpty: PropTypes.bool 19 | }; 20 | 21 | handleOptionChange = (event) => { 22 | const name = this.props.name; 23 | const value = event.target.value; 24 | this.props.onChange(name, value); 25 | } 26 | 27 | render() { 28 | const { 29 | value, 30 | options = [], 31 | optionValue, 32 | optionDisplay, 33 | preloadOneEmpty = true 34 | } = this.props; 35 | 36 | const optionList = []; 37 | if (preloadOneEmpty) { 38 | optionList.push( 39 | 40 | ); 41 | } 42 | 43 | options.forEach((option, index) => { 44 | let value; 45 | let display; 46 | if (optionValue && optionDisplay) { 47 | // The options contain objects. 48 | value = option[optionValue]; 49 | display = option[optionDisplay]; 50 | } else { 51 | // The options contain string or number. 52 | value = option; 53 | display = option; 54 | } 55 | 56 | optionList.push( 57 | 58 | ) 59 | }); 60 | 61 | return ( 62 | 68 | ) 69 | }; 70 | } 71 | 72 | export default Select; -------------------------------------------------------------------------------- /web-app/src/components/SelectButtons/SelectButtons.css: -------------------------------------------------------------------------------- 1 | .selectbuttons-container { 2 | display: flex; 3 | flex-direction: row; 4 | width: inherit; 5 | box-sizing: border-box; 6 | } 7 | 8 | .selectbuttons-container .select-button { 9 | margin-right: 5px; 10 | min-width: 100px; 11 | text-align: center; 12 | border-radius: 4px; 13 | font-weight: 700; 14 | height: 32px; 15 | line-height: 32px; 16 | cursor: pointer; 17 | background-color: #FFFFFF; 18 | border: 0; 19 | outline: none; 20 | color: #8993A4; 21 | box-shadow: rgba(50, 50, 93, 0.149) 0px 1px 3px, rgba(0, 0, 0, 0.0196) 0px 1px 0px; 22 | flex-grow: 1; 23 | } 24 | 25 | .selectbuttons-container .select-button:hover { 26 | background-color: #C1C7D0; 27 | } 28 | 29 | .selectbuttons-container .select-button-active { 30 | background-color: #212121; 31 | color: white; 32 | } 33 | 34 | 35 | -------------------------------------------------------------------------------- /web-app/src/components/SelectButtons/SelectButtons.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import './SelectButtons.css'; 4 | 5 | class SelectButtons extends React.Component { 6 | 7 | constructor(props) { 8 | super(props); 9 | this.state = {}; 10 | } 11 | 12 | static propTypes = { 13 | name: PropTypes.string.isRequired, 14 | value: PropTypes.string.isRequired, 15 | selections: PropTypes.array.isRequired, 16 | onChange: PropTypes.func.isRequired 17 | }; 18 | 19 | clickButton = (value) => { 20 | const { name } = this.props; 21 | this.props.onChange(name, value); 22 | } 23 | 24 | render() { 25 | const { 26 | value, 27 | selections = [] 28 | } = this.props; 29 | const buttonItems = []; 30 | for (let i = 0; i < selections.length; i++) { 31 | const { 32 | display, 33 | value: actualValue 34 | } = selections[i]; 35 | const activeClass = actualValue === value ? 'select-button-active' : ''; 36 | buttonItems.push( 37 |
this.clickButton(actualValue)} key={actualValue}>{display}
38 | ) 39 | } 40 | 41 | return ( 42 |
43 | {buttonItems} 44 |
45 | ) 46 | }; 47 | } 48 | 49 | export default SelectButtons; -------------------------------------------------------------------------------- /web-app/src/components/Tabs/Tabs.css: -------------------------------------------------------------------------------- 1 | .tab-header { 2 | list-style-type: none; 3 | border-bottom: 1px solid #ccc; 4 | padding: 0px; 5 | margin: 0px; 6 | } 7 | 8 | .tab-header-item { 9 | display: inline-block; 10 | list-style: none; 11 | margin-bottom: -1px; 12 | padding: 6px 12px; 13 | cursor: pointer; 14 | color: #8993A4; 15 | } 16 | 17 | .tab-header-active { 18 | border-bottom: 3px solid #0065FF; 19 | color: #212121; 20 | } 21 | 22 | .tab-content { 23 | } -------------------------------------------------------------------------------- /web-app/src/components/Tabs/Tabs.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './Tabs.css'; 3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 4 | 5 | class Tabs extends React.Component { 6 | 7 | handleTabClick = (title) => { 8 | this.props.onTabChange(title); 9 | } 10 | 11 | render() { 12 | const { 13 | activeTab, 14 | children, 15 | showTabBgColor = false 16 | } = this.props; 17 | 18 | const tabHeaders = []; 19 | let tabContent = null; 20 | for (let i = 0; i < children.length; i++) { 21 | if (children[i]) { 22 | const { 23 | title, 24 | icon, 25 | iconOnly = false 26 | } = children[i].props; 27 | 28 | let active = ''; 29 | let tabStyle = {}; 30 | if (title === activeTab) { 31 | active = 'tab-header-active'; 32 | tabContent = children[i].props.children; 33 | tabStyle = showTabBgColor ? {backgroundColor: '#FFFFFF'} : {}; 34 | } 35 | 36 | tabHeaders.push( 37 |
  • this.handleTabClick(title)}> 38 | { iconOnly ? ( 39 | 40 | ) : ( 41 | {title} 42 | )} 43 |
  • 44 | ); 45 | } 46 | } 47 | 48 | return ( 49 |
    50 |
      51 | {tabHeaders} 52 |
    53 |
    54 | {tabContent} 55 |
    56 |
    57 | ); 58 | } 59 | } 60 | 61 | export default Tabs; 62 | 63 | -------------------------------------------------------------------------------- /web-app/src/components/Toast/Toast.css: -------------------------------------------------------------------------------- 1 | .toast-container { 2 | text-align: center; 3 | font-weight: 700; 4 | color: #FFFFFF; 5 | position: fixed; 6 | padding: 10px; 7 | z-index: 300; 8 | left: 50%; 9 | width: 400px; 10 | top: 10px; 11 | margin-left: -200px; 12 | box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); 13 | overflow-y: auto; 14 | } -------------------------------------------------------------------------------- /web-app/src/components/Toast/Toast.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './Toast.css'; 3 | 4 | const COLOR_RED = '#ef5350'; 5 | const COLOR_GREEN = '#66bb6a'; 6 | 7 | class Toast extends React.Component { 8 | 9 | constructor(props) { 10 | super(props); 11 | this.state = { 12 | showToast: false, 13 | message: '', 14 | backgroundColor: COLOR_RED, 15 | timeoutId: '' 16 | }; 17 | 18 | Toast._toastRef = this; 19 | } 20 | 21 | componentWillUnmount() { 22 | const { timeoutId } = this.state; 23 | if (timeoutId) { 24 | clearInterval(timeoutId); 25 | } 26 | } 27 | 28 | show = (message, bgColor) => { 29 | const { timeoutId } = this.state; 30 | if (timeoutId) { 31 | clearTimeout(timeoutId); 32 | } 33 | this.setState({ 34 | showToast: true, 35 | message: message, 36 | backgroundColor: bgColor, 37 | timeoutId: '' 38 | }, () => { 39 | const timeoutId = setTimeout(() => { 40 | this.hide(); 41 | }, 3000); 42 | this.setState({ 43 | timeoutId: timeoutId 44 | }); 45 | }); 46 | } 47 | 48 | hide = () => { 49 | this.setState({ 50 | showToast: false, 51 | message: '' 52 | }); 53 | } 54 | 55 | static showSuccess = (message) => { 56 | Toast._toastRef.show(message, COLOR_GREEN); 57 | } 58 | 59 | static showError = (message) => { 60 | Toast._toastRef.show(message, COLOR_RED); 61 | } 62 | 63 | render() { 64 | const { 65 | showToast, 66 | message, 67 | backgroundColor 68 | } = this.state; 69 | 70 | const toastStatus = showToast ? 'display-block' : 'display-none'; 71 | const style = { 72 | backgroundColor: backgroundColor 73 | } 74 | 75 | return ( 76 |
    77 | {message} 78 |
    79 | ); 80 | } 81 | } 82 | 83 | export default Toast; -------------------------------------------------------------------------------- /web-app/src/components/filters/DatePicker.css: -------------------------------------------------------------------------------- 1 | .row:after { 2 | content: ""; 3 | display: table; 4 | clear: both; 5 | } 6 | 7 | .calendar-container { 8 | padding-top: 5px; 9 | } 10 | 11 | .calendar-dialog { 12 | width: 189px; 13 | border: 1px solid #dfe1e6; 14 | position: fixed; 15 | background-color: #FFFFFF; 16 | box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23); 17 | } 18 | 19 | .calendar-cell { 20 | width: 25px; 21 | height: 25px; 22 | line-height: 25px; 23 | float: left; 24 | text-align: center; 25 | } 26 | 27 | .calendar-container .select-date { 28 | -webkit-box-sizing: border-box; 29 | -moz-box-sizing: border-box; 30 | box-sizing: border-box; 31 | box-shadow: none !important; 32 | border: 2px solid #dfe1e6 !important; 33 | padding: 5px 8px; 34 | cursor: pointer; 35 | width: 100%; 36 | background-color: #FFFFFF; 37 | } 38 | 39 | .calendar-container .header-cell { 40 | border: 1px solid #dfe1e6; 41 | background-color: #dfe1e6; 42 | color: #000000; 43 | font-weight: bold; 44 | } 45 | 46 | .calendar-container .body-cell { 47 | border: 1px solid #dfe1e6; 48 | } 49 | 50 | .calendar-container .clickable { 51 | cursor: pointer; 52 | } 53 | 54 | .calendar-container .clickable:hover { 55 | background-color: #0065FF; 56 | color: #FFFFFF; 57 | } 58 | 59 | .calendar-container .non-clickable { 60 | background-color: #dfe1e6; 61 | } 62 | 63 | .calendar-container .cell-active { 64 | background-color: #000000; 65 | color: #FFFFFF; 66 | } 67 | 68 | .calendar-container .display-date { 69 | float: left; 70 | width: 109px; 71 | height: 25px; 72 | text-align: center; 73 | } 74 | 75 | .calendar-container .calendar-button { 76 | color: #0065FF; 77 | width: 40px; 78 | cursor: pointer; 79 | } 80 | 81 | .calendar-container .select-year { 82 | height: 20px; 83 | margin-top: 3px; 84 | border: 1px solid #dfe1e6 !important; 85 | } 86 | 87 | .calendar-overlay { 88 | position: fixed; 89 | top: 0; 90 | left: 0; 91 | right: 0; 92 | bottom: 0; 93 | } -------------------------------------------------------------------------------- /web-app/src/components/filters/InputRange.css: -------------------------------------------------------------------------------- 1 | .input-range-container { 2 | padding: 3px 10px 8px 5px; 3 | } 4 | 5 | .input-range { 6 | -webkit-appearance: none; 7 | width: 100%; 8 | height: 5px; 9 | border-radius: 8px; 10 | background: #cccccc; 11 | outline: none; 12 | opacity: 0.7; 13 | -webkit-transition: .2s; 14 | transition: opacity .2s; 15 | } 16 | 17 | .input-range:hover { 18 | opacity: 1; 19 | } 20 | 21 | .input-range::-webkit-slider-thumb { 22 | appearance: none; 23 | width: 20px; 24 | height: 20px; 25 | border-radius: 50%; 26 | background: #0065FF; 27 | cursor: pointer; 28 | } 29 | 30 | .input-range::-moz-range-thumb { 31 | width: 20px; 32 | height: 20px; 33 | border-radius: 50%; 34 | background: #0065FF; 35 | cursor: pointer; 36 | } 37 | 38 | .input-range-value { 39 | background-color: #373737; 40 | color: #dfdfdf; 41 | padding: 2px 4px; 42 | box-shadow: rgba(50, 50, 93, 0.149) 0px 1px 3px, rgba(0, 0, 0, 0.0196) 0px 1px 0px; 43 | outline: 0; 44 | border-radius: 4px; 45 | font-size: 16px; 46 | } -------------------------------------------------------------------------------- /web-app/src/components/filters/InputRange.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import './InputRange.css'; 4 | 5 | class InputRange extends React.Component { 6 | 7 | constructor(props) { 8 | super(props); 9 | this.state = {}; 10 | } 11 | 12 | static propTypes = { 13 | name: PropTypes.string.isRequired, 14 | min: PropTypes.number.isRequired, 15 | max: PropTypes.number.isRequired, 16 | value: PropTypes.number.isRequired, 17 | onChange: PropTypes.func.isRequired, 18 | step: PropTypes.number.isRequired 19 | }; 20 | 21 | handleInputChange = (event) => { 22 | const { name } = this.props; 23 | const value = Number(event.target.value); 24 | this.props.onChange(name, value); 25 | } 26 | 27 | render() { 28 | const { 29 | name, 30 | min, 31 | max, 32 | value, 33 | step 34 | } = this.props; 35 | 36 | return ( 37 |
    38 |
    39 | {value} 40 |
    41 | 51 |
    52 | ); 53 | } 54 | } 55 | 56 | export default InputRange; -------------------------------------------------------------------------------- /web-app/src/components/filters/Paginator.css: -------------------------------------------------------------------------------- 1 | 2 | .paginator-button { 3 | border: 1px solid black; 4 | padding: 6px 12px; 5 | text-align: center; 6 | text-decoration: none; 7 | display: inline-block; 8 | font-size: 16px; 9 | cursor: pointer; 10 | } -------------------------------------------------------------------------------- /web-app/src/components/filters/Paginator.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import './Paginator.css'; 4 | 5 | class Paginator extends React.Component { 6 | 7 | constructor(props) { 8 | super(props); 9 | this.state = {}; 10 | } 11 | 12 | static propTypes = { 13 | name: PropTypes.string.isRequired, 14 | value: PropTypes.number.isRequired, 15 | min: PropTypes.number, 16 | max: PropTypes.number 17 | }; 18 | 19 | previous = () => { 20 | const { 21 | name, 22 | value, 23 | min = 1 24 | } = this.props; 25 | const current = Number(value); 26 | let prev = current - 1; 27 | if (prev < min) { 28 | prev = current; 29 | } 30 | this.props.onChange(name, prev); 31 | } 32 | 33 | next = () => { 34 | const { 35 | name, 36 | value, 37 | max = 100 38 | } = this.props; 39 | const current = Number(value); 40 | let next = current + 1; 41 | if (next > max) { 42 | next = current; 43 | } 44 | this.props.onChange(name, next); 45 | } 46 | 47 | clickNumber = (value) => { 48 | const { 49 | name 50 | } = this.props; 51 | const current = Number(value); 52 | this.props.onChange(name, current); 53 | } 54 | 55 | render() { 56 | const { 57 | value, 58 | min = 1, 59 | max = 100 60 | } = this.props; 61 | 62 | if (max <= min) { 63 | return (
    error: min >= max
    ); 64 | } 65 | 66 | if (value > max || value < min) { 67 | return (
    error: value out of range
    ); 68 | } 69 | 70 | let showPrevious = true; 71 | let showNext = true; 72 | if (value === min) { 73 | showPrevious = false; 74 | } else if (value === max) { 75 | showNext = false; 76 | } 77 | 78 | return ( 79 |
    80 | { showPrevious && ( 81 | 82 | )} 83 | 84 | 85 | 86 | { showNext && ( 87 | 88 | )} 89 |
    90 | ); 91 | } 92 | } 93 | 94 | export default Paginator; -------------------------------------------------------------------------------- /web-app/src/components/filters/Slicer.css: -------------------------------------------------------------------------------- 1 | .slicer-body { 2 | padding-top: 5px; 3 | } 4 | 5 | .slicer-toggle-button { 6 | margin-top: 5px; 7 | background-color: #212121; 8 | color: #FFFFFF; 9 | border-radius: 4px; 10 | border: 0; 11 | height: 20px; 12 | line-height: 20px; 13 | outline: none; 14 | cursor: pointer; 15 | } -------------------------------------------------------------------------------- /web-app/src/components/filters/Slicer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 4 | 5 | import Checkbox from '../Checkbox/Checkbox'; 6 | import './Slicer.css'; 7 | 8 | class Slicer extends React.Component { 9 | 10 | constructor(props) { 11 | super(props); 12 | this.state = { 13 | isSelectAll: false, 14 | searchValue: '' 15 | }; 16 | } 17 | 18 | static propTypes = { 19 | checkBoxes: PropTypes.array.isRequired, 20 | onChange: PropTypes.func.isRequired, 21 | readOnly: PropTypes.bool 22 | }; 23 | 24 | toggleSelectAll = () => { 25 | const newIsSelectAll = !this.state.isSelectAll; 26 | const newCheckBoxes = [...this.props.checkBoxes]; 27 | for (let i = 0; i < newCheckBoxes.length; i++) { 28 | newCheckBoxes[i].isChecked = newIsSelectAll; 29 | } 30 | 31 | this.setState(prevState => ({ 32 | isSelectAll: !prevState.isSelectAll 33 | })); 34 | 35 | this.props.onChange(this.props.id, newCheckBoxes); 36 | } 37 | 38 | handleSearchValueChange = (event) => { 39 | const searchValue = event.target.value; 40 | this.setState({ 41 | searchValue: searchValue 42 | }); 43 | } 44 | 45 | handleCheckBoxChange = (name, isChecked) => { 46 | const newCheckBoxes = [...this.props.checkBoxes]; 47 | const index = newCheckBoxes.findIndex(x => x.value === name); 48 | newCheckBoxes[index].isChecked = isChecked; 49 | this.props.onChange(this.props.id, newCheckBoxes); 50 | } 51 | 52 | render() { 53 | const { 54 | checkBoxes = [], 55 | readOnly = false 56 | } = this.props; 57 | 58 | const { 59 | searchValue, 60 | isSelectAll 61 | } = this.state; 62 | 63 | const checkBoxItems = []; 64 | for (let i = 0; i < checkBoxes.length; i++) { 65 | const { 66 | value, 67 | isChecked 68 | } = checkBoxes[i]; 69 | if (!searchValue || (searchValue && value.includes(searchValue))) { 70 | checkBoxItems.push( 71 | ( 72 | 80 | ) 81 | ) 82 | } 83 | } 84 | return ( 85 |
    86 | {/* 87 | 95 | 104 | */} 105 |
    106 | {checkBoxItems} 107 |
    108 |
    109 | ); 110 | } 111 | } 112 | 113 | export default Slicer; 114 | -------------------------------------------------------------------------------- /web-app/src/components/processbar/Processbar.css: -------------------------------------------------------------------------------- 1 | .progress-container { 2 | background: #ddd; 3 | overflow: hidden; 4 | position: relative; 5 | width: inherit; 6 | height: 22px; 7 | } 8 | 9 | .progress-bar { 10 | background: #f80; 11 | left: 0; 12 | position: absolute; 13 | top: 0; 14 | height: 22px; 15 | } 16 | 17 | .progress-value { 18 | color: green; 19 | position: absolute; 20 | top: 0px; 21 | right: 8px; 22 | } -------------------------------------------------------------------------------- /web-app/src/components/processbar/Processbar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import './Processbar.css'; 4 | 5 | class Processbar extends React.Component { 6 | 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | } 11 | } 12 | 13 | static propTypes = { 14 | value: PropTypes.number.isRequired, 15 | percentage: PropTypes.number.isRequired, 16 | fontColor: PropTypes.string, 17 | barColor: PropTypes.string, 18 | backgroundColor: PropTypes.string 19 | }; 20 | 21 | componentDidMount() { 22 | } 23 | 24 | 25 | render() { 26 | const { 27 | value, 28 | percentage, 29 | fontColor = 'green', 30 | barColor = '#f80', 31 | backgroundColor = '#ddd' 32 | } = this.props; 33 | 34 | const containerStyle = { 35 | backgroundColor: backgroundColor 36 | }; 37 | 38 | const progressBarStyle = { 39 | width: percentage + '%', 40 | backgroundColor: barColor 41 | }; 42 | 43 | const valueStyle = { 44 | color: fontColor 45 | }; 46 | 47 | return ( 48 |
    49 |
    50 |
    {value}
    51 |
    52 | ); 53 | } 54 | } 55 | 56 | export default Processbar; 57 | -------------------------------------------------------------------------------- /web-app/src/components/table/Table.css: -------------------------------------------------------------------------------- 1 | /** ReactTable */ 2 | .ReactTable { 3 | font-size: 15px !important; 4 | } 5 | 6 | .ReactTable .rt-table { 7 | background-color: #FFFFFF; 8 | } 9 | 10 | .ReactTable .rt-thead.-header { 11 | box-shadow: none !important; 12 | border-bottom: 1px solid #B3BAC5; 13 | } 14 | 15 | .ReactTable .rt-tbody .rt-td { 16 | padding: 5px 3px; 17 | } 18 | 19 | .ReactTable .-pagination { 20 | width: 400px !important; 21 | box-shadow: none !important; 22 | border-top: none !important; 23 | float: right; 24 | margin-right: 5px; 25 | font-size: 12px !important; 26 | padding: 0px !important; 27 | } 28 | 29 | 30 | .ReactTable .pagination-bottom { 31 | border-top: 1px solid #B3BAC5; 32 | background-color: #FFFFFF; 33 | height: 35px; 34 | } 35 | 36 | .ReactTable .-pagination .-previous { 37 | width: 60px !important; 38 | flex: none !important; 39 | height: 26px; 40 | margin-top: 4px; 41 | } 42 | 43 | .ReactTable .-pagination .-center { 44 | width: 270px !important; 45 | flex: none !important; 46 | } 47 | 48 | .ReactTable .-pagination .-next { 49 | width: 60px !important; 50 | flex: none !important; 51 | height: 26px; 52 | margin-top: 4px; 53 | } 54 | 55 | .ReactTable .-pagination .-pageInfo { 56 | margin: 3px 0px 3px 0px !important; 57 | } 58 | 59 | .ReactTable .-pagination .-pageSizeOptions { 60 | margin: 3px 0px 3px 0px !important; 61 | } 62 | 63 | .ReactTable .-pagination .-btn { 64 | border: 0; 65 | outline: none; 66 | cursor: pointer; 67 | color: #000000 !important; 68 | background-color: #FFFFFF !important; 69 | } 70 | 71 | .ReactTable .rt-tbody .rt-tr-group:last-child { 72 | border-bottom: solid 1px rgba(0,0,0,0.05) !important; 73 | } 74 | 75 | .ReactTable .rt-thead .rt-th.-sort-asc, 76 | .ReactTable .rt-thead .rt-td.-sort-asc { 77 | box-shadow: inset 0 3px 0 0 #0065FF; 78 | } 79 | 80 | .ReactTable .rt-thead .rt-th.-sort-desc, 81 | .ReactTable .rt-thead .rt-td.-sort-desc { 82 | box-shadow: inset 0 -3px 0 0 #0065FF; 83 | } 84 | 85 | .ReactTable .rt-resizer { 86 | z-index: auto !important; 87 | } -------------------------------------------------------------------------------- /web-app/src/components/table/Table.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import ReactTable from 'react-table'; 4 | import 'react-table/react-table.css'; 5 | import './Table.css'; 6 | 7 | class Table extends React.PureComponent { 8 | 9 | static propTypes = { 10 | data: PropTypes.array.isRequired, 11 | columns: PropTypes.array.isRequired, 12 | defaultPageSize: PropTypes.number, 13 | drillThrough: PropTypes.array, 14 | showPagination: PropTypes.bool, 15 | height: PropTypes.number, 16 | errorMsg: PropTypes.string 17 | }; 18 | 19 | handleTdClick = (reportId, columnName, columnValue) => { 20 | this.props.onTableTdClick(reportId, columnName, columnValue); 21 | } 22 | 23 | render() { 24 | const { 25 | data = [], 26 | columns = [], 27 | drillThrough = [], 28 | defaultPageSize = 10, 29 | showPagination = true, 30 | height, 31 | errorMsg 32 | } = this.props; 33 | 34 | const columnHeaders = []; 35 | columns.forEach(column => { 36 | const columnName = column.name; 37 | const header = { 38 | Header: columnName, 39 | accessor: columnName 40 | }; 41 | if (drillThrough.length > 0) { 42 | const index = drillThrough.findIndex(d => d.columnName === columnName); 43 | if (index !== -1) { 44 | const reportId = drillThrough[index].reportId; 45 | header.Cell = (props => 46 | this.handleTdClick(reportId, columnName, props.value)}> 48 | {props.value} 49 | 50 | ); 51 | } 52 | } 53 | 54 | columnHeaders.push(header); 55 | }); 56 | 57 | if (errorMsg) { 58 | return ( 59 |
    {errorMsg}
    60 | ); 61 | } 62 | 63 | if (data.length === 0 || columns.length === 0) { 64 | return ( 65 |
    No data
    66 | ); 67 | } 68 | 69 | const pageSize = showPagination ? undefined : data.length; 70 | const style = height ? { 71 | height: height + 'px' 72 | } : {}; 73 | 74 | return ( 75 | 85 | ); 86 | } 87 | } 88 | 89 | export default Table; 90 | -------------------------------------------------------------------------------- /web-app/src/components/widgets/Card.css: -------------------------------------------------------------------------------- 1 | .card-container { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | height: 100%; 6 | } 7 | 8 | .card-value { 9 | text-overflow: ellipsis; 10 | white-space: nowrap; 11 | overflow: hidden; 12 | } -------------------------------------------------------------------------------- /web-app/src/components/widgets/Card.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import './Card.css'; 4 | 5 | class Card extends React.Component { 6 | 7 | static propTypes = { 8 | value: PropTypes.string.isRequired, 9 | fontSize: PropTypes.string, 10 | fontColor: PropTypes.string 11 | }; 12 | 13 | render() { 14 | const { 15 | value, 16 | fontSize = 16, 17 | fontColor = '#000000' 18 | } = this.props; 19 | 20 | const valueStyle = { 21 | fontSize: fontSize + 'px', 22 | color: fontColor 23 | } 24 | 25 | return ( 26 |
    27 |
    28 | {value} 29 |
    30 |
    31 | ); 32 | } 33 | } 34 | 35 | export default Card; -------------------------------------------------------------------------------- /web-app/src/components/widgets/Iframe.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | class Iframe extends React.Component { 5 | 6 | static propTypes = { 7 | src: PropTypes.string.isRequired, 8 | title: PropTypes.string.isRequired 9 | }; 10 | 11 | render() { 12 | const { 13 | src, 14 | title 15 | } = this.props; 16 | 17 | const style = { 18 | width: '100%', 19 | height: '100%', 20 | border: '0px', 21 | display: 'block' 22 | } 23 | 24 | return ( 25 | 26 | ); 27 | } 28 | } 29 | 30 | export default Iframe; -------------------------------------------------------------------------------- /web-app/src/components/widgets/ImageBox.css: -------------------------------------------------------------------------------- 1 | .image-original-scale { 2 | width: 100%; 3 | height: 100%; 4 | object-fit: contain; 5 | display: block; 6 | } 7 | 8 | .image-full { 9 | width: 100%; 10 | height: 100%; 11 | display: block; 12 | } -------------------------------------------------------------------------------- /web-app/src/components/widgets/ImageBox.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import './ImageBox.css'; 4 | 5 | class ImageBox extends React.Component { 6 | 7 | static propTypes = { 8 | src: PropTypes.string.isRequired, 9 | isFull: PropTypes.bool 10 | }; 11 | 12 | render() { 13 | const { 14 | isFull = false, 15 | src 16 | } = this.props; 17 | 18 | const imageClass = isFull ? 'image-full' : 'image-original-scale'; 19 | 20 | return ( 21 | not available 26 | ); 27 | } 28 | } 29 | 30 | export default ImageBox; -------------------------------------------------------------------------------- /web-app/src/components/widgets/InnerHtml.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | class InnerHtml extends React.Component { 5 | 6 | static propTypes = { 7 | html: PropTypes.string.isRequired, 8 | style: PropTypes.object 9 | }; 10 | 11 | render() { 12 | const { 13 | html, 14 | style 15 | } = this.props; 16 | 17 | const markup = { 18 | __html: html 19 | } 20 | 21 | return ( 22 |
    23 | ); 24 | } 25 | } 26 | 27 | export default InnerHtml; -------------------------------------------------------------------------------- /web-app/src/components/widgets/TextBox.css: -------------------------------------------------------------------------------- 1 | .text-box { 2 | word-wrap: break-word; 3 | overflow-y: auto; 4 | height: 100%; 5 | padding: 0px 5px; 6 | text-align: center; 7 | } -------------------------------------------------------------------------------- /web-app/src/components/widgets/TextBox.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import './TextBox.css'; 4 | 5 | class TextBox extends React.Component { 6 | 7 | static propTypes = { 8 | fontSize: PropTypes.string.isRequired, 9 | fontColor: PropTypes.string.isRequired, 10 | value: PropTypes.string.isRequired 11 | }; 12 | 13 | render() { 14 | const { 15 | fontSize = 16, 16 | fontColor = '#000000', 17 | isLink = false, 18 | value, 19 | linkUrl 20 | } = this.props; 21 | 22 | const style = { 23 | fontSize: fontSize + 'px', 24 | color: fontColor 25 | } 26 | 27 | return ( 28 |
    29 | { isLink ? 30 | ( 31 | {value} 32 | ) : 33 | ( 34 | value 35 | ) 36 | } 37 |
    38 | ); 39 | } 40 | } 41 | 42 | export default TextBox; -------------------------------------------------------------------------------- /web-app/src/i18n.js: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next'; 2 | import { initReactI18next } from 'react-i18next'; 3 | 4 | import translationEN from './locales/en/translation'; 5 | import translationZH from './locales/zh/translation'; 6 | import translationES from './locales/es/translation'; 7 | import translationFR from './locales/fr/translation'; 8 | 9 | // the translations 10 | const resources = { 11 | en: { 12 | translation: translationEN 13 | }, 14 | zh: { 15 | translation: translationZH 16 | }, 17 | es: { 18 | translation: translationES 19 | }, 20 | fr: { 21 | translation: translationFR 22 | } 23 | }; 24 | 25 | i18n 26 | .use(initReactI18next) 27 | .init({ 28 | resources: resources, 29 | lng: 'en', 30 | fallbackLng: 'en', 31 | interpolation: { 32 | escapeValue: false 33 | } 34 | }); 35 | 36 | export default i18n; -------------------------------------------------------------------------------- /web-app/src/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | #root, 4 | .app { 5 | width: 100%; 6 | height: 100%; 7 | margin: 0; 8 | padding: 0; 9 | font-family: Tahoma, Geneva, sans-serif; 10 | color: #212121; 11 | -webkit-font-smoothing: antialiased; 12 | text-rendering: optimizeLegibility; 13 | -webkit-transition: all 0.15s ease; 14 | transition: all 0.15s ease; 15 | } 16 | 17 | textarea, 18 | input, 19 | button, 20 | select { 21 | font-family: inherit; 22 | font-size: inherit; 23 | box-shadow: rgba(50, 50, 93, 0.149) 0px 1px 3px, rgba(0, 0, 0, 0.0196) 0px 1px 0px; 24 | } 25 | 26 | textarea { 27 | resize: vertical; 28 | } 29 | 30 | /* Toastify */ 31 | .Toastify__toast-body { 32 | white-space: pre-wrap; 33 | word-wrap: break-word; 34 | width: 260px; 35 | } 36 | 37 | /* Echarts */ 38 | .echarts { 39 | width: 100% !important; 40 | height: 100% !important; 41 | } 42 | 43 | /* Utility */ 44 | .display-flex { 45 | display: flex; 46 | } 47 | 48 | .row:after { 49 | content: ""; 50 | display: table; 51 | clear: both; 52 | } 53 | 54 | .float-left { 55 | float: left; 56 | } 57 | 58 | .float-right { 59 | float: right; 60 | } 61 | 62 | .inline-block { 63 | display: inline-block; 64 | } 65 | 66 | .ellipsis { 67 | text-overflow: ellipsis; 68 | white-space: nowrap; 69 | overflow: hidden; 70 | } 71 | 72 | .display-block { 73 | display: block; 74 | } 75 | 76 | .display-none { 77 | display: none; 78 | } 79 | 80 | .word-break-all { 81 | word-break: break-all; 82 | } 83 | 84 | hr { 85 | display: block; 86 | height: 1px; 87 | border: 0; 88 | border-top: 1px solid #ccc; 89 | margin: 8px 0; 90 | padding: 0; 91 | } 92 | 93 | .bold { 94 | font-weight: 700; 95 | } 96 | 97 | .cursor-pointer { 98 | cursor: pointer; 99 | } 100 | 101 | .full-width { 102 | width: 100%; 103 | } 104 | 105 | /* Scroll bar */ 106 | ::-webkit-scrollbar { 107 | width: 10px; 108 | height: 10px; 109 | } 110 | 111 | ::-webkit-scrollbar-thumb { 112 | background: #bdbdbd; 113 | border: 0px none transparent; 114 | } 115 | 116 | ::-webkit-scrollbar-thumb:hover { 117 | background: #bdbdbd; 118 | } 119 | 120 | ::-webkit-scrollbar-thumb:active { 121 | background: #bdbdbd; 122 | } 123 | 124 | ::-webkit-scrollbar-track { 125 | background: transparent; 126 | border: 0px none transparent; 127 | } 128 | 129 | ::-webkit-scrollbar-track:hover { 130 | background: transparent; 131 | } 132 | 133 | ::-webkit-scrollbar-track:active { 134 | background: transparent; 135 | } 136 | 137 | ::-webkit-scrollbar-corner { 138 | background: transparent; 139 | } -------------------------------------------------------------------------------- /web-app/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import { BrowserRouter } from "react-router-dom"; 5 | import './i18n'; 6 | import './index.css'; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | , 12 | document.getElementById('root') 13 | ); 14 | 15 | -------------------------------------------------------------------------------- /web-app/src/views/Login/Login.css: -------------------------------------------------------------------------------- 1 | .login-view { 2 | } 3 | 4 | .login-input { 5 | padding: 7px 10px !important; 6 | border: 2px solid #FFFFFF !important; 7 | } 8 | 9 | .login-input:focus { 10 | border: 2px solid #bdbdbd !important; 11 | } 12 | 13 | .login-panel { 14 | padding: 15px; 15 | position: fixed; 16 | top: 50%; 17 | left: 50%; 18 | width: 300px; 19 | -webkit-transform: translate(-50%, -50%); 20 | transform: translate(-50%, -50%); 21 | } 22 | 23 | .login-panel-body { 24 | padding: 15px; 25 | background-color:#eeeeee; 26 | border-radius: 5px; 27 | } 28 | 29 | .login-app-title { 30 | font-size: 35px; 31 | text-align: center; 32 | font-weight: 700; 33 | margin-bottom: 25px; 34 | color: #0065FF; 35 | } 36 | 37 | .login-button { 38 | width: 100%; 39 | } 40 | 41 | .login-error-msg { 42 | text-align: center; 43 | margin-bottom: 5px; 44 | color: #ef5350; 45 | font-weight: bold; 46 | } 47 | 48 | .version-number { 49 | position: absolute; 50 | bottom: 5px; 51 | right: 10px; 52 | font-size: 12px; 53 | } -------------------------------------------------------------------------------- /web-app/src/views/PageNotFound.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import { Link } from "react-router-dom"; 4 | 5 | class PageNotFound extends React.Component { 6 | render() { 7 | return ( 8 | 9 |

    PageNotFound

    10 | Login 11 |
    12 | ) 13 | }; 14 | } 15 | 16 | export default PageNotFound; -------------------------------------------------------------------------------- /web-app/src/views/Report/ComponentEditPanel.css: -------------------------------------------------------------------------------- 1 | 2 | .schema-table-title { 3 | padding: 4px 8px; 4 | cursor: pointer; 5 | margin-bottom: 3px; 6 | border: 0; 7 | outline: 0; 8 | border-radius: 4px; 9 | background-color: #FFFFFF; 10 | box-shadow: rgba(50, 50, 93, 0.149) 0px 1px 3px, rgba(0, 0, 0, 0.0196) 0px 1px 0px; 11 | font-weight: 700; 12 | } 13 | 14 | .schema-column-row { 15 | padding: 4px 8px 4px 20px; 16 | } 17 | 18 | .schema-column-type { 19 | color: #8993A4; 20 | } 21 | 22 | .tag-label { 23 | padding: 2px 12px; 24 | background-color: #8993A4; 25 | color: #FFFFFF; 26 | display: inline-block; 27 | } 28 | 29 | .grid-label { 30 | line-height: 32px; 31 | margin-right: 5px; 32 | } 33 | 34 | .grid-input { 35 | width: 50px; 36 | margin-right: 8px; 37 | text-align: right; 38 | } -------------------------------------------------------------------------------- /web-app/src/views/Report/ComponentViewPanel.css: -------------------------------------------------------------------------------- 1 | .report-component-style-panel { 2 | position: fixed; 3 | top: 40px; 4 | left: 0px; 5 | bottom: 0px; 6 | width: 200px; 7 | border: 0; 8 | outline: 0; 9 | z-index: 30; 10 | overflow-y: auto; 11 | background-color: #4B4B4B; 12 | color: #DFDFDF; 13 | z-index: 70; 14 | } -------------------------------------------------------------------------------- /web-app/src/views/Report/Report.css: -------------------------------------------------------------------------------- 1 | .report-sidebar { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | bottom: 0; 6 | width: 199px; 7 | overflow-y: auto; 8 | border-right: 1px solid #ccc; 9 | background-color: #efefef; 10 | } 11 | 12 | .report-content { 13 | position: absolute; 14 | top: 0; 15 | left: 200px; 16 | right: 0; 17 | bottom: 0; 18 | background-color: #FFFFFF; 19 | overflow: auto; 20 | } 21 | 22 | .report-menu-panel { 23 | padding: 5px 10px; 24 | } 25 | 26 | .report-menu-item { 27 | border-left: 3px solid #EBECF0; 28 | margin: 3px 0px; 29 | padding: 3px 6px; 30 | color: #8993A4; 31 | cursor: pointer; 32 | } 33 | 34 | .report-menu-item:hover { 35 | border-left: 3px solid #0065FF; 36 | } 37 | 38 | .report-menu-item-active { 39 | border-left: 3px solid #0065FF; 40 | color: #0065FF; 41 | font-weight: 700; 42 | } 43 | 44 | .report-content-component-panel { 45 | position: absolute; 46 | top: 0; 47 | left: 0; 48 | bottom: 0; 49 | right: 0; 50 | overflow: auto; 51 | padding: 0px 10px 10px 20px; 52 | } 53 | 54 | .report-drillthrough-name { 55 | padding: 6px 0px 6px 14px; 56 | font-size: 20px; 57 | } 58 | 59 | .report-name { 60 | padding: 6px 14px; 61 | font-size: 20px; 62 | } 63 | 64 | .report-name-input { 65 | margin-left: 10px; 66 | } 67 | 68 | .report-edit-component-dialog { 69 | left: 50%; 70 | width: 800px; 71 | top: 20px; 72 | bottom: 20px; 73 | margin-left: -400px; 74 | } 75 | 76 | .report-edit-filter-dialog { 77 | left: 50%; 78 | width: 1000px; 79 | top: 20px; 80 | bottom: 20px; 81 | margin-left: -500px; 82 | } 83 | 84 | .empty-report { 85 | margin-top: 20px; 86 | text-align: center; 87 | color:#0065FF; 88 | font-size: 20px; 89 | font-weight: 700; 90 | } 91 | 92 | .report-side-panel { 93 | position: fixed; 94 | top: 40px; 95 | left: 0px; 96 | bottom: 0px; 97 | width: 200px; 98 | border: 0; 99 | outline: 0; 100 | overflow-y: auto; 101 | background-color: #4B4B4B; 102 | color: #DFDFDF; 103 | z-index: 60; 104 | } 105 | 106 | .project-row { 107 | margin: 5px 0px; 108 | padding: 3px 0px; 109 | cursor: pointer; 110 | } 111 | 112 | .exporting-panel { 113 | position: fixed; 114 | top: 50%; 115 | left: 50%; 116 | width: 160px; 117 | height: 90px; 118 | margin-top: -45px; 119 | margin-left: -80px; 120 | text-align: center; 121 | } 122 | 123 | .exporting-panel-title { 124 | font-size: 22px; 125 | font-weight: 700; 126 | margin: 0px 0px 15px 0px; 127 | color: #FFFFFF; 128 | } 129 | 130 | .exporting-overlay { 131 | background: rgba(0, 0, 0, 0.6); 132 | position: fixed; 133 | top: 0; 134 | left: 0; 135 | right: 0; 136 | bottom: 0; 137 | z-index: 150; 138 | } -------------------------------------------------------------------------------- /web-app/src/views/ReportFullScreenView.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import ReportEditView from './Report/ReportEditView'; 4 | 5 | class ReportFullScreenView extends React.Component { 6 | 7 | render() { 8 | const href = window.location.href; 9 | 10 | return ( 11 |
    12 | 13 |
    14 | ); 15 | }; 16 | } 17 | 18 | export default ReportFullScreenView; 19 | -------------------------------------------------------------------------------- /web-app/src/views/Studio/ReactSelectHelper.js: -------------------------------------------------------------------------------- 1 | export const CUSTOM_STYLE = { 2 | control: (provided, state) => ({ 3 | ...provided, 4 | background: '#ffffff', 5 | borderColor: state.isSelected ? '#bdbdbd' : '#e0e0e0', 6 | minHeight: '32px', 7 | height: '32px', 8 | boxShadow: 'rgba(50, 50, 93, 0.149) 0px 1px 3px, rgba(0, 0, 0, 0.0196) 0px 1px 0px', 9 | '&:hover': { 10 | borderColor: state.isFocused ? '#bdbdbd' : '#e0e0e0' 11 | } 12 | }), 13 | valueContainer: (provided, state) => ({ 14 | ...provided, 15 | height: '32px', 16 | padding: '0 6px' 17 | }), 18 | indicatorsContainer: (provided, state) => ({ 19 | ...provided, 20 | height: '32px', 21 | }), 22 | singleValue: (provided, state) => ({ 23 | ...provided, 24 | top: '45%' 25 | }), 26 | placeholder: (provided, state) => ({ 27 | ...provided, 28 | top: '45%' 29 | }), 30 | menu: (provided, state) => ({ 31 | ...provided, 32 | marginTop: '0px', 33 | zIndex: '100' 34 | }), 35 | option: (provided, state) => ({ 36 | ...provided, 37 | color: state.isSelected ? '#FFFFFF' : '#212121', 38 | backgroundColor: state.isSelected ? '#212121' : '#FFFFFF', 39 | padding: '6px 8px', 40 | '&:hover': { 41 | backgroundColor: '#C1C7D0', 42 | color: '#FFFFFF' 43 | } 44 | }) 45 | } -------------------------------------------------------------------------------- /web-app/src/views/Studio/SchemaPanel.css: -------------------------------------------------------------------------------- 1 | .schema-side-panel { 2 | position: absolute; 3 | top: 0; 4 | right: 0; 5 | bottom: 0; 6 | z-index: 500; 7 | width: 449px; 8 | min-width: 249px; 9 | overflow-y: auto; 10 | border-left: 1px solid #ccc; 11 | background-color: #efefef; 12 | padding: 5px; 13 | } 14 | 15 | .schema-title-row { 16 | padding: 4px 8px; 17 | cursor: pointer; 18 | margin-bottom: 3px; 19 | border: 0; 20 | outline: 0; 21 | border-radius: 4px; 22 | background-color: #FFFFFF; 23 | box-shadow: rgba(50, 50, 93, 0.149) 0px 1px 3px, rgba(0, 0, 0, 0.0196) 0px 1px 0px; 24 | } 25 | 26 | .schema-column-row-type { 27 | color: #8993A4; 28 | } -------------------------------------------------------------------------------- /web-app/src/views/Studio/ScrollTabPanel.css: -------------------------------------------------------------------------------- 1 | .dynamic-tab-container { 2 | box-sizing: border-box; 3 | height: 40px; 4 | position: relative; 5 | border-bottom: 1px solid #ccc; 6 | margin-top: 3px; 7 | } 8 | 9 | .dynamic-tab-button-group { 10 | position: absolute; 11 | top: 0; 12 | left: 8px; 13 | width: 120px; 14 | line-height: 40px; 15 | height: 40px; 16 | } 17 | 18 | .dynamic-tab-droppable-container { 19 | position: absolute; 20 | top: 0; 21 | left: 120px; 22 | right: 0px; 23 | } 24 | 25 | .dynamic-tab-content-container { 26 | flex: 1 0 auto; 27 | display: flex; 28 | flex-direction: row; 29 | } 30 | 31 | .dynamic-tab-select-item { 32 | display: flex; 33 | height: 33px; 34 | line-height: 33px; 35 | } 36 | 37 | .dynamic-tab-select-value { 38 | width: 100px; 39 | text-overflow: ellipsis; 40 | white-space: nowrap; 41 | overflow: hidden; 42 | } 43 | 44 | .dynamic-tab-close-button { 45 | border-radius: 4px; 46 | height: 33px; 47 | width: 28px; 48 | outline: none; 49 | padding: 0px !important; 50 | cursor: pointer; 51 | box-shadow: none !important; 52 | border: 0 !important; 53 | background-color: transparent !important; 54 | } -------------------------------------------------------------------------------- /web-app/src/views/Studio/Studio.css: -------------------------------------------------------------------------------- 1 | .studio-container { 2 | box-sizing: border-box; 3 | display: flex; 4 | flex-direction: row; 5 | height: 100%; 6 | background-color: #FFFFFF; 7 | } 8 | 9 | .studio-menu-sidebar { 10 | width: 199px; 11 | min-width: 199px; 12 | overflow-y: auto; 13 | border-right: 1px solid #ccc; 14 | background-color: #efefef; 15 | } 16 | 17 | .studio-property-sidebar { 18 | width: 199px; 19 | min-width: 199px; 20 | overflow-y: auto; 21 | border-right: 1px solid #ccc; 22 | background-color: #efefef; 23 | } 24 | 25 | .studio-property-panel { 26 | border-bottom: 1px solid #ccc; 27 | background-color: #FFFFFF; 28 | padding: 5px 8px 0px 8px; 29 | } 30 | 31 | .studio-body { 32 | flex: 1 1 auto; 33 | display: flex; 34 | flex-direction: column; 35 | overflow: hidden; 36 | } 37 | 38 | .studio-editor-container { 39 | height: 320px; 40 | } 41 | 42 | .studio-result-control-container { 43 | border-top: 1px solid #ccc; 44 | border-bottom: 1px solid #ccc; 45 | padding: 5px 8px; 46 | display: flex; 47 | justify-content: space-between; 48 | } 49 | 50 | .studio-result-container { 51 | overflow-y: auto; 52 | } 53 | 54 | .studio-datasource-container { 55 | padding: 5px 8px; 56 | border-bottom: 1px solid #ccc; 57 | display: flex; 58 | } 59 | 60 | .studio-datasource-select { 61 | width: 300px; 62 | } 63 | 64 | .studio-query-menu-item { 65 | border-left: 3px solid #EBECF0; 66 | margin: 5px 0px; 67 | padding: 3px 3px 3px 8px; 68 | font-weight: 700; 69 | color: #8993A4; 70 | cursor: pointer; 71 | } 72 | 73 | .studio-query-menu-item:hover { 74 | border-left: 3px solid #0065FF; 75 | } 76 | 77 | .studio-query-menu-item-active { 78 | border-left: 3px solid #0065FF; 79 | color: #0065FF; 80 | } -------------------------------------------------------------------------------- /web-app/src/views/Workspace.css: -------------------------------------------------------------------------------- 1 | .workspace-name { 2 | display: block; 3 | color: #0065FF; 4 | padding: 7px 16px; 5 | font-size: 22px; 6 | font-weight: 700; 7 | float: left; 8 | margin-right: 6px; 9 | } 10 | 11 | .workspace-nav { 12 | width: 100%; 13 | height: 40px; 14 | background-color: #212121; 15 | box-sizing: border-box; 16 | } 17 | 18 | .workspace-nav-menu { 19 | list-style-type: none; 20 | margin: 0; 21 | padding: 0; 22 | overflow: hidden; 23 | float: left; 24 | } 25 | 26 | .workspace-nav-menu-text { 27 | margin-left: 6px; 28 | } 29 | 30 | .workspace-nav-menu-item { 31 | display: inline-block; 32 | list-style: none; 33 | font-size: 16px; 34 | } 35 | 36 | .workspace-nav-menu-item-value { 37 | display: block; 38 | color: #FFFFFF; 39 | padding: 11px 9px 10px 9px; 40 | cursor: pointer; 41 | } 42 | 43 | .workspace-nav-menu-item-dropdown { 44 | position: absolute; 45 | z-index: 1; 46 | background-color: #212121; 47 | min-width: 100px; 48 | } 49 | 50 | .workspace-nav-menu-item-value:hover { 51 | background-color: #003ccb; 52 | color: #FFFFFF !important; 53 | } 54 | 55 | .menu-item-active { 56 | background-color: #0065FF; 57 | color: #FFFFFF !important; 58 | } 59 | 60 | .workspace-content { 61 | position: fixed; 62 | top: 40px; 63 | left: 0px; 64 | right: 0; 65 | bottom: 0; 66 | background-color: #efefef; 67 | overflow-x: hidden; 68 | overflow-y: auto; 69 | } 70 | 71 | .workspace-account-menu { 72 | float: right; 73 | } 74 | 75 | .workspace-account-button { 76 | display: inline-block; 77 | color: #FFFFFF; 78 | padding: 11px 10px 10px 10px; 79 | text-decoration: none; 80 | cursor: pointer; 81 | } 82 | 83 | .workspace-account-button:hover { 84 | background-color: #003ccb; 85 | color: #FFFFFF !important; 86 | } 87 | 88 | .workspace-account-dropdown { 89 | position: absolute; 90 | right: 0px; 91 | z-index: 1; 92 | background-color: #212121; 93 | width: 80px; 94 | } 95 | 96 | .workspace-dropdown-button { 97 | color: #FFFFFF; 98 | cursor: pointer; 99 | padding: 8px 12px; 100 | } 101 | 102 | .workspace-dropdown-button:hover { 103 | background-color: #003ccb; 104 | color: #FFFFFF !important; 105 | } --------------------------------------------------------------------------------