diff --git a/packages/web/package.json b/packages/web/package.json
index c506298b5..79d93dbfd 100644
--- a/packages/web/package.json
+++ b/packages/web/package.json
@@ -10,6 +10,7 @@
"@testing-library/user-event": "^7.1.2",
"ace-builds": "^1.4.8",
"axios": "^0.19.0",
+ "chart.js": "^2.9.4",
"cross-env": "^6.0.3",
"dbgate-datalib": "^1.0.0",
"dbgate-sqltree": "^1.0.0",
@@ -21,6 +22,7 @@
"localforage": "^1.9.0",
"react": "^16.12.0",
"react-ace": "^8.0.0",
+ "react-chartjs-2": "^2.11.1",
"react-dom": "^16.12.0",
"react-dropzone": "^11.2.3",
"react-helmet": "^6.1.0",
diff --git a/packages/web/src/charts/ChartEditor.js b/packages/web/src/charts/ChartEditor.js
new file mode 100644
index 000000000..866cb11a7
--- /dev/null
+++ b/packages/web/src/charts/ChartEditor.js
@@ -0,0 +1,56 @@
+import React from 'react';
+import Chart from 'react-chartjs-2';
+import styled from 'styled-components';
+import useTheme from '../theme/useTheme';
+import useDimensions from '../utility/useDimensions';
+import { HorizontalSplitter } from '../widgets/Splitter';
+import WidgetColumnBar, { WidgetColumnBarItem } from '../widgets/WidgetColumnBar';
+import { FormCheckboxField, FormSelectField } from '../utility/forms';
+import { Formik, Form } from 'formik';
+import DataChart from './DataChart';
+
+const LeftContainer = styled.div`
+ background-color: ${(props) => props.theme.manager_background};
+ display: flex;
+ flex: 1;
+`;
+
+export default function ChartEditor({ data }) {
+ const [managerSize, setManagerSize] = React.useState(0);
+ const theme = useTheme();
+ const availableColumnNames = data ? data.structure.columns.map((x) => x.columnName) : [];
+
+ return (
+ {}}>
+
+
+ );
+}
diff --git a/packages/web/src/charts/DataChart.js b/packages/web/src/charts/DataChart.js
new file mode 100644
index 000000000..9e697315a
--- /dev/null
+++ b/packages/web/src/charts/DataChart.js
@@ -0,0 +1,99 @@
+import React from 'react';
+import _ from 'lodash'
+import Chart from 'react-chartjs-2';
+import styled from 'styled-components';
+import useTheme from '../theme/useTheme';
+import useDimensions from '../utility/useDimensions';
+import { HorizontalSplitter } from '../widgets/Splitter';
+import WidgetColumnBar, { WidgetColumnBarItem } from '../widgets/WidgetColumnBar';
+import { FormSelectField } from '../utility/forms';
+import { Formik, Form, useFormikContext } from 'formik';
+
+const ChartWrapper = styled.div`
+ flex: 1;
+ overflow: hidden;
+`;
+
+const chartData = {
+ labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'],
+ datasets: [
+ {
+ label: '# of Votes',
+ data: [12, 19, 3, 5, 2, 3],
+ backgroundColor: [
+ 'rgba(255, 99, 132, 0.2)',
+ 'rgba(54, 162, 235, 0.2)',
+ 'rgba(255, 206, 86, 0.2)',
+ 'rgba(75, 192, 192, 0.2)',
+ 'rgba(153, 102, 255, 0.2)',
+ 'rgba(255, 159, 64, 0.2)',
+ ],
+ borderColor: [
+ 'rgba(255, 99, 132, 1)',
+ 'rgba(54, 162, 235, 1)',
+ 'rgba(255, 206, 86, 1)',
+ 'rgba(75, 192, 192, 1)',
+ 'rgba(153, 102, 255, 1)',
+ 'rgba(255, 159, 64, 1)',
+ ],
+ borderWidth: 1,
+ },
+ ],
+};
+
+function createChartData(freeData, labelColumn, dataColumns) {
+ if (!freeData || !labelColumn || !dataColumns || dataColumns.length == 0) return {};
+ const labels = _.uniq(freeData.rows.map((x) => x[labelColumn]));
+ const res = {
+ labels: freeData.rows.map((x) => x[labelColumn]),
+ datasets: dataColumns.map((dataColumn) => ({
+ label: dataColumn,
+ // data: [12, 19, 3, 5, 2, 3],
+ data: freeData.rows.map((x) => x[dataColumn]),
+ backgroundColor: [
+ 'rgba(255, 99, 132, 0.2)',
+ 'rgba(54, 162, 235, 0.2)',
+ 'rgba(255, 206, 86, 0.2)',
+ 'rgba(75, 192, 192, 0.2)',
+ 'rgba(153, 102, 255, 0.2)',
+ 'rgba(255, 159, 64, 0.2)',
+ ],
+ borderColor: [
+ 'rgba(255, 99, 132, 1)',
+ 'rgba(54, 162, 235, 1)',
+ 'rgba(255, 206, 86, 1)',
+ 'rgba(75, 192, 192, 1)',
+ 'rgba(153, 102, 255, 1)',
+ 'rgba(255, 159, 64, 1)',
+ ],
+ borderWidth: 1,
+ })),
+ };
+
+ return res;
+}
+
+export default function DataChart({ data }) {
+ const [containerRef, { height: containerHeight, width: containerWidth }] = useDimensions();
+ const { values } = useFormikContext();
+
+ const { labelColumn } = values;
+ const dataColumns = [];
+ for (const key in values) {
+ if (key.startsWith('dataColumn_') && values[key]) {
+ dataColumns.push(key.substring('dataColumn_'.length));
+ }
+ }
+
+ return (
+
+
+
+ );
+}
diff --git a/packages/web/src/datagrid/DataGridContextMenu.js b/packages/web/src/datagrid/DataGridContextMenu.js
index 76b76ec6d..f9b0178d4 100644
--- a/packages/web/src/datagrid/DataGridContextMenu.js
+++ b/packages/web/src/datagrid/DataGridContextMenu.js
@@ -12,6 +12,7 @@ export default function DataGridContextMenu({
filterSelectedValue,
openQuery,
openFreeTable,
+ openChart,
}) {
return (
<>
@@ -53,6 +54,7 @@ export default function DataGridContextMenu({
)}
{openQuery && Open query}
Open selection in free table editor
+ Open chart from selection
>
);
}
diff --git a/packages/web/src/datagrid/DataGridCore.js b/packages/web/src/datagrid/DataGridCore.js
index 2d3cf6146..15aadf5d1 100644
--- a/packages/web/src/datagrid/DataGridCore.js
+++ b/packages/web/src/datagrid/DataGridCore.js
@@ -321,9 +321,18 @@ export default function DataGridCore(props) {
setFirstVisibleColumnScrollIndex(value);
};
- const handleOpenFreeTable = () => {
+ const getSelectedFreeData = () => {
const columns = getSelectedColumns();
const rows = getSelectedRowData().map((row) => _.pickBy(row, (v, col) => columns.find((x) => x.columnName == col)));
+ return {
+ structure: {
+ columns,
+ },
+ rows,
+ };
+ };
+
+ const handleOpenFreeTable = () => {
openNewTab(
setOpenedTabs,
{
@@ -332,11 +341,21 @@ export default function DataGridCore(props) {
tabComponent: 'FreeTableTab',
props: {},
},
+ getSelectedFreeData()
+ );
+ };
+
+ const handleOpenChart = () => {
+ openNewTab(
+ setOpenedTabs,
{
- structure: {
- columns,
- },
- rows,
+ title: 'Chart',
+ icon: 'img chart',
+ tabComponent: 'ChartTab',
+ props: {},
+ },
+ {
+ data: getSelectedFreeData(),
}
);
};
@@ -357,6 +376,7 @@ export default function DataGridCore(props) {
filterSelectedValue={display.filterable ? filterSelectedValue : null}
openQuery={openQuery}
openFreeTable={handleOpenFreeTable}
+ openChart={handleOpenChart}
/>
);
};
diff --git a/packages/web/src/icons.js b/packages/web/src/icons.js
index fe2f6b610..5c647702e 100644
--- a/packages/web/src/icons.js
+++ b/packages/web/src/icons.js
@@ -59,6 +59,7 @@ const iconNames = {
'img foreign-key': 'mdi mdi-key-link',
'img sql-file': 'mdi mdi-file',
'img shell': 'mdi mdi-flash color-blue-7',
+ 'img chart': 'mdi mdi-chart-bar color-magenta-7',
'img free-table': 'mdi mdi-table color-green-7',
'img macro': 'mdi mdi-hammer-wrench',
diff --git a/packages/web/src/tabs/ChartTab.js b/packages/web/src/tabs/ChartTab.js
new file mode 100644
index 000000000..6348c2521
--- /dev/null
+++ b/packages/web/src/tabs/ChartTab.js
@@ -0,0 +1,53 @@
+import React from 'react';
+import { createFreeTableModel } from 'dbgate-datalib';
+import useUndoReducer from '../utility/useUndoReducer';
+import { useSetOpenedTabs } from '../utility/globalState';
+import useGridConfig from '../utility/useGridConfig';
+import FreeTableGrid from '../freetable/FreeTableGrid';
+import SaveArchiveModal from '../modals/SaveArchiveModal';
+import useModalState from '../modals/useModalState';
+import axios from '../utility/axios';
+import LoadingInfo from '../widgets/LoadingInfo';
+import { changeTab } from '../utility/common';
+import ErrorInfo from '../widgets/ErrorInfo';
+import useEditorData from '../utility/useEditorData';
+import SaveTabModal from '../modals/SaveTabModal';
+import ChartEditor from '../charts/ChartEditor';
+
+export default function ChartTab({ tabVisible, toolbarPortalRef, tabid }) {
+ const [config, setConfig] = useGridConfig(tabid);
+ const [modelState, dispatchModel] = useUndoReducer(createFreeTableModel());
+ const saveFileModalState = useModalState();
+ const { initialData, setEditorData, errorMessage, isLoading } = useEditorData({
+ tabid,
+ });
+
+ React.useEffect(() => {
+ // @ts-ignore
+ if (initialData) dispatchModel({ type: 'reset', value: initialData });
+ }, [initialData]);
+
+ React.useEffect(() => {
+ setEditorData(modelState.value);
+ }, [modelState]);
+
+ if (isLoading) {
+ return ;
+ }
+ if (errorMessage) {
+ return ;
+ }
+
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/packages/web/src/tabs/index.js b/packages/web/src/tabs/index.js
index e77d0bc3e..37feccef3 100644
--- a/packages/web/src/tabs/index.js
+++ b/packages/web/src/tabs/index.js
@@ -7,6 +7,7 @@ import InfoPageTab from './InfoPageTab';
import ArchiveFileTab from './ArchiveFileTab';
import FreeTableTab from './FreeTableTab';
import PluginTab from './PluginTab';
+import ChartTab from './ChartTab';
export default {
TableDataTab,
@@ -18,4 +19,5 @@ export default {
ArchiveFileTab,
FreeTableTab,
PluginTab,
+ ChartTab,
};
diff --git a/packages/web/src/utility/useDimensions.js b/packages/web/src/utility/useDimensions.js
index 62f83e2e9..867b90c52 100644
--- a/packages/web/src/utility/useDimensions.js
+++ b/packages/web/src/utility/useDimensions.js
@@ -51,7 +51,7 @@ import ResizeObserver from 'resize-observer-polyfill';
// Export hook
export default function useDimensions(dependencies = []) {
const [node, setNode] = useState(null);
- const ref = useCallback(newNode => {
+ const ref = useCallback((newNode) => {
setNode(newNode);
}, []);
@@ -68,7 +68,7 @@ export default function useDimensions(dependencies = []) {
});
// Define measure function
- const measure = useCallback(innerNode => {
+ const measure = useCallback((innerNode) => {
const rect = innerNode.getBoundingClientRect();
setDimensions({
x: rect.left,
diff --git a/yarn.lock b/yarn.lock
index 6a64ad2f3..638bcf4b2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2971,6 +2971,29 @@ chardet@^0.7.0:
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
+chart.js@^2.9.4:
+ version "2.9.4"
+ resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.9.4.tgz#0827f9563faffb2dc5c06562f8eb10337d5b9684"
+ integrity sha512-B07aAzxcrikjAPyV+01j7BmOpxtQETxTSlQ26BEYJ+3iUkbNKaOJ/nDbT6JjyqYxseM0ON12COHYdU2cTIjC7A==
+ dependencies:
+ chartjs-color "^2.1.0"
+ moment "^2.10.2"
+
+chartjs-color-string@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz#1df096621c0e70720a64f4135ea171d051402f71"
+ integrity sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==
+ dependencies:
+ color-name "^1.0.0"
+
+chartjs-color@^2.1.0:
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/chartjs-color/-/chartjs-color-2.4.1.tgz#6118bba202fe1ea79dd7f7c0f9da93467296c3b0"
+ integrity sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==
+ dependencies:
+ chartjs-color-string "^0.6.0"
+ color-convert "^1.9.3"
+
chokidar@^2.0.2, chokidar@^2.1.8:
version "2.1.8"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917"
@@ -3144,7 +3167,7 @@ collection-visit@^1.0.0:
map-visit "^1.0.0"
object-visit "^1.0.0"
-color-convert@^1.9.0, color-convert@^1.9.1:
+color-convert@^1.9.0, color-convert@^1.9.1, color-convert@^1.9.3:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
@@ -7364,6 +7387,11 @@ lodash.uniq@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
+lodash@^4.17.19:
+ version "4.17.20"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
+ integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
+
loglevel@^1.6.4:
version "1.6.6"
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.6.tgz#0ee6300cc058db6b3551fa1c4bf73b83bb771312"
@@ -7755,6 +7783,11 @@ mkdirp@^1.0.3:
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
+moment@^2.10.2:
+ version "2.29.1"
+ resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
+ integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
+
moment@^2.24.0:
version "2.24.0"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
@@ -9675,6 +9708,14 @@ react-base16-styling@^0.6.0:
lodash.flow "^3.3.0"
pure-color "^1.2.0"
+react-chartjs-2@^2.11.1:
+ version "2.11.1"
+ resolved "https://registry.yarnpkg.com/react-chartjs-2/-/react-chartjs-2-2.11.1.tgz#a78d0df05fc8bc8ffcd4c4ab5b89a25dd2ca3278"
+ integrity sha512-G7cNq/n2Bkh/v4vcI+GKx7Q1xwZexKYhOSj2HmrFXlvNeaURWXun6KlOUpEQwi1cv9Tgs4H3kGywDWMrX2kxfA==
+ dependencies:
+ lodash "^4.17.19"
+ prop-types "^15.7.2"
+
react-dev-utils@^10.0.0:
version "10.1.0"
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-10.1.0.tgz#ccf82135f6dc2fc91969bc729ce57a69d8e86025"