Skip to content

Commit

Permalink
feat: 🎸 export results to image/JSON
Browse files Browse the repository at this point in the history
  • Loading branch information
Artur Pryka committed Sep 24, 2020
1 parent 48298c9 commit 9ff7f12
Show file tree
Hide file tree
Showing 14 changed files with 265 additions and 50 deletions.
48 changes: 47 additions & 1 deletion lib/js/app/components/ActionsMenu/ActionsMenu.styles.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import styled from 'styled-components';
import styled, { css } from 'styled-components';
import { transparentize } from 'polished';
import { motion } from 'framer-motion';
import { colors } from '@keen.io/colors';

export const Container = styled.div`
Expand All @@ -9,3 +11,47 @@ export const Container = styled.div`
export const DeleteQueryItem = styled.span`
color: ${colors.red[500]};
`;

export const MutedText = styled.div`
padding: 5px 15px;
font-family: 'Lato Regular', sans-serif;
font-size: 14px;
color: ${transparentize(0.5, colors.black[100])};
`;

export const TooltipMotion = styled(motion.div)`
position: absolute;
left: -100%;
top: 0;
z-index: 1;
`;

export const TooltipContent = styled.div`
font-family: 'Lato Regular', sans-serif;
font-size: 14px;
line-height: 17px;
white-space: nowrap;
color: ${colors.white[500]};
`;

export const ExportDataWrapper = styled.div`
position: relative;
`;

export const ExportDataLinks = styled.div<{ isActive: boolean }>`
${(props) =>
!props.isActive &&
css`
opacity: 0.5;
cursor: not-allowed;
`}
* {
${(props) =>
!props.isActive &&
css`
pointer-events: none;
`}
}
`;
38 changes: 37 additions & 1 deletion lib/js/app/components/ActionsMenu/ActionsMenu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const render = (overProps: any = {}) => {
};

const mockStore = configureStore([]);
const store = mockStore({});
const store = mockStore({ queries: {} });

const wrapper = rtlRender(
<Provider store={store}>
Expand Down Expand Up @@ -89,3 +89,39 @@ test('allows user to share query url', () => {
]
`);
});

test('allows user to export results as image', () => {
const {
wrapper: { getByText },
store,
} = render();

const exportImage = getByText(text.image);
fireEvent.click(exportImage);

expect(store.getActions()).toMatchInlineSnapshot(`
Array [
Object {
"type": "@app/EXPORT_CHART_TO_IMAGE",
},
]
`);
});

test('allows user to export results as JSON', () => {
const {
wrapper: { getByText },
store,
} = render();

const exportJson = getByText(text.json);
fireEvent.click(exportJson);

expect(store.getActions()).toMatchInlineSnapshot(`
Array [
Object {
"type": "@app/EXPORT_CHART_TO_JSON",
},
]
`);
});
62 changes: 56 additions & 6 deletions lib/js/app/components/ActionsMenu/ActionsMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
import React, { FC } from 'react';
import { useDispatch } from 'react-redux';
import { DropdownMenu } from '@keen.io/ui-core';
import React, { FC, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { DropdownMenu, Tooltip } from '@keen.io/ui-core';
import { AnimatePresence } from 'framer-motion';

import { Container, DeleteQueryItem } from './ActionsMenu.styles';
import { getQueryResults } from '../../modules/queries';

import {
Container,
DeleteQueryItem,
MutedText,
ExportDataWrapper,
ExportDataLinks,
TooltipContent,
TooltipMotion,
} from './ActionsMenu.styles';
import text from './text.json';

import { shareQueryUrl } from '../../modules/app';
import {
shareQueryUrl,
exportChartToImage,
exportChartToJson,
} from '../../modules/app';

type Props = {
/** Is new query */
Expand All @@ -16,21 +31,56 @@ type Props = {
onShareQuery?: () => void;
};

const tooltipMotion = {
initial: { opacity: 0 },
animate: { opacity: 1 },
exit: { opacity: 0 },
};

const ActionsMenu: FC<Props> = ({
isNewQuery,
onShareQuery,
onRemoveQuery,
}) => {
const dispatch = useDispatch();
const queryResults = useSelector(getQueryResults);
const [tooltip, showTooltip] = useState(false);
return (
<Container>
<DropdownMenu.Container>
<MutedText>{text.exportResult}</MutedText>
<ExportDataWrapper
onMouseEnter={() => !queryResults && showTooltip(true)}
onMouseLeave={() => tooltip && showTooltip(false)}
>
<AnimatePresence>
{tooltip && (
<TooltipMotion {...tooltipMotion}>
<Tooltip hasArrow={false} mode="dark">
<TooltipContent>{text.tooltip}</TooltipContent>
</Tooltip>
</TooltipMotion>
)}
</AnimatePresence>
<ExportDataLinks isActive={queryResults}>
<DropdownMenu.Item onClick={() => dispatch(exportChartToImage())}>
{text.image}
</DropdownMenu.Item>
<DropdownMenu.Item onClick={() => dispatch(exportChartToJson())}>
{text.json}
</DropdownMenu.Item>
<DropdownMenu.Item onClick={() => console.log('generate csv')}>
{text.csv}
</DropdownMenu.Item>
</ExportDataLinks>
</ExportDataWrapper>
<DropdownMenu.Divider />
{!isNewQuery && (
<DropdownMenu.Item onClick={onRemoveQuery}>
<DeleteQueryItem>{text.deleteQuery}</DeleteQueryItem>
</DropdownMenu.Item>
)}
<DropdownMenu.Divider />
{!isNewQuery && <DropdownMenu.Divider />}
<DropdownMenu.Item
onClick={() => {
onShareQuery && onShareQuery();
Expand Down
7 changes: 6 additions & 1 deletion lib/js/app/components/ActionsMenu/text.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
{
"deleteQuery": "Delete query",
"shareQuery": "Share query"
"shareQuery": "Share query",
"exportResult": "Export result to",
"image": "Image",
"json": "JSON",
"csv": "CSV",
"tooltip": "Run query to export result"
}
24 changes: 13 additions & 11 deletions lib/js/app/components/QueryVisualization/QueryVisualization.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,16 +72,18 @@ const QueryVisualization: FC<Props> = ({ queryResults, query }) => {
const showDataviz = widgetType !== 'json';

return (
<div>
{showDataviz ? (
<DataViz
analysisResults={queryResults}
visualization={widgetType}
ref={datavizContainerRef}
/>
) : (
<JSONView analysisResults={queryResults} />
)}
<>
<div id="query-visualization">
{showDataviz ? (
<DataViz
analysisResults={queryResults}
visualization={widgetType}
ref={datavizContainerRef}
/>
) : (
<JSONView analysisResults={queryResults} />
)}
</div>

<Settings>
{showDataviz && (
Expand Down Expand Up @@ -117,7 +119,7 @@ const QueryVisualization: FC<Props> = ({ queryResults, query }) => {
/>
</div>
</Settings>
</div>
</>
);
};

Expand Down
10 changes: 10 additions & 0 deletions lib/js/app/modules/app/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import {
SELECT_FIRST_QUERY,
SCREEN_RESIZE,
SET_SCREEN_DIMENSION,
EXPORT_CHART_TO_IMAGE,
EXPORT_CHART_TO_JSON,
} from './constants';

import {
Expand Down Expand Up @@ -136,3 +138,11 @@ export const hideConfirmation = (): AppActions => ({
export const acceptConfirmation = (): AppActions => ({
type: ACCEPT_CONFIRMATION,
});

export const exportChartToImage = () => ({
type: EXPORT_CHART_TO_IMAGE,
});

export const exportChartToJson = () => ({
type: EXPORT_CHART_TO_JSON,
});
2 changes: 2 additions & 0 deletions lib/js/app/modules/app/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export const HIDE_QUERY_SETTINGS_MODAL = '@app/HIDE_QUERY_SETTINGS_MODAL';
export const SELECT_FIRST_QUERY = '@app/SELECT_FIRST_QUERY';
export const SCREEN_RESIZE = '@app/SCREEN_RESIZE';
export const SET_SCREEN_DIMENSION = '@app/SET_SCREEN_DIMENSION';
export const EXPORT_CHART_TO_IMAGE = '@app/EXPORT_CHART_TO_IMAGE';
export const EXPORT_CHART_TO_JSON = '@app/EXPORT_CHART_TO_JSON';
export const APP_START = '@app/APP_START';

export const URL_STATE = 'keen_explorer_state';
4 changes: 4 additions & 0 deletions lib/js/app/modules/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import {
shareQueryUrl,
selectFirstSavedQuery,
appStart,
exportChartToImage,
exportChartToJson,
} from './actions';
import {
getConfirmation,
Expand Down Expand Up @@ -61,6 +63,8 @@ export {
showQuerySettingsModal,
hideQuerySettingsModal,
selectFirstSavedQuery,
exportChartToImage,
exportChartToJson,
ReducerState,
SettingsModalSource,
};
Expand Down
33 changes: 32 additions & 1 deletion lib/js/app/modules/app/saga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
} from '../savedQuery';
import {
resetQueryResults,
getQueryResults,
getSavedQueries,
fetchSavedQueries,
getOrganizationUsageLimits,
Expand All @@ -45,7 +46,7 @@ import {
b64EncodeUnicode,
b64DecodeUnicode,
} from './utils';
import { copyToClipboard } from '../../utils';
import { copyToClipboard, exportToImage, exportToJson } from '../../utils';

import { SET_QUERY_EVENT, NEW_QUERY_EVENT } from '../../queryCreator';
import { PUBSUB_CONTEXT, NOTIFICATION_MANAGER_CONTEXT } from '../../constants';
Expand All @@ -71,6 +72,8 @@ import {
SELECT_FIRST_QUERY,
URL_STATE,
SCREEN_RESIZE,
EXPORT_CHART_TO_IMAGE,
EXPORT_CHART_TO_JSON,
} from './constants';

const createScreenResizeChannel = () =>
Expand Down Expand Up @@ -246,6 +249,32 @@ export function* appStart({ payload }: AppStartAction) {
yield spawn(watchScreenResize);
}

export function* generateFileName() {
const savedQuery = yield select(getSavedQuery);
const query = yield select(getQuerySettings);

let fileName = 'chart';
if (savedQuery?.name) {
fileName = `${savedQuery.name}-${Date.now()}`;
} else if (query?.analysis_type && query?.event_collection) {
fileName = `${query.analysis_type}-${query.event_collection}-${Date.now()}`;
}
return fileName;
}

export function* exportChartToImage() {
const node = document.getElementById('query-visualization');
if (!node) throw new Error('Query visualization container is not available');
const fileName = yield generateFileName();
exportToImage({ fileName, node });
}

export function* exportChartToJson() {
const data = yield select(getQueryResults);
const fileName = yield generateFileName();
exportToJson({ data, fileName });
}

export function* appSaga() {
yield takeLatest(APP_START, appStart);
yield takeLatest(SHARE_QUERY_URL, shareQueryUrl);
Expand All @@ -256,5 +285,7 @@ export function* appSaga() {
yield takeLatest(CLEAR_QUERY, clearQuery);
yield takeLatest(SELECT_FIRST_QUERY, selectFirstSavedQuery);
yield takeLatest(EDIT_QUERY, editQuery);
yield takeLatest(EXPORT_CHART_TO_IMAGE, exportChartToImage);
yield takeLatest(EXPORT_CHART_TO_JSON, exportChartToJson);
yield debounce(200, SCREEN_RESIZE, resizeBrowserScreen);
}
30 changes: 30 additions & 0 deletions lib/js/app/utils/exportToImage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import domtoimage from 'dom-to-image';
import { saveAs } from 'file-saver';
import { colors } from '@keen.io/colors';

const FILENAME = 'chart';
const BG_COLOR = colors.white[500];

export const exportToImage = ({
fileName = FILENAME,
quality,
backgroundColor = BG_COLOR,
node,
}: {
fileName: string;
node: HTMLElement;
quality?: number;
backgroundColor?: string;
}) => {
if (quality) {
domtoimage
.toBlob(node, { quality, bgcolor: backgroundColor })
.then((blob) => {
saveAs(blob, `${fileName}.jpeg`);
});
} else {
domtoimage.toBlob(node).then((blob) => {
saveAs(blob, `${fileName}.png`);
});
}
};
Loading

0 comments on commit 9ff7f12

Please sign in to comment.