├── .gitmodules ├── packages ├── entities │ ├── .gitignore │ ├── src │ │ ├── documentId.ts │ │ ├── documentTypes.ts │ │ ├── images.ts │ │ ├── timestamp.ts │ │ ├── ciUser.ts │ │ ├── documentContent.ts │ │ ├── file.ts │ │ ├── datasetContent.ts │ │ ├── documentPart.ts │ │ ├── dataset.ts │ │ ├── visualization.ts │ │ ├── visualizationContent.ts │ │ ├── index.ts │ │ ├── datasetInfo.ts │ │ ├── documentInfo.ts │ │ ├── visualizationInfo.ts │ │ └── user.ts │ ├── classDiagram.dia │ ├── tsconfig.json │ └── package.json ├── ui │ ├── src │ │ ├── dist-symlink │ │ ├── index.js │ │ ├── utils │ │ │ └── preventDefault.js │ │ ├── constants.js │ │ ├── atoms │ │ │ ├── fullPage.js │ │ │ └── avatar.js │ │ ├── css │ │ │ ├── fullPage.sass │ │ │ ├── codemirror-tweaks.sass │ │ │ ├── index.sass │ │ │ ├── visualizationView.sass │ │ │ ├── editorGrid.sass │ │ │ └── ideGrid.sass │ │ ├── testingApp │ │ │ ├── testingApp.js │ │ │ ├── rootReducer.js │ │ │ ├── saveSimulationEpic.js │ │ │ └── files.js │ │ ├── redux │ │ │ ├── reducers │ │ │ │ ├── visualizationId.js │ │ │ │ ├── visualizationTitle.js │ │ │ │ ├── visualizationWidth.js │ │ │ │ ├── visualizationHeight.js │ │ │ │ ├── visualizationOwnerUser.js │ │ │ │ ├── visualizationDescription.js │ │ │ │ ├── runId.js │ │ │ │ ├── splitPaneDragging.js │ │ │ │ ├── saveStatus.js │ │ │ │ ├── activeFileName.js │ │ │ │ ├── index.js │ │ │ │ └── files.js │ │ │ ├── index.js │ │ │ ├── epics │ │ │ │ ├── runEpic.js │ │ │ │ ├── autoSaveEpic.js │ │ │ │ ├── confirmFileDeleteEpic.js │ │ │ │ ├── promptForNewFileNameEpic.js │ │ │ │ ├── confirmVisualizationDeleteEpic.js │ │ │ │ ├── index.js │ │ │ │ ├── promptForNewHeightEpic.js │ │ │ │ ├── promptForRenameEpic.js │ │ │ │ ├── updateTitleEpic.js │ │ │ │ └── updateDescriptionEpic.js │ │ │ ├── selectors.js │ │ │ └── actionTypes.js │ │ ├── visualizationRunner │ │ │ ├── getSrcDoc.js │ │ │ ├── computeScale.js │ │ │ ├── computeSrcDoc.js │ │ │ ├── runnerIframe.js │ │ │ └── index.js │ │ ├── visualizationFullscreen │ │ │ └── index.js │ │ ├── visualizationEditor │ │ │ ├── editorGrid.js │ │ │ └── fileList.js │ │ ├── exports.js │ │ ├── ide │ │ │ └── ideGrid.js │ │ └── visualizationView │ │ │ └── index.js │ ├── public │ │ └── index.html │ ├── rollup.config.js │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── CONTRIBUTING.md │ └── package.json ├── web │ ├── css │ │ ├── navBrand.sass │ │ ├── datasetContentTextPreview.sass │ │ ├── commentLink.sass │ │ ├── transitions.sass │ │ ├── variables.sass │ │ ├── visualizationPreview.sass │ │ └── index.sass │ ├── static │ │ ├── favicon.ico │ │ ├── favicon.png │ │ └── example.html │ ├── pages │ │ ├── upload-dataset │ │ │ ├── toFileName.js │ │ │ ├── suggestedName.js │ │ │ ├── fileUploader.js │ │ │ └── body.js │ │ ├── auth │ │ │ ├── error.js │ │ │ ├── callback.js │ │ │ └── index.js │ │ ├── _document.js │ │ ├── create-visualization │ │ │ └── body.js │ │ ├── thumbnailsTest.js │ │ └── visualization │ │ │ └── forkInvitation.js │ ├── README.md │ ├── utils │ │ ├── files.js │ │ ├── baseUrl.js │ │ ├── getJSON.js │ │ └── userFromSession.js │ ├── components │ │ ├── atoms │ │ │ ├── slightMargin.js │ │ │ ├── datasetContentTextPreview.js │ │ │ ├── textContainer.js │ │ │ ├── commentLink.js │ │ │ ├── permalinkPreview.js │ │ │ ├── titledPage.js │ │ │ ├── visualizationPreview.js │ │ │ └── unfurl.js │ │ ├── molecules │ │ │ ├── actionBox.js │ │ │ └── signIn.js │ │ ├── page.js │ │ └── organisms │ │ │ └── navBar.js │ ├── test │ │ └── flaring.csv │ ├── redux │ │ ├── selectors.js │ │ ├── rootReducer.js │ │ ├── epics │ │ │ ├── runBuildEpic.js │ │ │ ├── forkSuccessEpic.js │ │ │ ├── startBuildEpic.js │ │ │ ├── runNonJS.js │ │ │ ├── deleteVisualizationSuccessEpic.js │ │ │ ├── deleteVisualizationEpic.js │ │ │ ├── forkEpic.js │ │ │ ├── index.js │ │ │ ├── saveEpic.js │ │ │ └── buildEpic.js │ │ ├── actionTypes.js │ │ ├── reducers.js │ │ └── actionCreators.js │ ├── server │ │ ├── setupRaven.js │ │ ├── shareDBServer.js │ │ ├── next-auth.providers.js │ │ └── next-auth.functions.js │ ├── routes │ │ ├── index.js │ │ └── routeGenerators.js │ ├── LICENSE_NEXTJS_STARTER.txt │ ├── next.config.js │ └── models │ │ └── user.js ├── controllers │ ├── src │ │ ├── apiController │ │ │ ├── userIdFromReq.js │ │ │ ├── userAPIController │ │ │ │ ├── index.js │ │ │ │ └── getProfileDataController.js │ │ │ ├── index.js │ │ │ ├── datasetAPIController │ │ │ │ ├── index.js │ │ │ │ ├── createDatasetController.js │ │ │ │ └── getDatasetController.js │ │ │ └── visualizationAPIController │ │ │ │ ├── getVisualizationController.js │ │ │ │ ├── getAllVisualizationInfosController.js │ │ │ │ ├── createVisualizationController.js │ │ │ │ ├── forkVisualizationController.js │ │ │ │ ├── saveVisualizationController.js │ │ │ │ ├── deleteVisualizationController.js │ │ │ │ ├── getPreviewController.js │ │ │ │ ├── getThumbnailController.js │ │ │ │ ├── exportVisualizationController.js │ │ │ │ └── index.js │ │ ├── index.js │ │ └── userController.js │ ├── package.json │ └── package-lock.json ├── imageGenerationService │ ├── src │ │ ├── index.js │ │ ├── dimensions.js │ │ ├── computeImageDimensions.js │ │ ├── resize.js │ │ ├── generateScreenshot.js │ │ ├── generateImages.js │ │ └── service.js │ └── package.json ├── useCases │ ├── docs │ │ ├── useCasesDiagram.dia │ │ └── useCaseTemplate.md │ ├── src │ │ ├── utils │ │ │ ├── generateId.ts │ │ │ └── removeExtension.ts │ │ ├── interactors │ │ │ ├── createDataset │ │ │ │ ├── datasetDefaults.ts │ │ │ │ └── index.ts │ │ │ ├── exportVisualization │ │ │ │ ├── zipFiles.ts │ │ │ │ └── index.ts │ │ │ ├── getUser.ts │ │ │ ├── getAllVisualizationInfos.ts │ │ │ ├── getPreview.ts │ │ │ ├── getThumbnail.ts │ │ │ ├── saveVisualization.ts │ │ │ ├── createVisualization │ │ │ │ ├── visualizationDefaults.ts │ │ │ │ └── index.ts │ │ │ ├── createUser.ts │ │ │ ├── getDataset.ts │ │ │ ├── deleteVisualization.ts │ │ │ ├── getVisualization.ts │ │ │ ├── getUserProfileData.ts │ │ │ ├── forkVisualization.ts │ │ │ └── index.ts │ │ ├── interactor.ts │ │ ├── index.ts │ │ └── gatewayInterfaces │ │ │ ├── imageGeneratorGateway.ts │ │ │ ├── imageStorageGateway.ts │ │ │ ├── userGateway.ts │ │ │ ├── datasetGateway.ts │ │ │ └── visualizationGateway.ts │ ├── tsconfig.json │ ├── package.json │ ├── createVisualization.md │ └── saveVisualization.md ├── presenters │ ├── src │ │ ├── index.js │ │ ├── datasetViewModel.js │ │ ├── visualizationViewModel.js │ │ ├── d3Packages.js │ │ └── bundle.js │ ├── tsconfig.json │ └── package.json ├── database │ ├── src │ │ ├── collectionName.js │ │ ├── createUser.js │ │ ├── createVisualization.js │ │ ├── getUser.js │ │ ├── fetchShareDBQuery.js │ │ ├── save.js │ │ ├── fetchShareDBDoc.js │ │ ├── getUserByUserName.js │ │ ├── createDataset.js │ │ ├── saveVisualization.js │ │ ├── getDatasetInfosByUserId.js │ │ ├── getAllVisualizationInfos.js │ │ ├── setImagesUpdatedTimestamp.js │ │ ├── getPreview.js │ │ ├── getThumbnail.js │ │ ├── getVisualization.js │ │ ├── getVisualizationInfosByUserId.js │ │ ├── updateImages.js │ │ ├── deleteVisualization.js │ │ ├── getDataset.js │ │ └── index.js │ └── package.json ├── gateways │ ├── tsconfig.json │ ├── src │ │ ├── index.ts │ │ ├── databaseUserGateway.ts │ │ ├── databaseImageStorageGateway.ts │ │ ├── databaseDatasetGateway.ts │ │ └── databaseVisualizationGateway.ts │ ├── package.json │ └── test │ │ └── test.ts ├── i18n │ ├── package.json │ └── index.ts └── serverGateways │ ├── package.json │ └── src │ ├── index.js │ └── shareDB.js ├── .eslintignore ├── lerna.json ├── .gitmodules-ci ├── graveyard ├── oldUseCases │ ├── src │ │ ├── document.js │ │ └── index.js │ ├── package.json │ └── test │ │ └── test.js ├── computeSlug.js ├── clientServerGatewaySingleton.js ├── clientGateway.js └── computeReferences │ ├── test │ └── test.ts │ └── src │ ├── index.ts │ └── computeReferences.ts ├── .gitignore ├── deploy.sh ├── .gitlab-ci.yml ├── .eslintrc.js ├── LICENSE ├── ecosystem.config.js └── package.json /.gitmodules: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/entities/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /packages/ui/src/dist-symlink: -------------------------------------------------------------------------------- 1 | ../dist/ -------------------------------------------------------------------------------- /packages/ui/src/index.js: -------------------------------------------------------------------------------- 1 | import './testingApp/index'; 2 | -------------------------------------------------------------------------------- /packages/web/css/navBrand.sass: -------------------------------------------------------------------------------- 1 | .nav-brand img 2 | height: 32px 3 | -------------------------------------------------------------------------------- /packages/entities/src/documentId.ts: -------------------------------------------------------------------------------- 1 | // A unique identifier for a document. 2 | export type DocumentId = string; 3 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "2.11.0", 3 | "packages": [ 4 | "packages/*" 5 | ], 6 | "version": "0.0.0" 7 | } 8 | -------------------------------------------------------------------------------- /packages/controllers/src/apiController/userIdFromReq.js: -------------------------------------------------------------------------------- 1 | export const userIdFromReq = req => req.user && req.user.id; 2 | -------------------------------------------------------------------------------- /packages/web/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vizhub-core/vizhub-v1/HEAD/packages/web/static/favicon.ico -------------------------------------------------------------------------------- /packages/web/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vizhub-core/vizhub-v1/HEAD/packages/web/static/favicon.png -------------------------------------------------------------------------------- /packages/entities/classDiagram.dia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vizhub-core/vizhub-v1/HEAD/packages/entities/classDiagram.dia -------------------------------------------------------------------------------- /.gitmodules-ci: -------------------------------------------------------------------------------- 1 | [submodule "packages/vizhub-ui"] 2 | path = packages/ui 3 | url = https://github.com/datavis-tech/vizhub-ui.git 4 | -------------------------------------------------------------------------------- /graveyard/oldUseCases/src/document.js: -------------------------------------------------------------------------------- 1 | export const defaults = { 2 | title: '', 3 | slug: '', 4 | description: '' 5 | }; 6 | -------------------------------------------------------------------------------- /graveyard/oldUseCases/src/index.js: -------------------------------------------------------------------------------- 1 | export { 2 | createVisualization, 3 | updateVisualization 4 | } from './visualization'; 5 | -------------------------------------------------------------------------------- /packages/web/pages/upload-dataset/toFileName.js: -------------------------------------------------------------------------------- 1 | export const toFileName = name => name.replace(/ /g, '-').toLowerCase() + '.csv'; 2 | -------------------------------------------------------------------------------- /packages/entities/src/documentTypes.ts: -------------------------------------------------------------------------------- 1 | export const VISUALIZATION_TYPE = 'visualization'; 2 | export const DATASET_TYPE = 'dataset'; 3 | -------------------------------------------------------------------------------- /packages/entities/src/images.ts: -------------------------------------------------------------------------------- 1 | export interface Images { 2 | thumbnail: string; // base64-encoded PNG 3 | preview: string; 4 | } 5 | -------------------------------------------------------------------------------- /packages/controllers/src/index.js: -------------------------------------------------------------------------------- 1 | export { apiController } from './apiController'; 2 | export { userController } from './userController'; 3 | -------------------------------------------------------------------------------- /packages/imageGenerationService/src/index.js: -------------------------------------------------------------------------------- 1 | require('dotenv').load(); 2 | import { startService } from './service'; 3 | startService({}); 4 | -------------------------------------------------------------------------------- /packages/ui/src/utils/preventDefault.js: -------------------------------------------------------------------------------- 1 | export const preventDefault = fn => event => { 2 | event.preventDefault(); 3 | fn(); 4 | }; 5 | -------------------------------------------------------------------------------- /packages/useCases/docs/useCasesDiagram.dia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vizhub-core/vizhub-v1/HEAD/packages/useCases/docs/useCasesDiagram.dia -------------------------------------------------------------------------------- /packages/ui/src/constants.js: -------------------------------------------------------------------------------- 1 | // Times are in milliseconds. 2 | export const runDebounceTime = 800; 3 | export const autoSaveDebounceTime = 4000; 4 | -------------------------------------------------------------------------------- /packages/useCases/src/utils/generateId.ts: -------------------------------------------------------------------------------- 1 | import * as uuidV4 from 'uuid/v4'; 2 | export const generateId = () => uuidV4().replace(/-/g, ''); 3 | -------------------------------------------------------------------------------- /packages/useCases/src/utils/removeExtension.ts: -------------------------------------------------------------------------------- 1 | export const removeExtension = fileName => 2 | fileName.substr(0, fileName.lastIndexOf('.')); 3 | -------------------------------------------------------------------------------- /packages/web/README.md: -------------------------------------------------------------------------------- 1 | Derived from https://github.com/iaincollins/nextjs-starter 2 | 3 | # Development 4 | 5 | Use `yarn dev` to start dev server. 6 | -------------------------------------------------------------------------------- /packages/entities/src/timestamp.ts: -------------------------------------------------------------------------------- 1 | // Returns the Unix timestamp for the present. 2 | export const timestamp = () => Math.floor((new Date()).getTime() / 1000); 3 | -------------------------------------------------------------------------------- /packages/web/css/datasetContentTextPreview.sass: -------------------------------------------------------------------------------- 1 | .dataset-content-text-preview 2 | text-align: left 3 | pre 4 | height: 200px 5 | padding: 0px 10px 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | dist 4 | yarn.lock 5 | yarn-error.log 6 | lerna-debug.log 7 | .next 8 | *.swp 9 | *.autosave 10 | .env 11 | -------------------------------------------------------------------------------- /packages/web/utils/files.js: -------------------------------------------------------------------------------- 1 | export const hasName = name => file => file.name === name 2 | export const findFile = (name, files) => files.filter(hasName(name))[0]; 3 | -------------------------------------------------------------------------------- /packages/web/components/atoms/slightMargin.js: -------------------------------------------------------------------------------- 1 | export const SlightMargin = ({children}) => ( 2 |
3 | {children} 4 |
5 | ); 6 | -------------------------------------------------------------------------------- /packages/web/utils/baseUrl.js: -------------------------------------------------------------------------------- 1 | export const baseUrl = req => 2 | (req ? `${req.protocol}://${req.get('Host')}` : '') 3 | .replace('http://vizhub.com', 'https://vizhub.com'); 4 | -------------------------------------------------------------------------------- /packages/ui/src/atoms/fullPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const FullPage = ({children}) => ( 4 |
5 | {children} 6 |
7 | ); 8 | -------------------------------------------------------------------------------- /packages/ui/src/css/fullPage.sass: -------------------------------------------------------------------------------- 1 | .full-page 2 | display: flex 3 | flex-direction: column 4 | position: fixed 5 | top: 0px 6 | bottom: 0px 7 | left: 0px 8 | right: 0px 9 | -------------------------------------------------------------------------------- /packages/presenters/src/index.js: -------------------------------------------------------------------------------- 1 | export { VisualizationViewModel } from './visualizationViewModel'; 2 | export { DatasetViewModel } from './datasetViewModel'; 3 | export { bundle } from './bundle'; 4 | -------------------------------------------------------------------------------- /packages/web/components/atoms/datasetContentTextPreview.js: -------------------------------------------------------------------------------- 1 | export const DatasetContentTextPreview = ({text}) => ( 2 |
3 |
{text}
4 |
5 | ); 6 | -------------------------------------------------------------------------------- /packages/web/components/atoms/textContainer.js: -------------------------------------------------------------------------------- 1 | export const TextContainer = ({children}) => ( 2 |
3 | {children} 4 |
5 | ); 6 | -------------------------------------------------------------------------------- /packages/web/css/commentLink.sass: -------------------------------------------------------------------------------- 1 | .comment-link 2 | background-color: gray 3 | color: white 4 | position: fixed 5 | bottom: 0px 6 | left: 0px 7 | padding: 2px 10px 8 | a 9 | color: white 10 | -------------------------------------------------------------------------------- /packages/useCases/src/interactors/createDataset/datasetDefaults.ts: -------------------------------------------------------------------------------- 1 | export const datasetDefaults = { 2 | title: 'Untitled', 3 | slug: undefined, 4 | description: 'No description', 5 | file: '', 6 | format: 'csv' 7 | }; 8 | -------------------------------------------------------------------------------- /packages/web/utils/getJSON.js: -------------------------------------------------------------------------------- 1 | import { baseUrl } from './baseUrl'; 2 | 3 | export const getJSON = async (relativeUrl, req) => { 4 | const url = baseUrl(req) + relativeUrl; 5 | return await (await fetch(url)).json(); 6 | } 7 | -------------------------------------------------------------------------------- /packages/ui/src/testingApp/testingApp.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FullPage, IDEContainer } from '../exports'; 3 | 4 | export const TestingApp = () => ( 5 | 6 | 7 | 8 | ); 9 | -------------------------------------------------------------------------------- /graveyard/computeSlug.js: -------------------------------------------------------------------------------- 1 | const slugFromTitle = title => title.toLowerCase().replace(/ /g, '-'); 2 | 3 | const computeSlug = data => data.slug 4 | ? data.slug 5 | : data.title 6 | ? slugFromTitle(data.title) 7 | : undefined; 8 | -------------------------------------------------------------------------------- /packages/ui/src/css/codemirror-tweaks.sass: -------------------------------------------------------------------------------- 1 | .CodeMirror 2 | height: 100% 3 | font-family: $family-monospace 4 | line-height: 1 5 | font-size: $font-size-for-code 6 | 7 | .react-codemirror2, .code-editor-container 8 | height: 100% 9 | -------------------------------------------------------------------------------- /packages/web/components/atoms/commentLink.js: -------------------------------------------------------------------------------- 1 | export const CommentLink = () => ( 2 |
3 | Feedback 4 |
5 | ); 6 | -------------------------------------------------------------------------------- /packages/useCases/src/interactor.ts: -------------------------------------------------------------------------------- 1 | export interface RequestModel {}; 2 | 3 | export interface ResponseModel {}; 4 | 5 | export interface Interactor { 6 | execute(req: RequestModel | undefined): Promise; 7 | } 8 | -------------------------------------------------------------------------------- /packages/entities/src/ciUser.ts: -------------------------------------------------------------------------------- 1 | export const ciUser = { 2 | id: '47895473289547832938754', 3 | fullName: 'CI', 4 | email: 'ci@testing.com', 5 | userName: 'ci', 6 | avatarUrl: 'https://avatars0.githubusercontent.com/u/639823?v=4' 7 | }; 8 | -------------------------------------------------------------------------------- /packages/entities/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "sourceMap": true, 5 | "strictNullChecks": true, 6 | "lib": [ "ES2018" ] 7 | }, 8 | "include": [ 9 | "./src/" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /packages/useCases/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "sourceMap": true, 5 | "strictNullChecks": true, 6 | "lib": [ "ES2018" ] 7 | }, 8 | "include": [ 9 | "./src/" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | git checkout master 4 | git pull 5 | 6 | lerna bootstrap 7 | cd packages/web 8 | npm run build 9 | pm2 restart all 10 | 11 | # If no processes are set up in PM2, run this: 12 | # pm2 start ecosystem.config.js --env production 13 | -------------------------------------------------------------------------------- /packages/ui/src/testingApp/rootReducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { uiRedux } from '../exports'; 3 | 4 | const { 5 | reducers: { 6 | ide 7 | } 8 | } = uiRedux; 9 | 10 | export const rootReducer = combineReducers({ ide }); 11 | -------------------------------------------------------------------------------- /packages/controllers/src/apiController/userAPIController/index.js: -------------------------------------------------------------------------------- 1 | import { getProfileDataController } from './getProfileDataController'; 2 | 3 | export const userAPIController = (expressApp, userGateway) => { 4 | getProfileDataController(expressApp, userGateway); 5 | }; 6 | -------------------------------------------------------------------------------- /packages/web/components/atoms/permalinkPreview.js: -------------------------------------------------------------------------------- 1 | import { datasetUrl } from '../../routes/routeGenerators'; 2 | 3 | export const PermalinkPreview = ({userName, slug}) => ( 4 |
5 | Permalink: {datasetUrl({userName, slug})} 6 |
7 | ); 8 | -------------------------------------------------------------------------------- /packages/database/src/collectionName.js: -------------------------------------------------------------------------------- 1 | export const DOCUMENT_INFO = 'documentInfo'; 2 | export const DOCUMENT_CONTENT = 'documentContent'; 3 | export const USER = 'user'; 4 | export const THUMBNAIL_IMAGES = 'thumbnailImages'; 5 | export const PREVIEW_IMAGES = 'previewImages'; 6 | -------------------------------------------------------------------------------- /packages/useCases/src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | Interactor, 3 | RequestModel, 4 | ResponseModel 5 | } from './interactor'; 6 | 7 | export { 8 | VisualizationGateway 9 | } from './gatewayInterfaces/visualizationGateway'; 10 | 11 | export * from './interactors'; 12 | -------------------------------------------------------------------------------- /packages/gateways/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "sourceMap": true, 5 | "strictNullChecks": true, 6 | "lib": [ "ESNext", "dom" ], 7 | "allowJs": true 8 | }, 9 | "include": [ 10 | "./src/" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /packages/useCases/src/gatewayInterfaces/imageGeneratorGateway.ts: -------------------------------------------------------------------------------- 1 | import { Visualization, Images } from 'datavis-tech-entities'; 2 | 3 | export interface ImageGeneratorGateway { 4 | generateImages(visualization: Visualization, waitTime: number | undefined): Promise; 5 | } 6 | -------------------------------------------------------------------------------- /packages/entities/src/documentContent.ts: -------------------------------------------------------------------------------- 1 | import { DocumentPart } from './documentPart'; 2 | 3 | export class DocumentContent extends DocumentPart { 4 | constructor(data) { 5 | super({ 6 | id: data.id, 7 | documentType: data.documentType 8 | }); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/web/pages/upload-dataset/suggestedName.js: -------------------------------------------------------------------------------- 1 | const stripExtension = fileName => fileName.substr(0, fileName.lastIndexOf('.')); 2 | const capitalize = name => name.charAt(0).toUpperCase() + name.substr(1); 3 | export const suggestedName = fileName => capitalize(stripExtension(fileName)); 4 | -------------------------------------------------------------------------------- /packages/web/css/transitions.sass: -------------------------------------------------------------------------------- 1 | $transition-duration: 200ms 2 | 3 | a 4 | transition: color $transition-duration 5 | 6 | .button, 7 | .control input 8 | transition: border-color $transition-duration 9 | 10 | .dropdown-item 11 | transition: background-color $transition-duration 12 | -------------------------------------------------------------------------------- /packages/presenters/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "sourceMap": true, 5 | "strictNullChecks": true, 6 | "lib": [ "ES2018" ], 7 | "allowJs": true, 8 | "target": "es5" 9 | }, 10 | "include": [ 11 | "./src/" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /packages/database/src/createUser.js: -------------------------------------------------------------------------------- 1 | import { USER } from './collectionName'; 2 | 3 | export const createUser = connection => user => { 4 | return new Promise(resolve => { 5 | // TODO handle errors here 6 | connection.get(USER, user.id).create(user); 7 | resolve(user); 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /packages/web/test/flaring.csv: -------------------------------------------------------------------------------- 1 | Year,Total,Gas,Liquids,Solids,Cement Production,Gas Flaring,Per Capita 2 | 2010,9128,1696,3107,3812,446,67,1.32 3 | 2011,9503,1756,3134,4055,494,64,1.36 4 | 2012,9673,1783,3200,4106,519,65,1.36 5 | 2013,9773,1806,3220,4126,554,68,1.36 6 | 2014,9855,1823,3280,4117,568,68,1.36 7 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: node:10 2 | 3 | before_script: 4 | - mv .gitmodules-ci .gitmodules # Use HTTPS in CI, SSH in dev (so no password prompt) 5 | - git submodule sync 6 | - git submodule update --init 7 | - npm install -g lerna 8 | - lerna bootstrap 9 | 10 | test: 11 | script: 12 | - npm run test 13 | -------------------------------------------------------------------------------- /packages/gateways/src/index.ts: -------------------------------------------------------------------------------- 1 | export { DatabaseVisualizationGateway } from './databaseVisualizationGateway'; 2 | export { DatabaseDatasetGateway } from './databaseDatasetGateway'; 3 | export { DatabaseUserGateway } from './databaseUserGateway'; 4 | export { DatabaseImageStorageGateway } from './databaseImageStorageGateway'; 5 | -------------------------------------------------------------------------------- /packages/ui/src/redux/reducers/visualizationId.js: -------------------------------------------------------------------------------- 1 | import { SET_VISUALIZATION_ID } from '../actionTypes'; 2 | 3 | export const visualizationId = (state = '', action) => { 4 | switch (action.type) { 5 | case SET_VISUALIZATION_ID: 6 | return action.id; 7 | default: 8 | return state; 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /packages/useCases/docs/useCaseTemplate.md: -------------------------------------------------------------------------------- 1 | # Name of Use Case 2 | 3 | Actor: Foo 4 | 5 | Description goes here 6 | 7 | ## Triggers 8 | 9 | ## Preconditions 10 | 11 | ## Postconditions 12 | 13 | ## Normal Course of Events 14 | 15 | ## Alternative Courses of Events 16 | 17 | ## Exception 18 | 19 | ## Includes 20 | -------------------------------------------------------------------------------- /packages/i18n/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "datavis-tech-i18n", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "", 6 | "main": "index.ts", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "Curran Kelleher", 11 | "license": "UNLICENSED" 12 | } 13 | -------------------------------------------------------------------------------- /packages/web/utils/userFromSession.js: -------------------------------------------------------------------------------- 1 | import { User } from 'datavis-tech-entities'; 2 | 3 | // Creates a User entity from the Express session. 4 | export const userFromSession = session => ( 5 | session.user 6 | ? new User(Object.assign({ authenticated: true }, session.user)) 7 | : new User({ authenticated: false }) 8 | ); 9 | -------------------------------------------------------------------------------- /packages/ui/src/redux/reducers/visualizationTitle.js: -------------------------------------------------------------------------------- 1 | import { SET_VISUALIZATION_TITLE } from '../actionTypes'; 2 | 3 | export const visualizationTitle = (state = '', action) => { 4 | switch (action.type) { 5 | case SET_VISUALIZATION_TITLE: 6 | return action.title; 7 | default: 8 | return state; 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /packages/ui/src/redux/reducers/visualizationWidth.js: -------------------------------------------------------------------------------- 1 | import { SET_VISUALIZATION_WIDTH } from '../actionTypes'; 2 | 3 | export const visualizationWidth = (state = -1, action) => { 4 | switch (action.type) { 5 | case SET_VISUALIZATION_WIDTH: 6 | return action.width; 7 | default: 8 | return state; 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /packages/ui/src/redux/reducers/visualizationHeight.js: -------------------------------------------------------------------------------- 1 | import { SET_VISUALIZATION_HEIGHT } from '../actionTypes'; 2 | 3 | export const visualizationHeight = (state = -1, action) => { 4 | switch (action.type) { 5 | case SET_VISUALIZATION_HEIGHT: 6 | return action.height; 7 | default: 8 | return state; 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /packages/web/redux/selectors.js: -------------------------------------------------------------------------------- 1 | export const getCsrfToken = state => state.csrfToken; 2 | export const getVisualization = state => state.visualization; 3 | export const getUser = state => state.user; 4 | export const getUserName = state => (getUser(state) || {}).userName; 5 | export const getShowForkInvitation = state => state.showForkInvitation; 6 | -------------------------------------------------------------------------------- /packages/database/src/createVisualization.js: -------------------------------------------------------------------------------- 1 | import { DOCUMENT_INFO, DOCUMENT_CONTENT } from './collectionName'; 2 | import { saveVisualization } from './saveVisualization'; 3 | 4 | export const createVisualization = connection => visualization => 5 | saveVisualization(connection)({ visualization }) 6 | .then(() => ({ id: visualization.id })); 7 | -------------------------------------------------------------------------------- /packages/web/components/molecules/actionBox.js: -------------------------------------------------------------------------------- 1 | export const ActionBox = ({title, children}) => ( 2 |
6 |
7 |

{title}

8 | { children } 9 |
10 |
11 | ); 12 | -------------------------------------------------------------------------------- /packages/entities/src/file.ts: -------------------------------------------------------------------------------- 1 | export class File { 2 | 3 | // The name of the file. 4 | // For example "index.html", "index.js", "foo.js", "styles.css" 5 | name: string; 6 | 7 | // The text file content. 8 | text: string; 9 | 10 | constructor (data) { 11 | this.name = data.name; 12 | this.text = data.text; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/useCases/src/gatewayInterfaces/imageStorageGateway.ts: -------------------------------------------------------------------------------- 1 | import { DocumentId, Images } from 'datavis-tech-entities'; 2 | 3 | export interface ImageStorageGateway { 4 | updateImages({ id: DocumentId, images: Images}): Promise; 5 | getThumbnail({ id: DocumentId} ): Promise; 6 | getPreview({ id: DocumentId} ): Promise; 7 | } 8 | -------------------------------------------------------------------------------- /packages/ui/src/redux/reducers/visualizationOwnerUser.js: -------------------------------------------------------------------------------- 1 | import { SET_VISUALIZATION_OWNER_USER } from '../actionTypes'; 2 | 3 | export const visualizationOwnerUser = (state = {}, action) => { 4 | switch (action.type) { 5 | case SET_VISUALIZATION_OWNER_USER: 6 | return action.user; 7 | default: 8 | return state; 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /packages/ui/src/redux/reducers/visualizationDescription.js: -------------------------------------------------------------------------------- 1 | import { SET_VISUALIZATION_DESCRIPTION } from '../actionTypes'; 2 | 3 | export const visualizationDescription = (state = '', action) => { 4 | switch (action.type) { 5 | case SET_VISUALIZATION_DESCRIPTION: 6 | return action.description; 7 | default: 8 | return state; 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /packages/ui/src/redux/reducers/runId.js: -------------------------------------------------------------------------------- 1 | import { RUN_FILES } from '../actionTypes'; 2 | 3 | const generateRunId = () => Math.random().toString(36).substr(2); 4 | export const runId = (state = generateRunId(), action) => { 5 | switch (action.type) { 6 | case RUN_FILES: 7 | return generateRunId(); 8 | default: 9 | return state; 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /packages/ui/src/visualizationRunner/getSrcDoc.js: -------------------------------------------------------------------------------- 1 | import { computeSrcDoc } from './computeSrcDoc'; 2 | 3 | export const GetSrcDoc = (() => { 4 | let srcDoc; 5 | let prevRunId; 6 | return (files, runId) => { 7 | if (runId !== prevRunId) { 8 | srcDoc = computeSrcDoc(files); 9 | prevRunId = runId; 10 | } 11 | return srcDoc; 12 | }; 13 | }); 14 | -------------------------------------------------------------------------------- /packages/imageGenerationService/src/dimensions.js: -------------------------------------------------------------------------------- 1 | const defaultWidth = 960; 2 | const defaultHeight = 500; 3 | const aspectRatio = defaultWidth / defaultHeight; 4 | 5 | const dimensions = width => ({ 6 | width, 7 | height: Math.round(width / aspectRatio) 8 | }); 9 | 10 | export const thumbnailDimensions = dimensions(230); 11 | export const previewDimensions = dimensions(960); 12 | -------------------------------------------------------------------------------- /packages/web/redux/rootReducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { uiRedux } from 'vizhub-ui'; 3 | import { csrfToken, visualization, showForkInvitation, user } from './reducers'; 4 | const { reducers: { ide } } = uiRedux; 5 | 6 | export const rootReducer = combineReducers({ 7 | ide, 8 | csrfToken, 9 | visualization, 10 | showForkInvitation, 11 | user 12 | }); 13 | -------------------------------------------------------------------------------- /packages/useCases/src/gatewayInterfaces/userGateway.ts: -------------------------------------------------------------------------------- 1 | import { User, UserId } from 'datavis-tech-entities'; 2 | 3 | export interface UserGateway { 4 | createUser(user: User): Promise; 5 | getUser(id: UserId): Promise; 6 | getUserByUserName(userName: string): Promise; 7 | 8 | //saveUser(request: SaveUserRequestModel): 9 | // Promise; 10 | } 11 | -------------------------------------------------------------------------------- /packages/ui/src/redux/index.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from './actionTypes'; 2 | import * as actionCreators from './actionCreators'; 3 | import * as reducers from './reducers/index'; 4 | import * as selectors from './selectors'; 5 | import * as epics from './epics/index'; 6 | 7 | export const uiRedux = { 8 | actionTypes, 9 | actionCreators, 10 | reducers, 11 | selectors, 12 | epics 13 | }; 14 | -------------------------------------------------------------------------------- /packages/database/src/getUser.js: -------------------------------------------------------------------------------- 1 | import { User } from 'datavis-tech-entities'; 2 | import { USER } from './collectionName'; 3 | import { fetchShareDBDoc } from './fetchShareDBDoc'; 4 | 5 | export const getUser = connection => async (id) => { 6 | try { 7 | const userDoc = await fetchShareDBDoc(USER, id, connection); 8 | return new User(userDoc.data); 9 | } catch (error) { 10 | return null; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/ui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | VizHub UI 7 | 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/useCases/src/interactors/exportVisualization/zipFiles.ts: -------------------------------------------------------------------------------- 1 | import { constants } from 'fs'; 2 | import * as AdmZip from 'adm-zip'; 3 | import { File } from 'datavis-tech-entities'; 4 | 5 | export const zipFiles = (files: File[]) => { 6 | const zip = new AdmZip(); 7 | files.forEach(file => { 8 | zip.addFile(file.name, Buffer.alloc(file.text.length, file.text)); 9 | }); 10 | return zip.toBuffer(); 11 | } 12 | -------------------------------------------------------------------------------- /packages/ui/src/visualizationRunner/computeScale.js: -------------------------------------------------------------------------------- 1 | // Shrink and grow to fill available width and height. 2 | export const computeScale = options => { 3 | const { boundsWidth, boundsHeight, width, height } = options; 4 | 5 | const aspect = width / height; 6 | const boundsAspect = boundsWidth / boundsHeight; 7 | 8 | return aspect > boundsAspect 9 | ? boundsWidth / width 10 | : boundsHeight / height; 11 | }; 12 | -------------------------------------------------------------------------------- /packages/database/src/fetchShareDBQuery.js: -------------------------------------------------------------------------------- 1 | import { i18n } from 'datavis-tech-i18n'; 2 | 3 | export const fetchShareDBQuery = (collection, mongoQuery, connection) => ( 4 | new Promise((resolve, reject) => { 5 | const query = connection 6 | .createFetchQuery(collection, mongoQuery, {}, (error, results) => { 7 | error ? reject(error) : resolve(results); 8 | //query.destroy(); 9 | }); 10 | }) 11 | ); 12 | -------------------------------------------------------------------------------- /packages/ui/src/redux/reducers/splitPaneDragging.js: -------------------------------------------------------------------------------- 1 | import { 2 | SPLIT_PANE_DRAG_STARTED, 3 | SPLIT_PANE_DRAG_FINISHED 4 | } from '../actionTypes'; 5 | 6 | export const splitPaneDragging = (state = false, action) => { 7 | switch (action.type) { 8 | case SPLIT_PANE_DRAG_STARTED: 9 | return true; 10 | case SPLIT_PANE_DRAG_FINISHED: 11 | return false; 12 | default: 13 | return state; 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /packages/presenters/src/datasetViewModel.js: -------------------------------------------------------------------------------- 1 | import { Dataset } from 'datavis-tech-entities'; 2 | 3 | export class DatasetViewModel { 4 | constructor(dataset) { 5 | this.title = dataset.info.title; 6 | this.slug = dataset.info.slug; 7 | this.format = dataset.info.format; 8 | this.sourceName = dataset.info.sourceName; 9 | this.sourceUrl = dataset.info.sourceUrl; 10 | this.text = dataset.content.text; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/ui/src/atoms/avatar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const defaultScale = 30; 4 | export const Avatar = ({avatarUrl, scale = defaultScale}) => ( 5 | 15 | ); 16 | -------------------------------------------------------------------------------- /packages/entities/src/datasetContent.ts: -------------------------------------------------------------------------------- 1 | import { DocumentContent } from './documentContent'; 2 | import { DATASET_TYPE } from './documentTypes'; 3 | 4 | export class DatasetContent extends DocumentContent { 5 | 6 | // The text content of this dataset. 7 | text: string; 8 | 9 | constructor(data) { 10 | super({ 11 | id: data.id, 12 | documentType: DATASET_TYPE 13 | }); 14 | this.text = data.text; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/controllers/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "datavis-tech-controllers", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "src/index.js", 6 | "author": "Curran Kelleher", 7 | "license": "MIT", 8 | "dependencies": { 9 | "diff-match-patch": "^1.0.1", 10 | "json0-ot-diff": "^1.0.3", 11 | "datavis-tech-use-cases": "1", 12 | "datavis-tech-presenters": "1", 13 | "datavis-tech-entities": "1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /graveyard/clientServerGatewaySingleton.js: -------------------------------------------------------------------------------- 1 | import ClientGateway from './client'; 2 | 3 | // Singleton. 4 | let gateway; 5 | 6 | export const getGateway = () => { 7 | if (!gateway) { 8 | if (process.browser) { 9 | gateway = ClientGateway(); 10 | } else { 11 | 12 | // Dynamic require so it doesn't end up in the client bundle. 13 | gateway = require('./server').ServerGateway(); 14 | } 15 | } 16 | return gateway; 17 | }; 18 | -------------------------------------------------------------------------------- /packages/ui/src/css/index.sass: -------------------------------------------------------------------------------- 1 | @import "./codemirror-ubuntu-theme"; 2 | 3 | $font-size-for-code: 1.2rem 4 | $family-monospace: "Ubuntu Mono", monospace 5 | $family-sans: "Ubuntu", sans-serif 6 | 7 | @import "./codemirror-tweaks.sass"; 8 | 9 | $editor-break-small: 700px 10 | $editor-split-border: 1px solid #5e4e59 11 | @import "./editorGrid.sass"; 12 | @import "./ideGrid.sass"; 13 | 14 | @import "./fullPage.sass"; 15 | @import "./visualizationView.sass"; 16 | -------------------------------------------------------------------------------- /packages/ui/src/redux/epics/runEpic.js: -------------------------------------------------------------------------------- 1 | import { debounceTime, mapTo } from 'rxjs/operators'; 2 | import { CHANGE_FILE_TEXT, SET_VISUALIZATION_HEIGHT } from '../actionTypes'; 3 | import { runFiles } from '../actionCreators'; 4 | import { runDebounceTime } from '../../constants'; 5 | 6 | export const runEpic = action$ => 7 | action$.ofType(CHANGE_FILE_TEXT, SET_VISUALIZATION_HEIGHT).pipe( 8 | debounceTime(runDebounceTime), 9 | mapTo(runFiles()) 10 | ); 11 | -------------------------------------------------------------------------------- /packages/ui/src/visualizationFullscreen/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { VisualizationRunner } from '../visualizationRunner/index.js'; 3 | 4 | export const VisualizationFullscreen = props => { 5 | const { width, height, files } = props; 6 | 7 | return ( 8 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /packages/web/redux/epics/runBuildEpic.js: -------------------------------------------------------------------------------- 1 | import { mapTo, debounceTime } from 'rxjs/operators'; 2 | import { uiRedux } from 'vizhub-ui'; 3 | 4 | const { 5 | actionTypes: { 6 | BUILD_FINISHED, 7 | SET_VISUALIZATION_HEIGHT 8 | }, 9 | actionCreators: { 10 | runFiles 11 | } 12 | } = uiRedux; 13 | 14 | export const runBuildEpic = action$ => 15 | action$.ofType(BUILD_FINISHED, SET_VISUALIZATION_HEIGHT).pipe( 16 | mapTo(runFiles()) 17 | ); 18 | -------------------------------------------------------------------------------- /packages/ui/src/visualizationEditor/editorGrid.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const EditorGrid = ({children}) => ( 4 |
5 | { children } 6 |
7 | ); 8 | 9 | EditorGrid.Left = ({children}) => ( 10 |
11 | { children } 12 |
13 | ); 14 | 15 | EditorGrid.Center = ({children}) => ( 16 |
17 | { children } 18 |
19 | ); 20 | -------------------------------------------------------------------------------- /packages/web/server/setupRaven.js: -------------------------------------------------------------------------------- 1 | // This reports errors to https://sentry.io for alerts. 2 | import Raven from 'raven'; 3 | 4 | export const setupRaven = expressApp => { 5 | Raven.config('https://fa728910e7374f7a8be9ef439d153436@sentry.io/1246750').install(); 6 | expressApp.use(Raven.requestHandler()); 7 | 8 | expressApp.get('/testRaven', (req, res) => { 9 | throw new Error('Broke!'); 10 | }); 11 | 12 | expressApp.use(Raven.errorHandler()); 13 | }; 14 | -------------------------------------------------------------------------------- /packages/web/static/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Example Static File 5 | 6 | 7 |

Example Static File

8 |

If you want to serve static files - including images, or fonts - just place them in the /static directory.

9 |

This file is accessible from /static/example.html.

10 | 11 | 12 | -------------------------------------------------------------------------------- /graveyard/clientGateway.js: -------------------------------------------------------------------------------- 1 | import ShareDB from '@teamwork/sharedb/lib/client'; 2 | import { Gateway } from 'datavis-tech-gateways'; 3 | import { Database } from 'datavis-tech-database'; 4 | 5 | const ClientGateway = () => { 6 | const socket = new WebSocket('ws://' + window.location.host); 7 | const connection = new ShareDB.Connection(socket); 8 | const database = Database(connection); 9 | return Gateway(database); 10 | }; 11 | 12 | export default ClientGateway; 13 | -------------------------------------------------------------------------------- /packages/database/src/save.js: -------------------------------------------------------------------------------- 1 | import jsonDiff from 'json0-ot-diff'; 2 | import diffMatchPatch from 'diff-match-patch'; 3 | 4 | export const save = (doc, data) => new Promise((resolve, reject) => { 5 | const callback = error => error 6 | ? reject(error) 7 | : resolve({ status: 'success' }); 8 | 9 | if (!doc.type) { 10 | doc.create(data, callback); 11 | } else { 12 | doc.submitOp(jsonDiff(doc.data, data, diffMatchPatch), callback); 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /packages/ui/src/redux/epics/autoSaveEpic.js: -------------------------------------------------------------------------------- 1 | import { debounceTime, mapTo } from 'rxjs/operators'; 2 | import { CHANGE_FILE_TEXT, SET_VISUALIZATION_HEIGHT } from '../actionTypes'; 3 | import { save } from '../actionCreators'; 4 | import { autoSaveDebounceTime } from '../../constants'; 5 | 6 | export const autoSaveEpic = action$ => 7 | action$.ofType(CHANGE_FILE_TEXT, SET_VISUALIZATION_HEIGHT).pipe( 8 | debounceTime(autoSaveDebounceTime), 9 | mapTo(save()) 10 | ); 11 | -------------------------------------------------------------------------------- /packages/ui/src/redux/epics/confirmFileDeleteEpic.js: -------------------------------------------------------------------------------- 1 | import { map, filter } from 'rxjs/operators'; 2 | import { DELETE_FILE } from '../actionTypes'; 3 | import { fileDeleted } from '../actionCreators'; 4 | 5 | export const confirmFileDeleteEpic = action$ => 6 | action$.ofType(DELETE_FILE).pipe( 7 | map(action => ( 8 | window.confirm('Are you sure?') 9 | ? fileDeleted(action.fileName) 10 | : null 11 | )), 12 | filter(Boolean) 13 | ); 14 | -------------------------------------------------------------------------------- /packages/ui/src/redux/epics/promptForNewFileNameEpic.js: -------------------------------------------------------------------------------- 1 | import { map } from 'rxjs/operators'; 2 | import { CREATE_NEW_FILE } from '../actionTypes'; 3 | import { newFileCreated } from '../actionCreators'; 4 | 5 | export const promptForNewFileNameEpic = action$ => 6 | action$.ofType(CREATE_NEW_FILE).pipe( 7 | map(action => { 8 | const fileName = window.prompt('Please enter a file name', 'myNewFile.js'); 9 | return newFileCreated(fileName); 10 | }) 11 | ); 12 | -------------------------------------------------------------------------------- /packages/imageGenerationService/src/computeImageDimensions.js: -------------------------------------------------------------------------------- 1 | export const computeImageDimensions = ({ actual, desired }) => 2 | actual.width / actual.height < desired.width / desired.height 3 | ? 4 | { 5 | width: desired.width, 6 | height: Math.round(actual.height / actual.width * desired.width) 7 | } 8 | : 9 | { 10 | width: Math.round(actual.width / actual.height * desired.height), 11 | height: desired.height 12 | }; 13 | -------------------------------------------------------------------------------- /packages/ui/src/testingApp/saveSimulationEpic.js: -------------------------------------------------------------------------------- 1 | import { delay, mapTo } from 'rxjs/operators'; 2 | import { SAVE } from '../redux/actionTypes'; 3 | import { saveSuccess } from '../redux/actionCreators'; 4 | 5 | // Simulates saving the data to the server, with a delay of 1 second, 6 | // for testing the "Saving..." -> "Saved" part of the UI. 7 | export const saveSimulationEpic = action$ => 8 | action$.ofType(SAVE).pipe( 9 | delay(1000), 10 | mapTo(saveSuccess()) 11 | ); 12 | -------------------------------------------------------------------------------- /packages/controllers/src/apiController/index.js: -------------------------------------------------------------------------------- 1 | import { visualizationAPIController } from './visualizationAPIController/index'; 2 | import { datasetAPIController } from './datasetAPIController/index'; 3 | import { userAPIController } from './userAPIController/index'; 4 | 5 | export const apiController = (expressApp, gateways) => { 6 | visualizationAPIController(expressApp, gateways); 7 | datasetAPIController(expressApp, gateways); 8 | userAPIController(expressApp, gateways); 9 | }; 10 | -------------------------------------------------------------------------------- /packages/ui/src/exports.js: -------------------------------------------------------------------------------- 1 | export { uiRedux } from './redux/index'; 2 | export { IDEContainer } from './ide/index'; 3 | export { VisualizationRunner } from './visualizationRunner/index'; 4 | export { VisualizationFullscreen } from './visualizationFullscreen/index'; 5 | export { computeSrcDoc } from './visualizationRunner/computeSrcDoc'; 6 | export { FullPage } from './atoms/fullPage'; 7 | export { Avatar } from './atoms/avatar'; 8 | export { runDebounceTime, autoSaveDebounceTime } from './constants'; 9 | -------------------------------------------------------------------------------- /graveyard/oldUseCases/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "datavis-tech-use-cases", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "", 6 | "main": "src/index.js", 7 | "scripts": { 8 | "test": "mocha -r esm" 9 | }, 10 | "author": "Curran Kelleher", 11 | "license": "UNLICENSED", 12 | "dependencies": { 13 | "datavis-tech-entities": "1", 14 | "datavis-tech-i18n": "1", 15 | "esm": "^3.0.51", 16 | "mocha": "^5.2.0", 17 | "uuid": "^3.2.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/ui/rollup.config.js: -------------------------------------------------------------------------------- 1 | import buble from 'rollup-plugin-buble'; 2 | 3 | export default { 4 | input: 'src/exports.js', 5 | external: [ 6 | 'react', 7 | 'react-measure', 8 | 'magic-sandbox', 9 | 'react-codemirror2', 10 | 'redux', 11 | 'classnames', 12 | 'react-redux', 13 | 'lodash/fp/unionBy', 14 | 'rxjs/operators' 15 | ], 16 | plugins: [ 17 | buble() 18 | ], 19 | output: [ 20 | { file: 'dist/index.js', format: 'cjs' } 21 | ] 22 | }; 23 | -------------------------------------------------------------------------------- /packages/entities/src/documentPart.ts: -------------------------------------------------------------------------------- 1 | import { DocumentId } from './documentId'; 2 | 3 | export class DocumentPart { 4 | 5 | // The unique ID of the document 6 | // that this document part is a part of. 7 | id: DocumentId; 8 | 9 | // The type of this document. 10 | // Either documentTypes.VISUALIZATION_TYPE or documentTypes.DATASET_TYPE 11 | documentType: string; 12 | 13 | constructor(data) { 14 | this.id = data.id; 15 | this.documentType = data.documentType; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/controllers/src/apiController/datasetAPIController/index.js: -------------------------------------------------------------------------------- 1 | import { createDatasetController } from './createDatasetController'; 2 | import { getDatasetController } from './getDatasetController'; 3 | // import { saveDatasetController } from './saveDatasetController'; 4 | 5 | export const datasetAPIController = (expressApp, gateways) => { 6 | createDatasetController(expressApp, gateways); 7 | getDatasetController(expressApp, gateways); 8 | // saveDatasetController(expressApp, datasetGateway); 9 | }; 10 | -------------------------------------------------------------------------------- /packages/imageGenerationService/src/resize.js: -------------------------------------------------------------------------------- 1 | import sharp from 'sharp'; 2 | import { computeImageDimensions } from './computeImageDimensions'; 3 | 4 | export const resize = async options => { 5 | const { 6 | actualDimensions: actual, 7 | desiredDimensions: desired, 8 | screenshotBuffer 9 | } = options; 10 | 11 | const { width, height } = computeImageDimensions({ actual, desired }); 12 | 13 | return await sharp(screenshotBuffer) 14 | .resize(width, height) 15 | .toBuffer(); 16 | }; 17 | -------------------------------------------------------------------------------- /packages/ui/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | yarn.lock 24 | 25 | # editor turd 26 | *.swp 27 | 28 | # built files 29 | dist 30 | 31 | package-lock.json 32 | -------------------------------------------------------------------------------- /packages/presenters/src/visualizationViewModel.js: -------------------------------------------------------------------------------- 1 | import { Visualization, Files, DocumentId } from 'datavis-tech-entities'; 2 | 3 | export class VisualizationViewModel { 4 | constructor(visualization) { 5 | this.id = visualization.id; 6 | this.files = visualization.content.files; 7 | this.title = visualization.info.title; 8 | this.description = visualization.info.description; 9 | this.width = 960; // visualization.info.width; 10 | this.height = visualization.info.height || 500; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/web/redux/epics/forkSuccessEpic.js: -------------------------------------------------------------------------------- 1 | import Router from 'next/router'; 2 | import { map, filter } from 'rxjs/operators'; 3 | import { uiRedux } from 'vizhub-ui'; 4 | import { visualizationRoute } from '../../routes/routeGenerators'; 5 | 6 | const { 7 | actionTypes: { 8 | FORK_SUCCESS 9 | } 10 | } = uiRedux; 11 | 12 | export const forkSuccessEpic = (action$, state$) => 13 | action$.ofType(FORK_SUCCESS).pipe( 14 | map(action => Router.push(visualizationRoute(action))), 15 | filter(Boolean) 16 | ); 17 | -------------------------------------------------------------------------------- /packages/database/src/fetchShareDBDoc.js: -------------------------------------------------------------------------------- 1 | import { i18n } from 'datavis-tech-i18n'; 2 | 3 | export const fetchShareDBDoc = (collection, id, connection) => ( 4 | new Promise((resolve, reject) => { 5 | const shareDBDoc = connection.get(collection, id); 6 | shareDBDoc.fetch(error => { 7 | error ? reject(error) : resolve(shareDBDoc); 8 | // : shareDBDoc.type 9 | // ? resolve(shareDBDoc) 10 | // : reject({ message: i18n('errorDocNotFound'), statusCode: 404 }); 11 | }); 12 | }) 13 | ); 14 | -------------------------------------------------------------------------------- /packages/web/redux/actionTypes.js: -------------------------------------------------------------------------------- 1 | const prefix = name => `vizhub-app/${name}`; 2 | 3 | export const START_BUILD = prefix('START_BUILD'); 4 | export const BUILD_ERROR = prefix('BUILD_ERROR'); 5 | export const SET_CSRF_TOKEN = prefix('SET_CSRF_TOKEN'); 6 | export const SET_VISUALIZATION = prefix('SET_VISUALIZATION'); 7 | export const VISUALIZATION_DELETE_SUCCESS = prefix('VISUALIZATION_DELETE_SUCCESS'); 8 | export const VISUALIZATION_DELETE_ERROR = prefix('VISUALIZATION_DELETE_ERROR'); 9 | export const SET_USER = prefix('SET_USER'); 10 | -------------------------------------------------------------------------------- /packages/web/redux/epics/startBuildEpic.js: -------------------------------------------------------------------------------- 1 | import { startBuild } from '../actionCreators'; 2 | import { uiRedux, runDebounceTime } from 'vizhub-ui'; 3 | import { mapTo, debounceTime, filter } from 'rxjs/operators'; 4 | 5 | const { 6 | actionTypes: { 7 | CHANGE_FILE_TEXT 8 | } 9 | } = uiRedux; 10 | 11 | export const startBuildEpic = action$ => 12 | action$.ofType(CHANGE_FILE_TEXT).pipe( 13 | filter(action => action.fileName.endsWith('.js')), 14 | debounceTime(runDebounceTime), 15 | mapTo(startBuild()) 16 | ); 17 | -------------------------------------------------------------------------------- /packages/ui/src/redux/epics/confirmVisualizationDeleteEpic.js: -------------------------------------------------------------------------------- 1 | import { map, filter } from 'rxjs/operators'; 2 | import { DELETE_VISUALIZATION } from '../actionTypes'; 3 | import { visualizationDeleted } from '../actionCreators'; 4 | 5 | export const confirmVisualizationDeleteEpic = action$ => 6 | action$.ofType(DELETE_VISUALIZATION).pipe( 7 | map(action => ( 8 | window.confirm('Are you sure you want to delete this visualization?') 9 | ? visualizationDeleted() 10 | : null 11 | )), 12 | filter(Boolean) 13 | ); 14 | -------------------------------------------------------------------------------- /packages/web/css/variables.sass: -------------------------------------------------------------------------------- 1 | $family-sans-serif: "Ubuntu", sans-serif 2 | $family-monospace: "Ubuntu Mono", monospace 3 | $body-background-color: hsl(0, 0%, 98%) 4 | $radius: 0px 5 | $radius-large: 0px 6 | $input-shadow: none 7 | 8 | @import "../node_modules/bulma/sass/utilities/initial-variables" 9 | @import "../node_modules/bulma/sass/utilities/functions" 10 | 11 | $link: #005261 12 | $link-visited: $grey 13 | $link-focus-border: $grey 14 | $box-shadow: 0 0 0 1px $grey-lighter 15 | 16 | @import "../node_modules/bulma/sass/utilities/derived-variables" 17 | -------------------------------------------------------------------------------- /packages/web/routes/index.js: -------------------------------------------------------------------------------- 1 | import NextRoutes from 'next-routes'; 2 | 3 | export const routes = NextRoutes() 4 | .add('auth/callback', '/auth/callback') 5 | .add('auth/error', '/auth/error') 6 | .add('auth', '/auth') 7 | .add('create-visualization', '/create-visualization') 8 | .add('upload-dataset', '/upload-dataset') 9 | .add('thumbnailsTest', '/thumbnailsTest') 10 | .add('profile', '/:userName') 11 | .add('fullscreen', '/:userName/:id/fullscreen') 12 | .add('visualization', '/:userName/:id') 13 | .add('dataset', '/:userName/datasets/:slug') 14 | -------------------------------------------------------------------------------- /packages/web/components/atoms/titledPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Head from 'next/head'; 3 | import { CommentLink } from './commentLink'; 4 | 5 | export const TitledPage = ({title, children, disableCommentLink}) => { 6 | if (!title) { 7 | throw new Error('The "title" prop is required for TitledPage.'); 8 | } 9 | return ( 10 | 11 | 12 | {title} 13 | 14 | { children } 15 | { disableCommentLink ? null : } 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /packages/web/redux/epics/runNonJS.js: -------------------------------------------------------------------------------- 1 | import { mapTo, filter, debounceTime } from 'rxjs/operators'; 2 | import { uiRedux, runDebounceTime } from 'vizhub-ui'; 3 | 4 | const { 5 | actionTypes: { 6 | CHANGE_FILE_TEXT 7 | }, 8 | actionCreators: { 9 | runFiles 10 | } 11 | } = uiRedux; 12 | 13 | export const runNonJSEpic = action$ => 14 | action$.ofType(CHANGE_FILE_TEXT).pipe( 15 | filter(action => !action.fileName.endsWith('.js')), 16 | filter(action => action.fileName !== 'README.md'), 17 | debounceTime(runDebounceTime), 18 | mapTo(runFiles()) 19 | ); 20 | -------------------------------------------------------------------------------- /packages/database/src/getUserByUserName.js: -------------------------------------------------------------------------------- 1 | import { i18n } from 'datavis-tech-i18n'; 2 | import { USER } from './collectionName'; 3 | import { fetchShareDBQuery } from './fetchShareDBQuery'; 4 | 5 | export const getUserByUserName = connection => async (userName) => { 6 | const mongoQuery = { userName }; 7 | const results = await fetchShareDBQuery(USER, mongoQuery, connection); 8 | if (results.length === 0) { 9 | throw { 10 | message: i18n('errorUserNotFound'), 11 | statusCode: 404 12 | }; 13 | } 14 | const userDoc = results[0]; 15 | return userDoc.data; 16 | } 17 | -------------------------------------------------------------------------------- /packages/database/src/createDataset.js: -------------------------------------------------------------------------------- 1 | import { DOCUMENT_INFO, DOCUMENT_CONTENT } from './collectionName'; 2 | 3 | export const createDataset = connection => dataset => { 4 | const id = dataset.id; 5 | const slug = dataset.info.slug; 6 | 7 | return new Promise(resolve => { 8 | 9 | // TODO handle errors here. 10 | connection.get(DOCUMENT_INFO, id).create(dataset.info); 11 | connection.get(DOCUMENT_CONTENT, id).create(dataset.content); 12 | 13 | // TODO only resolve after document created, 14 | // to avoid race conditions. 15 | resolve({ slug }); 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /packages/web/redux/epics/deleteVisualizationSuccessEpic.js: -------------------------------------------------------------------------------- 1 | import Router from 'next/router'; 2 | import { map, filter } from 'rxjs/operators'; 3 | import { profileRoute } from '../../routes/routeGenerators'; 4 | import { getUserName } from '../selectors'; 5 | import { VISUALIZATION_DELETE_SUCCESS } from '../actionTypes'; 6 | 7 | export const deleteVisualizationSuccessEpic = (action$, state$) => 8 | action$.ofType(VISUALIZATION_DELETE_SUCCESS).pipe( 9 | map(action => Router.push(profileRoute({ 10 | userName: getUserName(state$.value) 11 | }))), 12 | filter(Boolean) 13 | ); 14 | -------------------------------------------------------------------------------- /packages/database/src/saveVisualization.js: -------------------------------------------------------------------------------- 1 | import { DOCUMENT_INFO, DOCUMENT_CONTENT } from './collectionName'; 2 | import { fetchShareDBDoc } from './fetchShareDBDoc'; 3 | import { save } from './save'; 4 | 5 | export const saveVisualization = connection => ({ visualization }) => ( 6 | Promise.all([ 7 | fetchShareDBDoc(DOCUMENT_INFO, visualization.id, connection), 8 | fetchShareDBDoc(DOCUMENT_CONTENT, visualization.id, connection) 9 | ]) 10 | .then(([info, content]) => Promise.all([ 11 | save(info, visualization.info), 12 | save(content, visualization.content) 13 | ])) 14 | ); 15 | -------------------------------------------------------------------------------- /packages/entities/src/dataset.ts: -------------------------------------------------------------------------------- 1 | import { DatasetInfo } from './datasetInfo'; 2 | import { DatasetContent } from './datasetContent'; 3 | import { DocumentId } from './documentId'; 4 | 5 | export class Dataset { 6 | 7 | // The unique ID of this dataset. 8 | id: DocumentId; 9 | 10 | // The info part of this dataset. 11 | info: DatasetInfo; 12 | 13 | // The content part of this dataset. 14 | content: DatasetContent; 15 | 16 | constructor(data) { 17 | this.id = data.datasetInfo.id; 18 | this.info = data.datasetInfo; 19 | this.content = data.datasetContent; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/database/src/getDatasetInfosByUserId.js: -------------------------------------------------------------------------------- 1 | import { i18n } from 'datavis-tech-i18n'; 2 | import { DatasetInfo, DATASET_TYPE } from 'datavis-tech-entities'; 3 | import { DOCUMENT_INFO } from './collectionName'; 4 | import { fetchShareDBQuery } from './fetchShareDBQuery'; 5 | 6 | export const getDatasetInfosByUserId = connection => async (id) => { 7 | const mongoQuery = { 8 | owner: id, 9 | documentType: DATASET_TYPE 10 | }; 11 | const results = await fetchShareDBQuery(DOCUMENT_INFO, mongoQuery, connection); 12 | return results.map(shareDBDoc => new DatasetInfo(shareDBDoc.data)); 13 | } 14 | -------------------------------------------------------------------------------- /packages/database/src/getAllVisualizationInfos.js: -------------------------------------------------------------------------------- 1 | import { i18n } from 'datavis-tech-i18n'; 2 | import { VisualizationInfo, VISUALIZATION_TYPE } from 'datavis-tech-entities'; 3 | import { DOCUMENT_INFO } from './collectionName'; 4 | import { fetchShareDBQuery } from './fetchShareDBQuery'; 5 | 6 | export const getAllVisualizationInfos = connection => async () => { 7 | const mongoQuery = { 8 | documentType: VISUALIZATION_TYPE 9 | }; 10 | const results = await fetchShareDBQuery(DOCUMENT_INFO, mongoQuery, connection); 11 | return results.map(shareDBDoc => new VisualizationInfo(shareDBDoc.data)); 12 | } 13 | -------------------------------------------------------------------------------- /packages/serverGateways/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vizhub-server-gateways", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "src/index.js", 6 | "scripts": { 7 | }, 8 | "author": "", 9 | "license": "UNLICENSED", 10 | "dependencies": { 11 | "@teamwork/sharedb": "^2.0.1", 12 | "@teamwork/sharedb-mingo-memory": "^1.0.5", 13 | "@teamwork/sharedb-mongo": "^2.0.3", 14 | "@teamwork/websocket-json-stream": "^1.1.0", 15 | "mongodb": "^3.1.3", 16 | "datavis-tech-gateways": "1", 17 | "datavis-tech-database": "1" 18 | }, 19 | "devDependencies": { 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/ui/src/redux/epics/index.js: -------------------------------------------------------------------------------- 1 | export { runEpic } from './runEpic'; 2 | export { autoSaveEpic } from './autoSaveEpic'; 3 | export { promptForNewFileNameEpic } from './promptForNewFileNameEpic'; 4 | export { promptForRenameEpic } from './promptForRenameEpic'; 5 | export { promptForNewHeightEpic } from './promptForNewHeightEpic'; 6 | export { confirmFileDeleteEpic } from './confirmFileDeleteEpic'; 7 | export { confirmVisualizationDeleteEpic } from './confirmVisualizationDeleteEpic'; 8 | export { updateTitleEpic } from './updateTitleEpic'; 9 | export { updateDescriptionEpic } from './updateDescriptionEpic'; 10 | -------------------------------------------------------------------------------- /packages/ui/src/redux/reducers/saveStatus.js: -------------------------------------------------------------------------------- 1 | import { 2 | CHANGE_FILE_TEXT, 3 | SAVE, 4 | SAVE_SUCCESS, 5 | SAVE_ERROR, 6 | SET_VISUALIZATION_HEIGHT 7 | } from '../actionTypes'; 8 | 9 | export const saveStatus = (state = 'Saved', action) => { 10 | switch (action.type) { 11 | case SAVE: 12 | return 'Saving'; 13 | case SAVE_SUCCESS: 14 | return 'Saved'; 15 | case SAVE_ERROR: 16 | return action.error; 17 | case CHANGE_FILE_TEXT: 18 | return ''; 19 | case SET_VISUALIZATION_HEIGHT: 20 | return ''; 21 | default: 22 | return state; 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /packages/ui/src/redux/reducers/activeFileName.js: -------------------------------------------------------------------------------- 1 | import { SET_ACTIVE_FILE, FILE_RENAMED, FILE_DELETED } from '../actionTypes'; 2 | 3 | export const activeFileName = (state = null, action) => { 4 | switch (action.type) { 5 | case SET_ACTIVE_FILE: 6 | return action.fileName; 7 | case FILE_RENAMED: 8 | if (state === action.oldFileName) { 9 | return action.newFileName; 10 | } 11 | break; 12 | case FILE_DELETED: 13 | if (state === action.fileName) { 14 | return 'index.html'; 15 | } 16 | break; 17 | default: 18 | return state; 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /packages/ui/src/redux/epics/promptForNewHeightEpic.js: -------------------------------------------------------------------------------- 1 | import { map } from 'rxjs/operators'; 2 | import { SET_HEIGHT_PROMPT } from '../actionTypes'; 3 | import { setVisualizationHeight } from '../actionCreators'; 4 | import { getVisualizationHeight } from '../selectors'; 5 | 6 | export const promptForNewHeightEpic = (action$, state$) => 7 | action$.ofType(SET_HEIGHT_PROMPT).pipe( 8 | map(() => { 9 | const oldHeight = getVisualizationHeight(state$.value); 10 | const newHeight = window.prompt('Please enter a new height', oldHeight); 11 | return setVisualizationHeight(+newHeight); 12 | }) 13 | ); 14 | -------------------------------------------------------------------------------- /packages/controllers/src/apiController/visualizationAPIController/getVisualizationController.js: -------------------------------------------------------------------------------- 1 | import { GetVisualization } from 'datavis-tech-use-cases'; 2 | 3 | export const getVisualizationController = (expressApp, gateways) => { 4 | const getVisualization = new GetVisualization(gateways); 5 | expressApp.get('/api/visualization/get/:id', async (req, res) => { 6 | try { 7 | const requestModel = { id: req.params.id }; 8 | const responseModel = await getVisualization.execute(requestModel); 9 | res.json(responseModel); 10 | } catch (error) { 11 | res.json({ error }) 12 | } 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /packages/entities/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "datavis-tech-entities", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "", 6 | "main": "dist/index.js", 7 | "scripts": { 8 | "build-ts": "tsc", 9 | "postinstall": "npm run build-ts", 10 | "test": "mocha -r ts-node/register test/**/test.ts" 11 | }, 12 | "author": "Curran Kelleher", 13 | "license": "UNLICENSED", 14 | "dependencies": {}, 15 | "devDependencies": { 16 | "@types/mocha": "^5.2.5", 17 | "@types/node": "^10.7.0", 18 | "mocha": "^5.2.0", 19 | "ts-node": "^7.0.1", 20 | "typescript": "^3.0.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/ui/src/redux/epics/promptForRenameEpic.js: -------------------------------------------------------------------------------- 1 | import { map, filter } from 'rxjs/operators'; 2 | import { RENAME_FILE } from '../actionTypes'; 3 | import { fileRenamed } from '../actionCreators'; 4 | 5 | export const promptForRenameEpic = action$ => 6 | action$.ofType(RENAME_FILE).pipe( 7 | map(action => { 8 | const oldFileName = action.fileName; 9 | const newFileName = window.prompt('Please enter a new file name.', oldFileName); 10 | return newFileName && newFileName !== oldFileName 11 | ? fileRenamed(oldFileName, newFileName) 12 | : null; 13 | }), 14 | filter(Boolean) 15 | ); 16 | -------------------------------------------------------------------------------- /packages/ui/src/visualizationRunner/computeSrcDoc.js: -------------------------------------------------------------------------------- 1 | import magicSandbox from 'magic-sandbox'; 2 | 3 | const template = files => { 4 | const indexHtml = files.find(file => file.name === 'index.html'); 5 | return indexHtml ? indexHtml.text : ''; 6 | }; 7 | 8 | const transform = files => ( 9 | files 10 | .filter(file => file.name !== 'index.html') 11 | .reduce((accumulator, file) => { 12 | accumulator[file.name] = { 13 | content: file.text 14 | }; 15 | return accumulator; 16 | }, {}) 17 | ); 18 | 19 | export const computeSrcDoc = files => ( 20 | magicSandbox(template(files), transform(files)) 21 | ); 22 | -------------------------------------------------------------------------------- /packages/database/src/setImagesUpdatedTimestamp.js: -------------------------------------------------------------------------------- 1 | import jsonDiff from 'json0-ot-diff'; 2 | import diffMatchPatch from 'diff-match-patch'; 3 | import { DOCUMENT_INFO } from './collectionName'; 4 | import { fetchShareDBDoc } from './fetchShareDBDoc'; 5 | import { save } from './save'; 6 | 7 | export const setImagesUpdatedTimestamp = connection => options => { 8 | const { id, imagesUpdatedTimestamp } = options; 9 | 10 | return fetchShareDBDoc(DOCUMENT_INFO, id, connection) 11 | .then(doc => { 12 | const newData = Object.assign({}, doc.data, { imagesUpdatedTimestamp }); 13 | return save(doc, newData); 14 | }); 15 | 16 | }; 17 | -------------------------------------------------------------------------------- /packages/controllers/src/apiController/visualizationAPIController/getAllVisualizationInfosController.js: -------------------------------------------------------------------------------- 1 | import { GetAllVisualizationInfos } from 'datavis-tech-use-cases'; 2 | 3 | export const getAllVisualizationInfosController = (expressApp, gateways) => { 4 | const getAllVisualizationInfos = new GetAllVisualizationInfos(gateways); 5 | expressApp.get('/api/visualization/metadata', async (req, res) => { 6 | try { 7 | const { visualizationInfos } = await getAllVisualizationInfos.execute(); 8 | res.json(visualizationInfos); 9 | } catch (error) { 10 | console.log(error); 11 | res.json({ error }) 12 | } 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /packages/database/src/getPreview.js: -------------------------------------------------------------------------------- 1 | import jsonDiff from 'json0-ot-diff'; 2 | import diffMatchPatch from 'diff-match-patch'; 3 | import { PREVIEW_IMAGES } from './collectionName'; 4 | import { fetchShareDBDoc } from './fetchShareDBDoc'; 5 | 6 | export const getPreview = connection => options => { 7 | const { id } = options; 8 | const doc = connection.get(PREVIEW_IMAGES, id); 9 | return new Promise((resolve, reject) => { 10 | doc.fetch(error => { 11 | if (!doc.type) { 12 | reject(new Error('Preview does not exist for document ' + id)); 13 | } else { 14 | resolve(doc.data); 15 | } 16 | }); 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /packages/presenters/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "datavis-tech-presenters", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "dist/index.js", 6 | "private": true, 7 | "scripts": { 8 | "test": "mocha -r esm test/test.js" 9 | }, 10 | "author": "Curran Kelleher", 11 | "license": "UNLICENSED", 12 | "dependencies": { 13 | "datavis-tech-entities": "1", 14 | "datavis-tech-use-cases": "1", 15 | "mocha": "^5.2.0", 16 | "path-posix": "^1.0.0", 17 | "rollup": "^1.1.2", 18 | "rollup-plugin-buble": "^0.19.6" 19 | }, 20 | "devDependencies": { 21 | "esm": "^3.2.4", 22 | "mocha": "^5.2.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/database/src/getThumbnail.js: -------------------------------------------------------------------------------- 1 | import jsonDiff from 'json0-ot-diff'; 2 | import diffMatchPatch from 'diff-match-patch'; 3 | import { THUMBNAIL_IMAGES } from './collectionName'; 4 | import { fetchShareDBDoc } from './fetchShareDBDoc'; 5 | 6 | export const getThumbnail = connection => options => { 7 | const { id } = options; 8 | const doc = connection.get(THUMBNAIL_IMAGES, id); 9 | return new Promise((resolve, reject) => { 10 | doc.fetch(error => { 11 | if (!doc.type) { 12 | reject(new Error('Thumbnail does not exist for document ' + id)); 13 | } else { 14 | resolve(doc.data); 15 | } 16 | }); 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /packages/database/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "datavis-tech-database", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "src/index.js", 6 | "private": true, 7 | "scripts": { 8 | "test": "mocha -r esm" 9 | }, 10 | "author": "Curran Kelleher", 11 | "license": "UNLICENSED", 12 | "dependencies": { 13 | "@teamwork/sharedb": "^2.0.1", 14 | "@teamwork/sharedb-mingo-memory": "^1.0.5", 15 | "datavis-tech-entities": "1", 16 | "datavis-tech-gateways": "1", 17 | "datavis-tech-i18n": "1", 18 | "diff-match-patch": "^1.0.1", 19 | "esm": "^3.0.76", 20 | "json0-ot-diff": "^1.0.3", 21 | "mocha": "^5.2.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/entities/src/visualization.ts: -------------------------------------------------------------------------------- 1 | import { VisualizationInfo } from './visualizationInfo'; 2 | import { VisualizationContent } from './visualizationContent'; 3 | import { DocumentId } from './documentId'; 4 | 5 | export class Visualization { 6 | 7 | // The unique ID of this visualization. 8 | id: DocumentId; 9 | 10 | // The info part of this visualization. 11 | info: VisualizationInfo; 12 | 13 | // The content part of this visualization. 14 | content: VisualizationContent; 15 | 16 | constructor(data) { 17 | this.id = data.visualizationInfo.id; 18 | this.info = data.visualizationInfo; 19 | this.content = data.visualizationContent; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/gateways/src/databaseUserGateway.ts: -------------------------------------------------------------------------------- 1 | import { UserGateway } from 'datavis-tech-use-cases'; 2 | 3 | import { User, UserId } from 'datavis-tech-entities'; 4 | 5 | export class DatabaseUserGateway implements UserGateway { 6 | database: any; 7 | 8 | constructor(database) { 9 | this.database = database; 10 | } 11 | 12 | async createUser(user: User): Promise { 13 | return await this.database.createUser(user); 14 | } 15 | 16 | async getUser(id: UserId) { 17 | return await this.database.getUser(id); 18 | } 19 | 20 | async getUserByUserName(userName: string) { 21 | return await this.database.getUserByUserName(userName); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/i18n/index.ts: -------------------------------------------------------------------------------- 1 | const strings = { 2 | errorNoOwner: { 3 | en: 'No owner specified, cannot create document.' 4 | }, 5 | errorNoId: { 6 | en: 'No id specified, cannot get document.' 7 | }, 8 | errorDocNotFound: { 9 | en: 'The requested document does not exist' 10 | }, 11 | errorUserNotFound: { 12 | en: 'The requested user does not exist' 13 | }, 14 | errorNotOwnerCantSave: { 15 | en: 'Not saved' 16 | }, 17 | errorNotOwnerCantDelete: { 18 | en: 'You must be the owner in order to delete.' 19 | } 20 | }; 21 | 22 | export const defaultLocale = 'en'; 23 | 24 | export const i18n = (name, locale = defaultLocale) => strings[name][locale]; 25 | -------------------------------------------------------------------------------- /packages/gateways/src/databaseImageStorageGateway.ts: -------------------------------------------------------------------------------- 1 | import { ImageStorageGateway } from 'datavis-tech-use-cases'; 2 | 3 | import { Images, DocumentId } from 'datavis-tech-entities'; 4 | 5 | export class DatabaseImageStorageGateway implements ImageStorageGateway { 6 | database: any; 7 | 8 | constructor(database) { 9 | this.database = database; 10 | } 11 | 12 | async updateImages(options){ 13 | return await this.database.updateImages(options); 14 | } 15 | 16 | async getThumbnail(options){ 17 | return await this.database.getThumbnail(options); 18 | } 19 | 20 | async getPreview(options){ 21 | return await this.database.getPreview(options); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/web/server/shareDBServer.js: -------------------------------------------------------------------------------- 1 | //import WebSocket from 'ws'; 2 | //import JSONStream from '@teamwork/websocket-json-stream'; 3 | //import { getShareDB } from './shareDB'; 4 | // 5 | // This is for later, when we use ShareDB in the client side. 6 | 7 | const start = () => { 8 | throw new Error('ShareDB server not implemented'); 9 | // TODO figure out where to put this. Separate package? 10 | // const webSocketServer = new WebSocket.Server({ 11 | // server: httpServer 12 | // }); 13 | 14 | // webSocketServer.on('connection', webSocket => { 15 | // getShareDB().listen(new JSONStream(webSocket)); 16 | // }); 17 | }; 18 | 19 | export const ShareDBServer = { start }; 20 | -------------------------------------------------------------------------------- /packages/database/src/getVisualization.js: -------------------------------------------------------------------------------- 1 | import { 2 | Visualization, 3 | VisualizationInfo, 4 | VisualizationContent 5 | } from 'datavis-tech-entities'; 6 | import { DOCUMENT_INFO, DOCUMENT_CONTENT } from './collectionName'; 7 | import { fetchShareDBDoc } from './fetchShareDBDoc'; 8 | 9 | export const getVisualization = connection => ({ id }) => ( 10 | Promise.all([ 11 | fetchShareDBDoc(DOCUMENT_INFO, id, connection), 12 | fetchShareDBDoc(DOCUMENT_CONTENT, id, connection) 13 | ]) 14 | .then(([info, content]) => new Visualization({ 15 | visualizationInfo: new VisualizationInfo(info.data), 16 | visualizationContent: new VisualizationContent(content.data) 17 | })) 18 | ); 19 | -------------------------------------------------------------------------------- /packages/database/src/getVisualizationInfosByUserId.js: -------------------------------------------------------------------------------- 1 | import { i18n } from 'datavis-tech-i18n'; 2 | import { VisualizationInfo, VISUALIZATION_TYPE } from 'datavis-tech-entities'; 3 | import { DOCUMENT_INFO } from './collectionName'; 4 | import { fetchShareDBQuery } from './fetchShareDBQuery'; 5 | 6 | export const getVisualizationInfosByUserId = connection => async (id) => { 7 | const mongoQuery = { 8 | owner: id, 9 | documentType: VISUALIZATION_TYPE 10 | }; 11 | const results = await fetchShareDBQuery(DOCUMENT_INFO, mongoQuery, connection); 12 | return results 13 | .map(shareDBDoc => new VisualizationInfo(shareDBDoc.data)) 14 | .reverse(); // Show most recent first 15 | } 16 | -------------------------------------------------------------------------------- /packages/database/src/updateImages.js: -------------------------------------------------------------------------------- 1 | import jsonDiff from 'json0-ot-diff'; 2 | import diffMatchPatch from 'diff-match-patch'; 3 | import { THUMBNAIL_IMAGES, PREVIEW_IMAGES } from './collectionName'; 4 | import { fetchShareDBDoc } from './fetchShareDBDoc'; 5 | import { save } from './save'; 6 | 7 | export const updateImages = connection => options => { 8 | const { id, images } = options; 9 | Promise.all([ 10 | fetchShareDBDoc(THUMBNAIL_IMAGES, id, connection), 11 | fetchShareDBDoc(PREVIEW_IMAGES, id, connection) 12 | ]) 13 | .then(([thumbnailDoc, previewDoc]) => Promise.all([ 14 | save(thumbnailDoc, images.thumbnail), 15 | save(previewDoc, images.preview) 16 | ])) 17 | }; 18 | -------------------------------------------------------------------------------- /packages/serverGateways/src/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | DatabaseVisualizationGateway, 3 | DatabaseDatasetGateway, 4 | DatabaseUserGateway, 5 | DatabaseImageStorageGateway 6 | } from 'datavis-tech-gateways'; 7 | import { Database } from 'datavis-tech-database'; 8 | import { getConnection } from './shareDB'; 9 | 10 | export const serverGateways = () => { 11 | const database = Database(getConnection()); 12 | return { 13 | visualizationGateway: new DatabaseVisualizationGateway(database), 14 | datasetGateway: new DatabaseDatasetGateway(database), 15 | userGateway: new DatabaseUserGateway(database), 16 | imageStorageGateway: new DatabaseImageStorageGateway(database), 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /packages/web/css/visualizationPreview.sass: -------------------------------------------------------------------------------- 1 | // Inspired by https://vega.github.io/vega/ 2 | .visualization-preview 3 | display: inline-block 4 | margin: 10px 5 | width: 230px 6 | 7 | .visualization-preview-image 8 | height: 120px 9 | padding: 0px 10 | background-position: left top 11 | background-size: cover 12 | overflow: hidden 13 | position: relative 14 | transition: background-position 3s 15 | outline: 1px solid #ddd 16 | 17 | .visualization-preview:hover .visualization-preview-image 18 | background-position: right bottom 19 | 20 | .visualization-preview-title 21 | width: 230px 22 | overflow: hidden 23 | white-space: nowrap 24 | text-overflow: ellipsis 25 | font-size: 0.8em 26 | -------------------------------------------------------------------------------- /packages/imageGenerationService/src/generateScreenshot.js: -------------------------------------------------------------------------------- 1 | import { computeSrcDoc } from 'vizhub-ui'; 2 | import puppeteer from 'puppeteer'; 3 | 4 | export const generateScreenshot = async ({ visualizationViewModel, waitTime }) => { 5 | const { width, height, files } = visualizationViewModel; 6 | 7 | const html = computeSrcDoc(files); 8 | 9 | const browser = await puppeteer.launch(); 10 | const page = await browser.newPage(); 11 | 12 | await page.setViewport({ width, height }); 13 | await page.setContent(html); 14 | await page.waitFor(waitTime); 15 | 16 | const screenshotBuffer = await page.screenshot(); 17 | await page.close(); 18 | await browser.close(); 19 | 20 | return screenshotBuffer; 21 | }; 22 | -------------------------------------------------------------------------------- /packages/controllers/src/apiController/visualizationAPIController/createVisualizationController.js: -------------------------------------------------------------------------------- 1 | import { CreateVisualization } from 'datavis-tech-use-cases'; 2 | import { userIdFromReq } from '../userIdFromReq'; 3 | 4 | export const createVisualizationController = (expressApp, gateways) => { 5 | const createVisualization = new CreateVisualization(gateways); 6 | 7 | expressApp.get('/api/visualization/create', async (req, res) => { 8 | try { 9 | const requestModel = { owner: userIdFromReq(req) }; 10 | const responseModel = await createVisualization.execute(requestModel); 11 | res.json(responseModel); 12 | } catch (error) { 13 | res.json({ error: error.message }); 14 | } 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /packages/controllers/src/userController.js: -------------------------------------------------------------------------------- 1 | import { User } from 'datavis-tech-entities'; 2 | import { CreateUser, GetUser } from 'datavis-tech-use-cases'; 3 | 4 | export const userController = userGateway => { 5 | const createUser = new CreateUser({ userGateway }); 6 | const getUser = new GetUser({ userGateway }); 7 | return { 8 | createUser: async (_, oAuthProfile) => { 9 | const requestModel = { oAuthProfile }; 10 | const responseModel = await createUser.execute(requestModel); 11 | return responseModel.user; 12 | }, 13 | getUser: async ({ id }) => { 14 | const requestModel = { id }; 15 | const responseModel = await getUser.execute(requestModel); 16 | return responseModel.user; 17 | } 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /packages/database/src/deleteVisualization.js: -------------------------------------------------------------------------------- 1 | import { DOCUMENT_INFO, DOCUMENT_CONTENT } from './collectionName'; 2 | import { fetchShareDBDoc } from './fetchShareDBDoc'; 3 | 4 | export const deleteDoc = doc => new Promise((resolve, reject) => { 5 | doc.del(error => { 6 | error ? reject(error) : resolve({ status: 'success' }) 7 | }); 8 | }); 9 | 10 | export const deleteVisualization = connection => ({ id }) => { 11 | return Promise 12 | .all([ 13 | fetchShareDBDoc(DOCUMENT_INFO, id, connection), 14 | fetchShareDBDoc(DOCUMENT_CONTENT, id, connection) 15 | ]) 16 | .then(([info, content]) => Promise.all([ 17 | deleteDoc(info), 18 | deleteDoc(content) 19 | ])) 20 | .then(() => ({ status: 'success' })); 21 | }; 22 | -------------------------------------------------------------------------------- /packages/useCases/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "datavis-tech-use-cases", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "", 6 | "main": "dist/index.js", 7 | "scripts": { 8 | "build-ts": "tsc", 9 | "postinstall": "npm run build-ts", 10 | "test": "mocha -r ts-node/register test/**/test.ts" 11 | }, 12 | "author": "Curran Kelleher", 13 | "license": "UNLICENSED", 14 | "dependencies": { 15 | "adm-zip": "^0.4.11", 16 | "datavis-tech-entities": "1", 17 | "datavis-tech-i18n": "1", 18 | "uuid": "^3.3.2" 19 | }, 20 | "devDependencies": { 21 | "@types/mocha": "^5.2.5", 22 | "@types/node": "^10.7.0", 23 | "mocha": "^5.2.0", 24 | "ts-node": "^7.0.1", 25 | "typescript": "^3.0.1" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/web/components/page.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NextAuth } from 'next-auth/client'; 3 | import { userFromSession } from '../utils/userFromSession'; 4 | 5 | import '../css/index.sass'; 6 | 7 | // Google Analytics 8 | if (process.browser) { 9 | window.dataLayer = window.dataLayer || []; 10 | function gtag(){dataLayer.push(arguments);} 11 | gtag('js', new Date()); 12 | gtag('config', 'UA-73285761-2'); 13 | } 14 | 15 | export default class Page extends React.Component { 16 | static async getInitialProps({req, query}) { 17 | const session = await NextAuth.init({req}); 18 | return { 19 | user: userFromSession(session), 20 | csrfToken: session.csrfToken, 21 | lang: 'en', 22 | query 23 | }; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/gateways/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "datavis-tech-gateways", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "dist/index.js", 6 | "private": true, 7 | "scripts": { 8 | "build-ts": "tsc", 9 | "postinstall": "npm run build-ts", 10 | "test": "mocha -r ts-node/register test/**/test.ts" 11 | }, 12 | "author": "Curran Kelleher", 13 | "license": "UNLICENSED", 14 | "dependencies": { 15 | "@types/uuid": "^3.4.3", 16 | "datavis-tech-entities": "1", 17 | "datavis-tech-use-cases": "1", 18 | "datavis-tech-i18n": "1" 19 | }, 20 | "devDependencies": { 21 | "@types/mocha": "^5.2.4", 22 | "@types/node": "^10.5.1", 23 | "mocha": "^5.2.0", 24 | "ts-node": "^7.0.0", 25 | "typescript": "^2.9.2" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/serverGateways/src/shareDB.js: -------------------------------------------------------------------------------- 1 | import ShareDB from '@teamwork/sharedb'; 2 | import ShareDBMingoMemory from '@teamwork/sharedb-mingo-memory'; 3 | import ShareDBMongo from '@teamwork/sharedb-mongo'; 4 | 5 | // Singletons. 6 | let shareDB; 7 | let connection; 8 | 9 | export const getShareDB = () => { 10 | if (!shareDB) { 11 | if (process.env.MONGO_URI) { 12 | shareDB = ShareDB({ 13 | db: new ShareDBMongo(process.env.MONGO_URI) 14 | }); 15 | } else { 16 | shareDB = ShareDB({ 17 | db: new ShareDBMingoMemory() 18 | }); 19 | } 20 | } 21 | return shareDB; 22 | }; 23 | 24 | export const getConnection = () => { 25 | if (!connection) { 26 | connection = getShareDB().connect(); 27 | } 28 | return connection; 29 | }; 30 | -------------------------------------------------------------------------------- /packages/controllers/src/apiController/visualizationAPIController/forkVisualizationController.js: -------------------------------------------------------------------------------- 1 | import { ForkVisualization } from 'datavis-tech-use-cases'; 2 | import { userIdFromReq } from '../userIdFromReq'; 3 | 4 | export const forkVisualizationController = (expressApp, gateways) => { 5 | const forkVisualization = new ForkVisualization(gateways); 6 | 7 | expressApp.post('/api/visualization/fork', async (req, res) => { 8 | try { 9 | const requestModel = { 10 | visualization: req.body.visualization, 11 | owner: userIdFromReq(req) 12 | }; 13 | const responseModel = await forkVisualization.execute(requestModel); 14 | res.json(responseModel); 15 | } catch (error) { 16 | res.json({ error: error.message }); 17 | } 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /packages/controllers/src/apiController/visualizationAPIController/saveVisualizationController.js: -------------------------------------------------------------------------------- 1 | import { SaveVisualization } from 'datavis-tech-use-cases'; 2 | import { userIdFromReq } from '../userIdFromReq'; 3 | 4 | export const saveVisualizationController = (expressApp, gateways) => { 5 | const saveVisualization = new SaveVisualization(gateways); 6 | 7 | expressApp.post('/api/visualization/save', async (req, res) => { 8 | try { 9 | const requestModel = { 10 | visualization: req.body.visualization, 11 | userId: userIdFromReq(req) 12 | }; 13 | const responseModel = await saveVisualization.execute(requestModel); 14 | res.json(responseModel); 15 | } catch (error) { 16 | res.json({ error: error.message }) 17 | } 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /packages/web/components/atoms/visualizationPreview.js: -------------------------------------------------------------------------------- 1 | import { visualizationRoute, thumbnailUrl } from '../../routes/routeGenerators'; 2 | 3 | export const VisualizationPreview = ({ info, userName }) => ( 4 | 9 |
18 |
19 | { info.title } 20 |
21 |
22 | ) 23 | -------------------------------------------------------------------------------- /packages/controllers/src/apiController/userAPIController/getProfileDataController.js: -------------------------------------------------------------------------------- 1 | import { GetUserProfileData } from 'datavis-tech-use-cases'; 2 | 3 | export const getProfileDataController = (expressApp, gateways) => { 4 | 5 | const getUserProfileData = new GetUserProfileData(gateways); 6 | 7 | expressApp.get('/api/user/getProfileData/:userName', async (req, res) => { 8 | try { 9 | const { userName } = req.params; 10 | const requestModel = { userName }; 11 | const responseModel = await getUserProfileData.execute(requestModel); 12 | res.json(responseModel); 13 | } catch (error) { 14 | res.json({ 15 | error: { 16 | message: error.message, 17 | statusCode: error.statusCode 18 | } 19 | }); 20 | } 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /packages/controllers/src/apiController/visualizationAPIController/deleteVisualizationController.js: -------------------------------------------------------------------------------- 1 | import { DeleteVisualization } from 'datavis-tech-use-cases'; 2 | import { userIdFromReq } from '../userIdFromReq'; 3 | 4 | export const deleteVisualizationController = (expressApp, gateways) => { 5 | const deleteVisualization = new DeleteVisualization(gateways); 6 | 7 | expressApp.post('/api/visualization/delete', async (req, res) => { 8 | try { 9 | const requestModel = { 10 | id: req.body.id, 11 | userId: userIdFromReq(req) 12 | }; 13 | const responseModel = await deleteVisualization.execute(requestModel); 14 | res.json(responseModel); 15 | } catch (error) { 16 | res.json({ 17 | error: error.message 18 | }); 19 | } 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /packages/controllers/src/apiController/datasetAPIController/createDatasetController.js: -------------------------------------------------------------------------------- 1 | import { CreateDataset } from 'datavis-tech-use-cases'; 2 | import { userIdFromReq } from '../userIdFromReq'; 3 | 4 | export const createDatasetController = (expressApp, gateways) => { 5 | const createDataset = new CreateDataset(gateways); 6 | 7 | expressApp.post('/api/dataset/create', async (req, res) => { 8 | try { 9 | const owner = userIdFromReq(req); 10 | const { title, file, sourceName, sourceUrl } = req.body; 11 | 12 | const requestModel = { owner, title, file, sourceName, sourceUrl }; 13 | const responseModel = await createDataset.execute(requestModel); 14 | res.json(responseModel); 15 | } catch (error) { 16 | res.json({ error: error.message }); 17 | } 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /packages/presenters/src/d3Packages.js: -------------------------------------------------------------------------------- 1 | // Derived from https://github.com/d3/d3/blob/master/index.js 2 | export const d3Packages = [ 3 | 'd3', 4 | // 'd3-array', 5 | // 'd3-axis', 6 | // 'd3-brush', 7 | // 'd3-chord', 8 | // 'd3-collection', 9 | // 'd3-color', 10 | // 'd3-contour', 11 | // 'd3-dispatch', 12 | // 'd3-drag', 13 | // 'd3-dsv', 14 | // 'd3-ease', 15 | // 'd3-fetch', 16 | // 'd3-force', 17 | // 'd3-format', 18 | // 'd3-geo', 19 | // 'd3-hierarchy', 20 | // 'd3-interpolate', 21 | // 'd3-path', 22 | // 'd3-polygon', 23 | // 'd3-quadtree', 24 | // 'd3-random', 25 | // 'd3-scale', 26 | // 'd3-scale-chromatic', 27 | // 'd3-selection', 28 | // 'd3-shape', 29 | // 'd3-time', 30 | // 'd3-time-format', 31 | // 'd3-timer', 32 | // 'd3-transition', 33 | // 'd3-voronoi', 34 | // 'd3-zoom' 35 | ]; 36 | -------------------------------------------------------------------------------- /packages/web/LICENSE_NEXTJS_STARTER.txt: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2018, Iain Collins 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -------------------------------------------------------------------------------- /packages/useCases/src/gatewayInterfaces/datasetGateway.ts: -------------------------------------------------------------------------------- 1 | import { UserId, DocumentId, File, DatasetInfo } from 'datavis-tech-entities'; 2 | 3 | import { 4 | CreateDatasetRequestModel, 5 | CreateDatasetResponseModel, 6 | GetDatasetRequestModel, 7 | GetDatasetResponseModel, 8 | //SaveDatasetRequestModel, 9 | //SaveDatasetResponseModel 10 | } from '../interactors'; 11 | 12 | export interface DatasetGateway { 13 | createDataset(request: CreateDatasetRequestModel): 14 | Promise; 15 | 16 | getDataset(options: { 17 | owner: UserId, 18 | slug: string 19 | }): Promise; 20 | 21 | getDatasetInfosByUserId(id: UserId): Promise<[DatasetInfo]>; 22 | 23 | //saveDataset(request: SaveDatasetRequestModel): 24 | // Promise; 25 | } 26 | -------------------------------------------------------------------------------- /packages/web/components/atoms/unfurl.js: -------------------------------------------------------------------------------- 1 | export const Unfurl = ({ title, description, image, url }) => ( 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ) 18 | -------------------------------------------------------------------------------- /packages/imageGenerationService/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vizhub-image-generation-service", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "start": "NODE_ENV=production node -r esm src/index.js", 8 | "dev": "NODE_ENV=development node -r esm src/index.js", 9 | "test": "mocha -r esm test/test.js" 10 | }, 11 | "author": "Curran Kelleher", 12 | "license": "UNLICENSED", 13 | "dependencies": { 14 | "datavis-tech-entities": "1", 15 | "datavis-tech-presenters": "1", 16 | "datavis-tech-use-cases": "1", 17 | "dotenv": "^6.1.0", 18 | "puppeteer": "^1.8.0", 19 | "sharp": "^0.20.8", 20 | "vizhub-server-gateways": "1", 21 | "vizhub-ui": "1" 22 | }, 23 | "devDependencies": { 24 | "esm": "^3.0.84", 25 | "mocha": "^5.2.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/ui/src/ide/ideGrid.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SplitPane from 'react-split-pane'; 3 | 4 | export const IDEGrid = props => { 5 | const { 6 | children, 7 | onSplitPaneDragStarted, 8 | onSplitPaneDragFinished 9 | } = props; 10 | 11 | return ( 12 |
13 | 19 | { children } 20 | 21 |
22 | ); 23 | }; 24 | 25 | IDEGrid.Left = ({children}) => ( 26 |
27 | { children } 28 |
29 | ); 30 | 31 | IDEGrid.Right = ({children}) => ( 32 |
33 | { children } 34 |
35 | ); 36 | -------------------------------------------------------------------------------- /packages/web/pages/auth/error.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Page from '../../components/page'; 3 | import { TitledPage } from '../../components/atoms/titledPage'; 4 | 5 | export default class extends Page { 6 | 7 | static async getInitialProps({req, query}) { 8 | let props = await super.getInitialProps({req}); 9 | props.action = query.action || null; 10 | props.type = query.type || null; 11 | props.service = query.service || null; 12 | return props; 13 | } 14 | 15 | render() { 16 | return ( 17 | 18 |
19 |

Error signing in

20 |

An error occured while trying to sign in.

21 |
22 |
23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/web/pages/upload-dataset/fileUploader.js: -------------------------------------------------------------------------------- 1 | const onChange = onFileChosen => event => { 2 | const file = event.target.files[0]; 3 | const reader = new FileReader(); 4 | reader.onload = () => { 5 | onFileChosen({ 6 | name: file.name, 7 | text: reader.result 8 | }); 9 | }; 10 | reader.readAsText(file); 11 | }; 12 | 13 | export const FileUploader = ({ onFileChosen }) => ( 14 |
15 | 27 |
28 | ); 29 | -------------------------------------------------------------------------------- /packages/controllers/src/apiController/visualizationAPIController/getPreviewController.js: -------------------------------------------------------------------------------- 1 | import { GetPreview } from 'datavis-tech-use-cases'; 2 | 3 | export const getPreviewController = (expressApp, gateways) => { 4 | const getPreview = new GetPreview(gateways); 5 | expressApp.get('/api/visualization/preview/:id.png', async (req, res) => { 6 | try { 7 | const id = req.params.id; 8 | const requestModel = { id }; 9 | const responseModel = await getPreview.execute(requestModel); 10 | const preview = responseModel.preview; 11 | 12 | const img = new Buffer(preview, 'base64'); 13 | 14 | res.writeHead(200, { 15 | 'Content-Type': 'image/png', 16 | 'Content-Length': img.length 17 | }); 18 | 19 | res.end(img); 20 | } catch (error) { 21 | res.json({ error }) 22 | } 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /packages/controllers/src/apiController/visualizationAPIController/getThumbnailController.js: -------------------------------------------------------------------------------- 1 | import { GetThumbnail } from 'datavis-tech-use-cases'; 2 | 3 | export const getThumbnailController = (expressApp, gateways) => { 4 | const getThumbnail = new GetThumbnail(gateways); 5 | expressApp.get('/api/visualization/thumbnail/:id.png', async (req, res) => { 6 | try { 7 | const id = req.params.id; 8 | const requestModel = { id }; 9 | const responseModel = await getThumbnail.execute(requestModel); 10 | const thumbnail = responseModel.thumbnail; 11 | 12 | const img = new Buffer(thumbnail, 'base64'); 13 | 14 | res.writeHead(200, { 15 | 'Content-Type': 'image/png', 16 | 'Content-Length': img.length 17 | }); 18 | 19 | res.end(img); 20 | } catch (error) { 21 | res.json({ error }) 22 | } 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /packages/entities/src/visualizationContent.ts: -------------------------------------------------------------------------------- 1 | import { DocumentContent } from './documentContent'; 2 | import { File } from './file'; 3 | import { VISUALIZATION_TYPE } from './documentTypes'; 4 | 5 | export class VisualizationContent extends DocumentContent { 6 | 7 | // A representation of "files". 8 | // 9 | // Expected files include: 10 | // 11 | // index.html - Required, the main HTML page. 12 | // index.js - Optional, the JS entry point. 13 | // May import other JS files in this Visualization using ES6 imports, 14 | // `import { something } from './${filename}';` 15 | // styles.css - Optional, CSS file, could be any name ending with .css. 16 | files: File[]; 17 | 18 | constructor(data) { 19 | super({ 20 | id: data.id, 21 | documentType: VISUALIZATION_TYPE 22 | }); 23 | 24 | this.files = data.files; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/entities/src/index.ts: -------------------------------------------------------------------------------- 1 | export { User, UserId } from './user'; 2 | export { ciUser } from './ciUser'; 3 | 4 | export { DocumentPart } from './documentPart'; 5 | export { DocumentInfo } from './documentInfo'; 6 | export { DocumentContent } from './documentContent'; 7 | 8 | export { VisualizationInfo } from './visualizationInfo'; 9 | export { VisualizationContent } from './visualizationContent'; 10 | export { Visualization } from './visualization'; 11 | export { File } from './file'; 12 | 13 | export { DatasetInfo } from './datasetInfo'; 14 | export { DatasetContent } from './datasetContent'; 15 | export { Dataset } from './dataset'; 16 | 17 | export { VISUALIZATION_TYPE, DATASET_TYPE } from './documentTypes'; 18 | 19 | import * as testData from './testData'; 20 | export { testData }; 21 | 22 | export { timestamp } from './timestamp'; 23 | 24 | export { Images } from './images'; 25 | -------------------------------------------------------------------------------- /packages/controllers/src/apiController/visualizationAPIController/exportVisualizationController.js: -------------------------------------------------------------------------------- 1 | import { ExportVisualization } from 'datavis-tech-use-cases'; 2 | 3 | export const exportVisualizationController = (expressApp, gateways) => { 4 | const exportVisualization = new ExportVisualization(gateways); 5 | expressApp.get('/api/visualization/export/:id', async (req, res) => { 6 | try { 7 | const requestModel = { id: req.params.id }; 8 | const responseModel = await exportVisualization.execute(requestModel); 9 | 10 | const { zipFileBuffer, zipFileName } = responseModel; 11 | 12 | res.set({ 13 | 'Content-Disposition': `attachment; filename="${zipFileName}"`, 14 | 'Content-Type': 'application/zip' 15 | }); 16 | 17 | res.send(zipFileBuffer); 18 | 19 | } catch (error) { 20 | res.json({ error }) 21 | } 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /packages/database/src/getDataset.js: -------------------------------------------------------------------------------- 1 | import { Dataset, DatasetInfo, DatasetContent, DATASET_TYPE } from 'datavis-tech-entities'; 2 | import { DOCUMENT_INFO, DOCUMENT_CONTENT } from './collectionName'; 3 | import { fetchShareDBDoc } from './fetchShareDBDoc'; 4 | import { fetchShareDBQuery } from './fetchShareDBQuery'; 5 | 6 | export const getDataset = connection => async ({ owner, slug }) => { 7 | 8 | const mongoQuery = { 9 | documentType: DATASET_TYPE, 10 | slug, 11 | owner 12 | }; 13 | const results = await fetchShareDBQuery(DOCUMENT_INFO, mongoQuery, connection); 14 | const info = results[0]; 15 | 16 | const content = await fetchShareDBDoc(DOCUMENT_CONTENT, info.id, connection); 17 | 18 | return { 19 | dataset: new Dataset({ 20 | datasetInfo: new DatasetInfo(info.data), 21 | datasetContent: new DatasetContent(content.data) 22 | }) 23 | }; 24 | }; 25 | -------------------------------------------------------------------------------- /packages/ui/src/css/visualizationView.sass: -------------------------------------------------------------------------------- 1 | .visualization-view 2 | position: relative 3 | display: flex 4 | flex-direction: column 5 | min-height: 100%; 6 | font-family: $family-sans 7 | iframe.visualization-runner 8 | position: absolute 9 | .visualization-view-body 10 | flex: 1 11 | margin-left: 5px 12 | margin-top: 5px 13 | .visualization-view-title 14 | font-size: 2.5em 15 | .visualization-view-description 16 | margin-top: 12px 17 | .responsive-youtube 18 | margin-left: -5px 19 | position: relative 20 | padding-bottom: 56.25% /* 16:9 */ 21 | padding-top: 25px 22 | height: 0 23 | .responsive-youtube iframe 24 | position: absolute 25 | top: 0 26 | left: 0 27 | width: 100% 28 | height: 100% 29 | .license-info 30 | color: #cccccc 31 | margin-left: 5px 32 | margin-top: 5px 33 | -------------------------------------------------------------------------------- /packages/web/routes/routeGenerators.js: -------------------------------------------------------------------------------- 1 | export const absolute = relative => 'https://vizhub.com' + relative; 2 | 3 | export const thumbnailUrl = id => `/api/visualization/thumbnail/${id}.png`; 4 | export const previewUrl = id => `/api/visualization/preview/${id}.png`; 5 | 6 | export const profileRoute = ({userName}) => 7 | `/${userName}`; 8 | 9 | export const visualizationRoute = ({userName, id}) => 10 | `/${userName}/${id}`; 11 | 12 | export const visualizationRouteFullscreen = ({userName, id}) => 13 | `/${userName}/${id}/fullscreen`; 14 | 15 | export const datasetRoute = ({userName, slug}) => 16 | `/${userName}/datasets/${slug}`; 17 | 18 | export const datasetUrl = ({userName, slug}) => 19 | `https://vizhub.com${datasetRoute({userName, slug})}`; 20 | 21 | export const datasetDownloadUrl = ({userName, slug, format, baseUrl}) => 22 | `${baseUrl}${datasetRoute({userName, slug})}.${format}`; 23 | -------------------------------------------------------------------------------- /packages/web/next.config.js: -------------------------------------------------------------------------------- 1 | const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); 2 | const withSass = require('@zeit/next-sass'); 3 | const withCSS = require('@zeit/next-css'); 4 | const commonsChunkConfig = require('@zeit/next-css/commons-chunk-config'); 5 | const { ANALYZE } = process.env; 6 | 7 | module.exports = withSass(withCSS({ 8 | webpack: config => { 9 | config.resolve.extensions = [ '.mjs', '.js', '.jsx', '.json' ]; 10 | 11 | // Enable use of both .sass and .css. 12 | // Should be a temporary workaround. 13 | // See https://github.com/zeit/next-plugins/issues/127 14 | config = commonsChunkConfig(config, /\.(sass|css)$/); 15 | 16 | if (ANALYZE) { 17 | config.plugins.push(new BundleAnalyzerPlugin({ 18 | analyzerMode: 'server', 19 | analyzerPort: 8888, 20 | openAnalyzer: true 21 | })); 22 | } 23 | 24 | return config; 25 | } 26 | })); 27 | -------------------------------------------------------------------------------- /packages/web/models/user.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A simple client-side only model used to list users in the Admin view, which 3 | * is acessible if logged in on a profile that has { "admin": true } set on it. 4 | * It is not used for anything else. 5 | */ 6 | import fetch from 'isomorphic-fetch'; 7 | 8 | export default class { 9 | 10 | static async list({ 11 | page = 0, 12 | size = 10 13 | } = {}) { 14 | return fetch(`/admin/users?page=${page}&size=${size}`, { 15 | credentials: 'same-origin' 16 | }) 17 | .then(response => { 18 | if (response.ok) { 19 | return Promise.resolve(response.json()); 20 | } else { 21 | return Promise.reject(Error('HTTP error when trying to list users')); 22 | } 23 | }) 24 | .then(data => { 25 | return data; 26 | }) 27 | .catch(() => Promise.reject(Error('Error trying to list users'))); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'globals': { 3 | // Globals for Mocha 4 | 'it': false, 5 | 'describe': false, 6 | 'after': false 7 | }, 8 | 'env': { 9 | 'browser': true, 10 | 'es6': true, 11 | 'node': true 12 | }, 13 | 'extends': ['eslint:recommended', 'plugin:react/recommended'], 14 | 'parserOptions': { 15 | 'ecmaFeatures': { 16 | 'jsx': true 17 | }, 18 | 'ecmaVersion': 2018, 19 | 'sourceType': 'module' 20 | }, 21 | 'plugins': [ 22 | 'react' 23 | ], 24 | 'rules': { 25 | 'indent': [ 26 | 'error', 27 | 2 28 | ], 29 | 'linebreak-style': [ 30 | 'error', 31 | 'unix' 32 | ], 33 | 'quotes': [ 34 | 'error', 35 | 'single' 36 | ], 37 | 'semi': [ 38 | 'error', 39 | 'always' 40 | ], 41 | 'no-console': 0, 42 | 'react/react-in-jsx-scope': 0, 43 | 'react/prop-types': 0 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /packages/ui/src/redux/epics/updateTitleEpic.js: -------------------------------------------------------------------------------- 1 | import { merge } from 'rxjs'; 2 | import { debounceTime, map, filter } from 'rxjs/operators'; 3 | import { CHANGE_FILE_TEXT, INIT_FILES } from '../actionTypes'; 4 | import { setTitle } from '../actionCreators'; 5 | import { runDebounceTime } from '../../constants'; 6 | import { getFile } from '../selectors'; 7 | 8 | const extractTitle = htmlFile => { 9 | const titleMatch = htmlFile.text.match(/(.*?)<\/title>/i); 10 | return titleMatch ? titleMatch[1] : 'Untitled'; 11 | }; 12 | 13 | export const updateTitleEpic = (action$, state$) => 14 | merge( 15 | action$.ofType(INIT_FILES), 16 | action$.ofType(CHANGE_FILE_TEXT).pipe( 17 | filter(action => action.fileName === 'index.html'), 18 | debounceTime(runDebounceTime) 19 | ), 20 | ) 21 | .pipe( 22 | map(action => 23 | setTitle(extractTitle(getFile(state$.value, 'index.html'))) 24 | ) 25 | ); 26 | -------------------------------------------------------------------------------- /packages/ui/src/css/editorGrid.sass: -------------------------------------------------------------------------------- 1 | .editor-grid 2 | display: flex 3 | height: 100% 4 | background-color: #300a24 5 | @media only screen and (max-width: $editor-break-small) 6 | flex-direction: column 7 | .editor-grid-left 8 | @media screen and (max-width: $editor-break-small) 9 | border-bottom: $editor-split-border 10 | @media screen and (min-width: $editor-break-small) 11 | border-right: $editor-split-border 12 | color: white 13 | font-size: $font-size-for-code 14 | font-family: $family-monospace 15 | .file-list:focus 16 | outline: none 17 | .file, .action-link 18 | padding-left: 10px 19 | padding-right: 10px 20 | cursor: pointer 21 | user-select: none 22 | .file.active 23 | background-color: #6c6c6c 24 | .action-link 25 | color: #7a5173 26 | display: block 27 | .editor-grid-center 28 | flex: 1 1 auto 29 | min-width: 0 30 | 31 | -------------------------------------------------------------------------------- /packages/useCases/src/interactors/getUser.ts: -------------------------------------------------------------------------------- 1 | import { User, UserId, ciUser } from 'datavis-tech-entities'; 2 | import { Interactor, RequestModel, ResponseModel } from '../interactor'; 3 | import { UserGateway } from '../gatewayInterfaces/userGateway' 4 | 5 | export interface GetUserRequestModel extends RequestModel { 6 | id: UserId, 7 | } 8 | 9 | export interface GetUserResponseModel extends ResponseModel { 10 | user: User 11 | } 12 | 13 | export class GetUser implements Interactor { 14 | userGateway: UserGateway; 15 | 16 | constructor({ userGateway }) { 17 | this.userGateway = userGateway; 18 | } 19 | 20 | async execute(requestModel: GetUserRequestModel) { 21 | const { id } = requestModel; 22 | const responseModel: GetUserResponseModel = { 23 | user: ( 24 | id === ciUser.id 25 | ? ciUser 26 | : await this.userGateway.getUser(id) 27 | ) 28 | }; 29 | return responseModel; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /graveyard/computeReferences/test/test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { computeReferences } from '../src/computeReferences'; 3 | //import { presentVisualization } from '../src'; 4 | 5 | describe('Presenter', () => { 6 | describe('computeReferences', () => { 7 | it('should compute references', async () => { 8 | const references = await computeReferences() 9 | assert.deepEqual(references, [ 'd3-selection', './bar' ]); 10 | }); 11 | }); 12 | 13 | //describe('presentVisualization', () => { 14 | // it('should compute references', async () => { 15 | // //const gateway = { 16 | // // fetchDocument: id => Promise.resolve({}), 17 | // // fetchUser: id => Promise.resolve({}) 18 | // //}; 19 | // //const presenter = Presenter(gateway); 20 | // const { references } = await presentVisualization() 21 | // assert.deepEqual(references, [ 'd3-selection', './bar' ]); 22 | // }); 23 | //}); 24 | }); 25 | -------------------------------------------------------------------------------- /packages/useCases/src/interactors/getAllVisualizationInfos.ts: -------------------------------------------------------------------------------- 1 | import { User, VisualizationInfo, ciUser } from 'datavis-tech-entities'; 2 | import { Interactor, RequestModel, ResponseModel } from '../interactor'; 3 | import { UserGateway } from '../gatewayInterfaces/userGateway' 4 | import { VisualizationGateway } from '../gatewayInterfaces/visualizationGateway' 5 | import { DatasetGateway } from '../gatewayInterfaces/datasetGateway' 6 | 7 | export interface GetAllVisualizationInfosResponseModel extends ResponseModel { 8 | visualizationInfos: [VisualizationInfo] 9 | } 10 | 11 | export class GetAllVisualizationInfos implements Interactor { 12 | visualizationGateway: VisualizationGateway; 13 | 14 | constructor({ visualizationGateway }) { 15 | this.visualizationGateway = visualizationGateway; 16 | } 17 | 18 | async execute() { 19 | return { 20 | visualizationInfos: await this.visualizationGateway.getAllVisualizationInfos() 21 | }; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/imageGenerationService/src/generateImages.js: -------------------------------------------------------------------------------- 1 | import { VisualizationViewModel } from 'datavis-tech-presenters'; 2 | import { generateScreenshot } from './generateScreenshot'; 3 | import { resize } from './resize'; 4 | import { thumbnailDimensions, previewDimensions } from './dimensions'; 5 | 6 | export const defaultWaitTime = 10000; 7 | 8 | export const generateImages = async (visualization, waitTime) => { 9 | const visualizationViewModel = new VisualizationViewModel(visualization); 10 | 11 | const screenshotBuffer = await generateScreenshot({ 12 | visualizationViewModel, 13 | waitTime: waitTime || defaultWaitTime 14 | }); 15 | 16 | const thumbnailBuffer = await resize({ 17 | actualDimensions: visualizationViewModel, 18 | desiredDimensions: thumbnailDimensions, 19 | screenshotBuffer 20 | }); 21 | 22 | return { 23 | thumbnail: thumbnailBuffer.toString('base64'), 24 | preview: screenshotBuffer.toString('base64') 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /packages/web/pages/_document.js: -------------------------------------------------------------------------------- 1 | import Document, { Head, Main, NextScript } from 'next/document'; 2 | 3 | export default class DefaultDocument extends Document { 4 | static async getInitialProps (ctx) { 5 | return await Document.getInitialProps(ctx); 6 | } 7 | 8 | render() { 9 | return ( 10 | <html lang={this.props.__NEXT_DATA__.props.lang || 'en'}> 11 | <Head> 12 | <meta charSet="UTF-8" /> 13 | <meta name="viewport" content="width=device-width, initial-scale=1"/> 14 | <link rel="stylesheet" href="/_next/static/style.css" /> 15 | <link href="https://fonts.googleapis.com/css?family=Ubuntu|Ubuntu+Mono" rel="stylesheet" /> 16 | <script async src="https://www.googletagmanager.com/gtag/js?id=UA-73285761-2"></script> 17 | </Head> 18 | <body> 19 | {this.props.customValue} 20 | <Main /> 21 | <NextScript /> 22 | </body> 23 | </html> 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/entities/src/datasetInfo.ts: -------------------------------------------------------------------------------- 1 | import { DocumentInfo } from './documentInfo'; 2 | import { DATASET_TYPE } from './documentTypes'; 3 | 4 | type Format = 'csv' | 'tsv' | 'json' | 'geojson' | 'topojson' | 'txt'; 5 | 6 | export class DatasetInfo extends DocumentInfo { 7 | 8 | // The format of this dataset. 9 | format: Format; 10 | 11 | // The name of the source of this dataset. 12 | sourceName: string; 13 | 14 | // The URL of the page from which this dataset was downloaded. 15 | sourceUrl: string 16 | 17 | constructor(data) { 18 | 19 | super({ 20 | documentType: DATASET_TYPE, 21 | id: data.id, 22 | owner: data.owner, 23 | title: data.title, 24 | slug: data.slug, 25 | description: data.description, 26 | createdTimestamp: data.createdTimestamp, 27 | lastUpdatedTimestamp: data.lastUpdatedTimestamp 28 | }); 29 | 30 | this.format = data.format; 31 | this.sourceName = data.sourceName; 32 | this.sourceUrl = data.sourceUrl; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /graveyard/computeReferences/src/index.ts: -------------------------------------------------------------------------------- 1 | import { computeReferences } from './computeReferences'; 2 | import { CVResponse } from 'datavis-tech-use-cases'; 3 | 4 | export interface VisualizationViewModel { 5 | references: string[] 6 | } 7 | 8 | export async function presentVisualization (): Promise<VisualizationViewModel> { 9 | const references = await computeReferences(); 10 | return { references }; 11 | }; 12 | 13 | //export const presentVisualization = gateway => ({ 14 | // presentVisualization: id => ( 15 | // gateway.fetchDocument(id).then(async visualization => { 16 | // //return await Promise 17 | // // .all([ 18 | // // Promise.all(references.map(gateway.fetchDocument)), 19 | // // gateway.fetchUser(visualization.owner) 20 | // // ]) 21 | // // .then((referencedDocuments, owner) => ({ 22 | // // visualization, 23 | // // references, 24 | // // referencedDocuments, 25 | // // owner 26 | // // })); 27 | // }) 28 | // ) 29 | //}); 30 | -------------------------------------------------------------------------------- /packages/ui/src/redux/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | import { files } from './files'; 4 | import { activeFileName } from './activeFileName'; 5 | import { visualizationWidth } from './visualizationWidth'; 6 | import { visualizationHeight } from './visualizationHeight'; 7 | import { visualizationOwnerUser } from './visualizationOwnerUser'; 8 | import { runId } from './runId'; 9 | import { saveStatus } from './saveStatus'; 10 | import { visualizationTitle } from './visualizationTitle'; 11 | import { visualizationId } from './visualizationId'; 12 | import { visualizationDescription } from './visualizationDescription'; 13 | import { splitPaneDragging } from './splitPaneDragging'; 14 | 15 | export const ide = combineReducers({ 16 | files, 17 | activeFileName, 18 | visualizationWidth, 19 | visualizationHeight, 20 | visualizationOwnerUser, 21 | runId, 22 | saveStatus, 23 | visualizationTitle, 24 | visualizationId, 25 | visualizationDescription, 26 | splitPaneDragging 27 | }); 28 | -------------------------------------------------------------------------------- /packages/ui/src/visualizationRunner/runnerIframe.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | export class RunnerIFrame extends Component { 3 | 4 | constructor(props) { 5 | super(props); 6 | this.iFrameRef = React.createRef(); 7 | this.setSrcDoc = () => { 8 | this.iFrameRef.current.setAttribute('srcDoc', this.props.srcDoc); 9 | }; 10 | } 11 | 12 | componentDidMount() { 13 | this.setSrcDoc(); 14 | } 15 | 16 | componentDidUpdate(prevProps) { 17 | if (prevProps.runId !== this.props.runId) { 18 | console.clear(); 19 | this.setSrcDoc(); 20 | } 21 | } 22 | 23 | render() { 24 | const { width, height, scale } = this.props; 25 | return ( 26 | <iframe 27 | className='visualization-runner' 28 | ref={this.iFrameRef} 29 | width={width} 30 | height={height} 31 | title='Visualization Runner' 32 | style={{ 33 | transform: `scale(${scale})`, 34 | transformOrigin: '0 0', 35 | }} 36 | /> 37 | ); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /packages/web/server/next-auth.providers.js: -------------------------------------------------------------------------------- 1 | // Load environment variables from a .env file if one exists 2 | require('dotenv').load(); 3 | 4 | const DEV_GITHUB_ID = '75389c43f767c2dd2347'; 5 | const DEV_GITHUB_SECRET = '99a50654eb1244953dcbbe9093703ff83c537b08'; 6 | 7 | module.exports = () => { 8 | let providers = []; 9 | 10 | providers.push({ 11 | providerName: 'GitHub', 12 | providerOptions: { 13 | scope: ['email', 'public_profile'] 14 | }, 15 | Strategy: require('passport-github').Strategy, 16 | strategyOptions: { 17 | clientID: process.env.GITHUB_ID || DEV_GITHUB_ID, 18 | clientSecret: process.env.GITHUB_SECRET || DEV_GITHUB_SECRET, 19 | profileFields: ['id', 'displayName', 'email', 'link'] 20 | }, 21 | getProfile(profile) { 22 | // Normalize profile into one with {id, name, email} keys 23 | return { 24 | id: profile.id, 25 | name: profile._json.name, 26 | email: profile._json.email 27 | }; 28 | } 29 | }); 30 | 31 | return providers; 32 | }; 33 | -------------------------------------------------------------------------------- /packages/web/redux/reducers.js: -------------------------------------------------------------------------------- 1 | import { 2 | SET_CSRF_TOKEN, 3 | SET_VISUALIZATION, 4 | SET_USER 5 | } from './actionTypes'; 6 | import { uiRedux } from 'vizhub-ui'; 7 | 8 | const { actionTypes: { SAVE_ERROR } } = uiRedux; 9 | 10 | export const csrfToken = (state = null, action) => { 11 | switch (action.type) { 12 | case SET_CSRF_TOKEN: 13 | return action.csrfToken || null; 14 | default: 15 | return state; 16 | } 17 | }; 18 | 19 | export const visualization = (state = null, action) => { 20 | switch (action.type) { 21 | case SET_VISUALIZATION: 22 | return action.visualization; 23 | default: 24 | return state; 25 | } 26 | }; 27 | 28 | export const user = (state = null, action) => { 29 | switch (action.type) { 30 | case SET_USER: 31 | return action.user; 32 | default: 33 | return state; 34 | } 35 | }; 36 | 37 | export const showForkInvitation = (state = false, action) => { 38 | switch (action.type) { 39 | case SAVE_ERROR: 40 | return true; 41 | default: 42 | return state; 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /packages/entities/src/documentInfo.ts: -------------------------------------------------------------------------------- 1 | import { DocumentPart } from './documentPart'; 2 | import { UserId } from './user'; 3 | 4 | export class DocumentInfo extends DocumentPart { 5 | 6 | // The ID of the user that owns this document. 7 | owner: UserId; 8 | 9 | // The title of the document. 10 | title: string; 11 | 12 | // The URL slug for the document. 13 | slug: string | undefined; 14 | 15 | // The Markdown description of the document. 16 | description: string; 17 | 18 | // The Unix timestamp at which this document was created. 19 | createdTimestamp: number; 20 | 21 | // The Unix timestamp at which this document was last updated. 22 | lastUpdatedTimestamp: number; 23 | 24 | constructor(data) { 25 | super({ 26 | id: data.id, 27 | documentType: data.documentType 28 | }); 29 | this.owner = data.owner; 30 | this.title = data.title; 31 | this.slug = data.slug; 32 | this.description = data.description; 33 | this.createdTimestamp = data.createdTimestamp; 34 | this.lastUpdatedTimestamp = data.lastUpdatedTimestamp; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/useCases/createVisualization.md: -------------------------------------------------------------------------------- 1 | # Create Visualization 2 | 3 | Actor: Student, Teacher 4 | 5 | A new visualization is to be created. 6 | 7 | ## Triggers 8 | 9 | Access the user interface element for creating a new visualization. 10 | 11 | ## Preconditions 12 | 13 | The user is authenticated. 14 | 15 | ## Postconditions 16 | 17 | A new visualization was created. 18 | 19 | ## Normal Course of Events 20 | 21 | * A user interface for creating a new visualization appears. 22 | * The user is asked whether they want to 23 | * start from scratch, or 24 | * fork a template. 25 | * The user confirms that the visualization should be created. 26 | * The user is informed that the visualization was created. 27 | * The user is presented with the visualization editor interface. 28 | 29 | ### Choose a Template 30 | 31 | * The user may chooses a template to start from. 32 | * In this case, the template visualization will be "forked". 33 | 34 | ## Exceptional Case 35 | 36 | * If the user is not authenticated, the user should be informed that they need to log in to create a visualization. -------------------------------------------------------------------------------- /packages/ui/src/visualizationEditor/fileList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | const BACKSPACE = 8; 5 | const DELETE = 46; 6 | 7 | export const FileList = props => { 8 | const { 9 | files, 10 | activeFileName, 11 | onFileClick, 12 | onFileDoubleClick, 13 | onFileDelete 14 | } = props; 15 | 16 | const isActive = file => file.name === activeFileName; 17 | 18 | const onKeyDown = event => { 19 | if (event.keyCode === BACKSPACE || event.keyCode === DELETE) { 20 | onFileDelete(activeFileName); 21 | } 22 | }; 23 | 24 | return ( 25 | <div className='file-list' tabIndex='0' onKeyDown={onKeyDown}> 26 | { 27 | files.map(file => ( 28 | <div 29 | key={file.name} 30 | className={classNames('file', { active: isActive(file) })} 31 | onClick={() => onFileClick(file.name)} 32 | onDoubleClick={() => onFileDoubleClick(file.name)} 33 | > 34 | { file.name } 35 | </div> 36 | )) 37 | } 38 | </div> 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /packages/ui/src/testingApp/files.js: -------------------------------------------------------------------------------- 1 | const defaultIndexHTML = 2 | `<!DOCTYPE html> 3 | <html> 4 | <head> 5 | <meta charset="utf-8"> 6 | <meta name="viewport" content="width=device-width"> 7 | <title>Untitled 8 | 9 | 10 | 11 | 12 | Hello 13 | 14 | `; 15 | 16 | const defaultStylesCSS = 17 | `body { 18 | background-color: red; 19 | margin: 0px; 20 | font-size: 5em; 21 | }`; 22 | 23 | const defaultIndexJS = 24 | `console.log('Hello World!');`; 25 | 26 | const defaultReadmeMD = 27 | `This is a cool [dataviz](https://twitter.com/search?q=%23dataviz)! 28 | 29 | `; 30 | 31 | export const files = [ 32 | { name: 'index.html', text: defaultIndexHTML }, 33 | { name: 'styles.css', text: defaultStylesCSS }, 34 | { name: 'index.js', text: defaultIndexJS }, 35 | { name: 'README.md', text: defaultReadmeMD }, 36 | ]; 37 | -------------------------------------------------------------------------------- /packages/ui/src/redux/epics/updateDescriptionEpic.js: -------------------------------------------------------------------------------- 1 | import marked from 'marked'; 2 | import { merge } from 'rxjs'; 3 | import { debounceTime, map, filter } from 'rxjs/operators'; 4 | import { CHANGE_FILE_TEXT, INIT_FILES } from '../actionTypes'; 5 | import { setDescription } from '../actionCreators'; 6 | import { runDebounceTime } from '../../constants'; 7 | import { getFile } from '../selectors'; 8 | 9 | const responsiveYouTube = html => html 10 | .replace(//g, match => 11 | `
${match}
` 12 | ); 13 | 14 | const renderMarkdown = markdownFile => 15 | markdownFile 16 | ? responsiveYouTube(marked(markdownFile.text)) 17 | : ''; 18 | 19 | export const updateDescriptionEpic = (action$, state$) => 20 | merge( 21 | action$.ofType(INIT_FILES), 22 | action$.ofType(CHANGE_FILE_TEXT).pipe( 23 | filter(action => action.fileName === 'README.md'), 24 | debounceTime(runDebounceTime) 25 | ), 26 | ) 27 | .pipe( 28 | map(action => ( 29 | setDescription(renderMarkdown(getFile(state$.value, 'README.md'))) 30 | )) 31 | ); 32 | -------------------------------------------------------------------------------- /packages/useCases/src/interactors/getPreview.ts: -------------------------------------------------------------------------------- 1 | import { Images, User, DocumentId } from 'datavis-tech-entities'; 2 | import { i18n } from 'datavis-tech-i18n'; 3 | import { Interactor, RequestModel, ResponseModel } from '../interactor'; 4 | import { ImageStorageGateway } from '../gatewayInterfaces/imageStorageGateway'; 5 | import { UserGateway } from '../gatewayInterfaces/userGateway'; 6 | 7 | export interface GetPreviewRequestModel extends RequestModel { 8 | id: DocumentId 9 | } 10 | 11 | export interface GetPreviewResponseModel extends ResponseModel { 12 | preview: string 13 | } 14 | 15 | export class GetPreview implements Interactor { 16 | imageStorageGateway: ImageStorageGateway; 17 | 18 | constructor({ imageStorageGateway, userGateway }) { 19 | this.imageStorageGateway = imageStorageGateway; 20 | } 21 | 22 | async execute(requestModel: GetPreviewRequestModel) { 23 | if (!requestModel.id) { 24 | throw new Error(i18n('errorNoId')) 25 | } 26 | 27 | const preview = await this.imageStorageGateway.getPreview({ 28 | id: requestModel.id 29 | }); 30 | 31 | return { preview }; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Datavis Tech INC 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 | -------------------------------------------------------------------------------- /packages/web/pages/create-visualization/body.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { VisualizationPreview } from '../../components/atoms/visualizationPreview'; 3 | 4 | export const BodyAuthenticated = ({onFromScratchClick, templates}) => { 5 | const fromScratchClick = e => { 6 | e.preventDefault(); 7 | onFromScratchClick(); 8 | }; 9 | 10 | return process.env.NODE_ENV === 'development' 11 | ? ( 12 | 13 | Start from scratch 14 | 15 | ) 16 | : ( 17 | 18 |
19 | Create a visualization by forking one of these starter templates. 20 |
21 |
22 | { 23 | templates.map(info => 24 | 25 | ) 26 | } 27 |
28 |
29 | ); 30 | }; 31 | 32 | export const BodyNotAuthenticated = () => ( 33 |
You must first log in to create a visualization.
34 | ); 35 | -------------------------------------------------------------------------------- /packages/ui/src/css/ideGrid.sass: -------------------------------------------------------------------------------- 1 | // This element is created by react-split-pane. 2 | // Styling inspired by https://github.com/tomkp/react-split-pane#example-styling 3 | .Resizer 4 | background: #000 5 | opacity: .2 6 | 7 | // Above CodeMirror, whose editor is 2 and scrollbar is 6. 8 | z-index: 7 9 | 10 | box-sizing: border-box 11 | background-clip: padding-box 12 | .Resizer.vertical 13 | width: 15px 14 | margin-right: -15px 15 | border-left: 0px solid rgba(255, 255, 255, 0) 16 | border-right: 15px solid rgba(255, 255, 255, 0) 17 | cursor: col-resize 18 | .Resizer.vertical:hover 19 | border-left: 0px solid rgba(0, 0, 0, 0.5) 20 | border-right: 15px solid rgba(0, 0, 0, 0.5) 21 | 22 | .ide-grid 23 | height: 100% 24 | position: relative 25 | .Pane2 26 | min-width: 0px 27 | .ide-grid-left 28 | height: 100% 29 | .save-status 30 | position: absolute 31 | top: 0px 32 | right: 15px 33 | font-family: $family-monospace 34 | font-size: $font-size-for-code 35 | color: rgba(173, 127, 168, 0.6) 36 | .ide-grid-right 37 | height: 100% 38 | overflow-x: hidden 39 | overflow-y: auto 40 | -------------------------------------------------------------------------------- /packages/useCases/src/interactors/getThumbnail.ts: -------------------------------------------------------------------------------- 1 | import { Images, User, DocumentId } from 'datavis-tech-entities'; 2 | import { i18n } from 'datavis-tech-i18n'; 3 | import { Interactor, RequestModel, ResponseModel } from '../interactor'; 4 | import { ImageStorageGateway } from '../gatewayInterfaces/imageStorageGateway'; 5 | import { UserGateway } from '../gatewayInterfaces/userGateway'; 6 | 7 | export interface GetThumbnailRequestModel extends RequestModel { 8 | id: DocumentId 9 | } 10 | 11 | export interface GetThumbnailResponseModel extends ResponseModel { 12 | thumbnail: string 13 | } 14 | 15 | export class GetThumbnail implements Interactor { 16 | imageStorageGateway: ImageStorageGateway; 17 | 18 | constructor({ imageStorageGateway, userGateway }) { 19 | this.imageStorageGateway = imageStorageGateway; 20 | } 21 | 22 | async execute(requestModel: GetThumbnailRequestModel) { 23 | if (!requestModel.id) { 24 | throw new Error(i18n('errorNoId')) 25 | } 26 | 27 | const thumbnail = await this.imageStorageGateway.getThumbnail({ 28 | id: requestModel.id 29 | }); 30 | 31 | return { thumbnail }; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/ui/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Datavis Tech INC 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 | -------------------------------------------------------------------------------- /packages/gateways/test/test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { 3 | DatabaseVisualizationGateway, 4 | DatabaseDatasetGateway 5 | } from '../src'; 6 | 7 | describe('Visualization Gateway', () => { 8 | 9 | describe('createVisualization', () => { 10 | it('should invoke db if success.', done => { 11 | const database = { 12 | createVisualization: async () => ({ id: '123' }) 13 | }; 14 | const visualizationGateway = new DatabaseVisualizationGateway(database); 15 | visualizationGateway.createVisualization({ owner: 'bob' }) 16 | .then(({id}) => { 17 | assert.equal(id, '123'); 18 | done(); 19 | }); 20 | }); 21 | }); 22 | 23 | describe('createDataset', () => { 24 | it('should invoke db if success.', done => { 25 | const database = { 26 | createDataset: async () => ({ id: '123' }) 27 | }; 28 | const datasetGateway = new DatabaseDatasetGateway(database); 29 | datasetGateway.createDataset({ owner: 'bob' }) 30 | .then(({id}) => { 31 | assert.equal(id, '123'); 32 | done(); 33 | }); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /packages/imageGenerationService/src/service.js: -------------------------------------------------------------------------------- 1 | import { serverGateways } from 'vizhub-server-gateways'; 2 | import { UpdateImages } from 'datavis-tech-use-cases'; 3 | import { generateImages, defaultWaitTime } from './generateImages'; 4 | 5 | const noop = () => {}; 6 | 7 | // Give some time after image generation for the images to be stored. 8 | // Otherwise we'd generate the same image twice in a row. 9 | const downTime = 1000; 10 | 11 | export const startService = ({ waitTime = defaultWaitTime }) => { 12 | 13 | const gateways = Object.assign({}, serverGateways(), { 14 | imageGeneratorGateway: { 15 | generateImages 16 | } 17 | }); 18 | 19 | const updateImages = new UpdateImages(gateways, waitTime); 20 | 21 | console.log('Image generation service starting...'); 22 | 23 | let loop = () => { 24 | console.log('Checking for visualizations in need of images..'); 25 | Promise.all([ 26 | updateImages.execute(), 27 | new Promise(resolve => setTimeout(resolve, waitTime + downTime)) 28 | ]).then(loop); 29 | } 30 | loop(); 31 | 32 | return { 33 | stopService: () => { 34 | loop = noop; 35 | } 36 | }; 37 | }; 38 | -------------------------------------------------------------------------------- /packages/entities/src/visualizationInfo.ts: -------------------------------------------------------------------------------- 1 | import { DocumentInfo } from './documentInfo'; 2 | import { DocumentId } from './documentId'; 3 | import { VISUALIZATION_TYPE } from './documentTypes'; 4 | 5 | export class VisualizationInfo extends DocumentInfo { 6 | 7 | // The visualization that this visualization was forked from. 8 | forkedFrom: DocumentId | undefined; 9 | 10 | height: number; 11 | 12 | // The Unix timestamp at which the thumbnail and preview 13 | // images for this visualization were last updated. 14 | // A value of undefined means there were never any images generated. 15 | imagesUpdatedTimestamp: number | undefined; 16 | 17 | constructor(data) { 18 | super({ 19 | documentType: VISUALIZATION_TYPE, 20 | id: data.id, 21 | owner: data.owner, 22 | title: data.title, 23 | slug: data.slug, 24 | description: data.description, 25 | createdTimestamp: data.createdTimestamp, 26 | lastUpdatedTimestamp: data.lastUpdatedTimestamp 27 | }); 28 | 29 | this.forkedFrom = data.forkedFrom; 30 | this.height = data.height; 31 | this.imagesUpdatedTimestamp = data.imagesUpdatedTimestamp; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/web/redux/actionCreators.js: -------------------------------------------------------------------------------- 1 | import { uiRedux } from 'vizhub-ui'; 2 | const { actionTypes: { BUILD_FINISHED } } = uiRedux; 3 | 4 | import { 5 | START_BUILD, 6 | BUILD_ERROR, 7 | SET_CSRF_TOKEN, 8 | SET_VISUALIZATION, 9 | VISUALIZATION_DELETE_ERROR, 10 | VISUALIZATION_DELETE_SUCCESS, 11 | SET_USER, 12 | } from './actionTypes'; 13 | 14 | export const startBuild = () => ({ 15 | type: START_BUILD 16 | }); 17 | 18 | // TODO move into ui package 19 | export const buildFinished = files => ({ 20 | type: BUILD_FINISHED, 21 | files 22 | }); 23 | 24 | export const buildError = error => ({ 25 | type: BUILD_ERROR, 26 | message: error.message 27 | }); 28 | 29 | export const setCsrfToken = csrfToken => ({ 30 | type: SET_CSRF_TOKEN, 31 | csrfToken 32 | }); 33 | 34 | export const setVisualization = visualization => ({ 35 | type: SET_VISUALIZATION, 36 | visualization 37 | }); 38 | 39 | export const visualizationDeleteSuccess = () => ({ 40 | type: VISUALIZATION_DELETE_SUCCESS 41 | }); 42 | 43 | export const visualizationDeleteError = () => ({ 44 | type: VISUALIZATION_DELETE_ERROR 45 | }); 46 | 47 | export const setUser = user => ({ 48 | type: SET_USER, 49 | user 50 | }); 51 | -------------------------------------------------------------------------------- /packages/ui/src/redux/reducers/files.js: -------------------------------------------------------------------------------- 1 | import unionBy from 'lodash/fp/unionBy'; 2 | import { 3 | INIT_FILES, 4 | CHANGE_FILE_TEXT, 5 | BUILD_FINISHED, 6 | NEW_FILE_CREATED, 7 | FILE_RENAMED, 8 | FILE_DELETED 9 | } from '../actionTypes'; 10 | 11 | export const files = (state = [], action) => { 12 | switch (action.type) { 13 | case INIT_FILES: 14 | return action.files; 15 | case CHANGE_FILE_TEXT: 16 | return state.map(file => ( 17 | file.name === action.fileName 18 | ? Object.assign({}, file, { text: action.text }) 19 | : file 20 | )); 21 | case BUILD_FINISHED: 22 | return unionBy(file => file.name)(action.files)(state); 23 | case NEW_FILE_CREATED: 24 | const newFile = { name: action.fileName, text: '' }; 25 | return unionBy(file => file.name)([newFile])(state); 26 | case FILE_RENAMED: 27 | return state.map(file => ( 28 | file.name === action.oldFileName 29 | ? Object.assign(file, { name: action.newFileName }) 30 | : file 31 | )); 32 | case FILE_DELETED: 33 | return state.filter(file => file.name !== action.fileName) 34 | default: 35 | return state; 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /packages/useCases/src/interactors/saveVisualization.ts: -------------------------------------------------------------------------------- 1 | import { Visualization, UserId, timestamp } from 'datavis-tech-entities'; 2 | import { i18n } from 'datavis-tech-i18n'; 3 | import { Interactor, RequestModel, ResponseModel } from '../interactor'; 4 | import { VisualizationGateway } from '../gatewayInterfaces/visualizationGateway' 5 | 6 | export interface SaveVisualizationRequestModel extends RequestModel { 7 | visualization: Visualization, 8 | userId: UserId 9 | } 10 | 11 | export interface SaveVisualizationResponseModel extends ResponseModel { 12 | status: string 13 | } 14 | 15 | export class SaveVisualization implements Interactor { 16 | visualizationGateway: VisualizationGateway; 17 | 18 | constructor({ visualizationGateway }) { 19 | this.visualizationGateway = visualizationGateway; 20 | } 21 | 22 | async execute(requestModel: SaveVisualizationRequestModel) { 23 | const { visualization, userId } = requestModel; 24 | if (visualization.info.owner !== userId) { 25 | throw new Error(i18n('errorNotOwnerCantSave')) 26 | } 27 | visualization.info.lastUpdatedTimestamp = timestamp(); 28 | return await this.visualizationGateway.saveVisualization(requestModel); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/web/pages/thumbnailsTest.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Page from '../components/page'; 3 | import { TitledPage } from '../components/atoms/titledPage'; 4 | import { VisualizationPreview } from '../components/atoms/visualizationPreview'; 5 | import { getJSON } from '../utils/getJSON'; 6 | import { testData } from 'datavis-tech-entities'; 7 | 8 | const { visualizationInfo } = testData; 9 | 10 | // For testing long titles. 11 | visualizationInfo.title = 'fdhsasjkf dhsasjkfl dhsasjk lfhda sdjklfh as djk has klhfjksd'; 12 | 13 | export default class extends Page { 14 | static async getInitialProps({req}) { 15 | const props = await super.getInitialProps({ req }); 16 | props.metadata = await getJSON(`/api/visualization/metadata`, req);; 17 | return props; 18 | } 19 | render() { 20 | return ( 21 | 22 |

Preview

23 | 24 | { 25 | this.props.metadata.map(info => ( 26 | 27 | )) 28 | } 29 |
30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/ui/src/redux/selectors.js: -------------------------------------------------------------------------------- 1 | export const getFiles = state => state.ide.files; 2 | 3 | export const getActiveFileName = state => state.ide.activeFileName; 4 | 5 | export const getActiveFileText = state => { 6 | const files = getFiles(state); 7 | const activeFileName = getActiveFileName(state); 8 | return files && activeFileName 9 | ? files.find(({name}) => name === activeFileName).text 10 | : ''; 11 | }; 12 | 13 | export const getVisualizationWidth = state => state.ide.visualizationWidth; 14 | export const getVisualizationHeight = state => state.ide.visualizationHeight; 15 | export const getRunId = state => state.ide.runId; 16 | export const getSaveStatus = state => state.ide.saveStatus; 17 | export const getVisualizationTitle = state => state.ide.visualizationTitle; 18 | export const getVisualizationId = state => state.ide.visualizationId; 19 | export const getVisualizationDescription = state => state.ide.visualizationDescription; 20 | 21 | export const getVisualizationOwnerUser = state => 22 | state.ide.visualizationOwnerUser; 23 | 24 | export const getFile = (state, fileName) => getFiles(state) 25 | .filter(file => file.name === fileName)[0]; 26 | 27 | export const getSplitPaneDragging = state => state.ide.splitPaneDragging; 28 | -------------------------------------------------------------------------------- /packages/web/redux/epics/deleteVisualizationEpic.js: -------------------------------------------------------------------------------- 1 | import { defer } from 'rxjs'; 2 | import { switchMap } from 'rxjs/operators'; 3 | import { uiRedux } from 'vizhub-ui'; 4 | import { getVisualization } from '../selectors'; 5 | import { 6 | visualizationDeleteSuccess, 7 | visualizationDeleteError 8 | } from '../actionCreators'; 9 | 10 | const { 11 | actionTypes: { 12 | VISUALIZATION_DELETED 13 | } 14 | } = uiRedux; 15 | 16 | export const deleteVisualizationEpic = (action$, state$) => 17 | action$.ofType(VISUALIZATION_DELETED).pipe( 18 | switchMap(action => defer(async () => { 19 | 20 | const state = state$.value; 21 | const visualization = getVisualization(state); 22 | const id = visualization.id; 23 | 24 | const url = `/api/visualization/delete`; 25 | const options = { 26 | credentials: 'include', 27 | method: 'POST', 28 | headers: { 29 | 'Content-Type': 'application/json' 30 | }, 31 | body: JSON.stringify({ id }) 32 | }; 33 | const response = await (await fetch(url, options)).json(); 34 | 35 | return response.error 36 | ? visualizationDeleteError(response.error) 37 | : visualizationDeleteSuccess(); 38 | })) 39 | ); 40 | -------------------------------------------------------------------------------- /packages/useCases/src/interactors/createVisualization/visualizationDefaults.ts: -------------------------------------------------------------------------------- 1 | const files = [ 2 | 3 | { name: 'index.html', text: 4 | ` 5 | 6 | 7 | 8 | 9 | Untitled 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | `}, 18 | 19 | { name: 'styles.css', text: 20 | `body { 21 | background-color: red; 22 | margin 0px; 23 | font-size: 8em; 24 | }`}, 25 | 26 | { name: 'index.js', text: 27 | `import { select } from 'd3'; 28 | import { message } from './message'; 29 | select('#message').text(message);`}, 30 | 31 | { name: 'message.js', text: 32 | `export const message = "D3 and ES6 imports are working !"`}, 33 | 34 | { name: 'README.md', text: 35 | `This is a cool [dataviz](https://twitter.com/search?q=%23dataviz)!`}, 36 | ]; 37 | 38 | export const visualizationDefaults = { 39 | title: 'Untitled', 40 | slug: undefined, 41 | description: 'No description', 42 | files, 43 | forkedFrom: undefined, 44 | height: undefined 45 | }; 46 | -------------------------------------------------------------------------------- /ecosystem.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps : [ 3 | { 4 | name: 'web', 5 | script: 'cd packages/web; npm run build && npm start', 6 | 7 | // Options reference: https://pm2.io/doc/en/runtime/reference/ecosystem-file/ 8 | instances: 1, 9 | autorestart: true, 10 | watch: false, 11 | max_memory_restart: '1G', 12 | env: { 13 | NODE_ENV: 'development' 14 | }, 15 | env_production: { 16 | NODE_ENV: 'production' 17 | } 18 | }, 19 | { 20 | name: 'thumbnailsService', 21 | script: 'packages/imageGenerationService/src/index.js', 22 | node_args: '-r esm', 23 | instances: 1, 24 | autorestart: true, 25 | watch: false, 26 | max_memory_restart: '1G', 27 | env: { 28 | NODE_ENV: 'development' 29 | }, 30 | env_production: { 31 | NODE_ENV: 'production' 32 | } 33 | } 34 | ], 35 | /* 36 | deploy : { 37 | production : { 38 | user : 'node', 39 | host : '212.83.163.1', 40 | ref : 'origin/master', 41 | repo : 'git@github.com:repo.git', 42 | path : '/var/www/production', 43 | 'post-deploy' : 'npm install && pm2 reload ecosystem.config.js --env production' 44 | } 45 | } 46 | */ 47 | }; 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "bootstrap": "lerna bootstrap && echo -ne '\\007';sleep 0.2; echo -ne '\\007'; sleep 0.2; echo -ne '\\007' && npm run testAll", 4 | "lint": "eslint . --fix", 5 | "test": "cd packages/entities && npm test && cd ../useCases && npm test && cd ../gateways && npm test && cd ../presenters && npm test && cd ../controllers && npm test && cd ../database && npm test && echo 'All tests pass!'", 6 | "testAll": "npm run test && cd packages/web && npm test", 7 | "startServer": "cd packages/web; npm run --silent dev &", 8 | "stopServer": "kill $(lsof -t -i:3000)", 9 | "ci": "lerna bootstrap && npm run startServer && npm run testAll && npm run stopServer", 10 | "updateDeps": "npm install -g npm-check-updates; ncu -a; cd packages/entities && ncu -a && cd ../useCases && ncu -a cd ../gateways && ncu -a && cd ../presenters && ncu -a && cd ../controllers && ncu -a && cd ../database && ncu -a && cd ../web && ncu -a && cd ../ui && ncu -a && echo 'All dependencies upgraded!'" 11 | }, 12 | "devDependencies": { 13 | "eslint": "^5.3.0", 14 | "eslint-config-airbnb-base": "^13.1.0", 15 | "eslint-plugin-import": "^2.14.0", 16 | "eslint-plugin-node": "^7.0.1", 17 | "eslint-plugin-react": "^7.11.0", 18 | "esm": "^3.0.84", 19 | "lerna": "^3.0.4" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /graveyard/computeReferences/src/computeReferences.ts: -------------------------------------------------------------------------------- 1 | import { rollup } from 'rollup'; 2 | import * as virtual from 'rollup-plugin-virtual'; 3 | 4 | // A Rollup plugin that detects and reports all unresolved references. 5 | const detective = ({ detect }) => { 6 | return { 7 | name: 'detective', 8 | resolveId: (id: string) => { 9 | detect(id); 10 | return `detected-${id}`; 11 | }, 12 | load: () => 'detective-stub' 13 | }; 14 | }; 15 | 16 | export async function computeReferences(): Promise { 17 | 18 | const indexJS = ` 19 | import { selection } from "d3-selection"; 20 | import { bar } from "./bar"; 21 | export const foo = "test"; 22 | `; 23 | 24 | const references: string[] = []; 25 | 26 | await rollup({ 27 | input: 'index.js', 28 | plugins: [ 29 | virtual({ 30 | 'index.js': indexJS 31 | }), 32 | detective({ 33 | detect: id => references.push(id) 34 | }) 35 | ] 36 | }); 37 | 38 | // const bundle = await rollup(inputOptions); 39 | // const { code, map } = await bundle.generate({ format: 'iife', name: 'bundle' }); 40 | // console.log(code); 41 | // const localFileReferences = detective.references.filter(str => str.startsWith('./')); 42 | // console.log(localFileReferences); 43 | 44 | return references; 45 | } 46 | -------------------------------------------------------------------------------- /packages/entities/src/user.ts: -------------------------------------------------------------------------------- 1 | 2 | // A unique identifier for a user. 3 | export type UserId = string; 4 | 5 | export class User { 6 | 7 | // Whether or not the user is authenticated. 8 | authenticated: boolean; 9 | 10 | // The unique ID of this user. 11 | id: UserId; 12 | 13 | // This user's unique camelCase user name. 14 | userName: string; 15 | 16 | // This user's full name (first name, last name). 17 | fullName: string; 18 | 19 | // The email address of this user. 20 | email: string; 21 | 22 | // The URL for loading this user's avatar image. 23 | avatarUrl: string; 24 | 25 | // The company that this user works for (if provided). 26 | company: string; 27 | 28 | // The URL of the website or blog associated with this user (if provided). 29 | website: string; 30 | 31 | // The physical location of this user (if provided). 32 | location: string; 33 | 34 | // Biography. 35 | bio: string; 36 | 37 | constructor(data) { 38 | this.authenticated = data.authenticated; 39 | this.id = data.id; 40 | this.userName = data.userName; 41 | this.fullName = data.fullName; 42 | this.email = data.email; 43 | this.avatarUrl = data.avatarUrl; 44 | this.company = data.company; 45 | this.website = data.website; 46 | this.location = data.location; 47 | this.bio = data.bio; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/useCases/src/interactors/createUser.ts: -------------------------------------------------------------------------------- 1 | import { User } from 'datavis-tech-entities'; 2 | import { Interactor, RequestModel, ResponseModel } from '../interactor'; 3 | import { UserGateway } from '../gatewayInterfaces/userGateway' 4 | 5 | export interface CreateUserRequestModel extends RequestModel { 6 | oAuthProfile: any 7 | } 8 | 9 | export interface CreateUserResponseModel extends ResponseModel { 10 | user: User 11 | } 12 | 13 | export class CreateUser implements Interactor { 14 | userGateway: UserGateway; 15 | 16 | constructor({ userGateway }) { 17 | this.userGateway = userGateway; 18 | } 19 | 20 | async execute(requestModel: CreateUserRequestModel) { 21 | const { 22 | oAuthProfile: { 23 | id, 24 | username, 25 | _json: { 26 | name, 27 | avatar_url, 28 | company, 29 | blog, 30 | location, 31 | email, 32 | bio 33 | } 34 | } 35 | } = requestModel; 36 | 37 | const user = new User({ 38 | id, 39 | userName: username, 40 | fullName: name, 41 | email, 42 | avatarUrl: avatar_url, 43 | company, 44 | website: blog, 45 | location, 46 | bio 47 | }); 48 | 49 | return { 50 | user: await this.userGateway.createUser(user) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/useCases/src/interactors/getDataset.ts: -------------------------------------------------------------------------------- 1 | import { Dataset, UserId, ciUser } from 'datavis-tech-entities'; 2 | import { i18n } from 'datavis-tech-i18n'; 3 | import { Interactor, RequestModel, ResponseModel } from '../interactor'; 4 | import { DatasetGateway } from '../gatewayInterfaces/datasetGateway' 5 | import { UserGateway } from '../gatewayInterfaces/userGateway' 6 | 7 | export interface GetDatasetRequestModel extends RequestModel { 8 | userName: string, 9 | slug: string 10 | } 11 | 12 | export interface GetDatasetResponseModel extends ResponseModel { 13 | dataset: Dataset 14 | } 15 | 16 | export class GetDataset implements Interactor { 17 | datasetGateway: DatasetGateway; 18 | userGateway: UserGateway; 19 | 20 | constructor({ userGateway, datasetGateway }) { 21 | this.userGateway = userGateway; 22 | this.datasetGateway = datasetGateway; 23 | } 24 | 25 | async execute(requestModel: GetDatasetRequestModel) { 26 | const { slug, userName } = requestModel; 27 | if (!slug) { 28 | throw new Error(i18n('errorNoId')) 29 | } 30 | 31 | const ownerUser = ( 32 | userName === ciUser.userName 33 | ? ciUser 34 | : await this.userGateway.getUserByUserName(userName) 35 | ); 36 | 37 | return await this.datasetGateway.getDataset({ 38 | owner: ownerUser.id, 39 | slug 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/controllers/src/apiController/visualizationAPIController/index.js: -------------------------------------------------------------------------------- 1 | import { createVisualizationController } from './createVisualizationController'; 2 | import { getVisualizationController } from './getVisualizationController'; 3 | import { exportVisualizationController } from './exportVisualizationController'; 4 | import { saveVisualizationController } from './saveVisualizationController'; 5 | import { deleteVisualizationController } from './deleteVisualizationController'; 6 | import { forkVisualizationController } from './forkVisualizationController'; 7 | import { getAllVisualizationInfosController } from './getAllVisualizationInfosController'; 8 | import { getThumbnailController } from './getThumbnailController'; 9 | import { getPreviewController } from './getPreviewController'; 10 | 11 | export const visualizationAPIController = (expressApp, gateways) => { 12 | createVisualizationController(expressApp, gateways); 13 | getVisualizationController(expressApp, gateways); 14 | exportVisualizationController(expressApp, gateways); 15 | saveVisualizationController(expressApp, gateways); 16 | deleteVisualizationController(expressApp, gateways); 17 | forkVisualizationController(expressApp, gateways); 18 | getAllVisualizationInfosController(expressApp, gateways); 19 | getThumbnailController(expressApp, gateways); 20 | getPreviewController(expressApp, gateways); 21 | }; 22 | -------------------------------------------------------------------------------- /packages/web/css/index.sass: -------------------------------------------------------------------------------- 1 | @charset "utf-8" 2 | 3 | @import "./variables" 4 | 5 | @import "../node_modules/bulma/sass/utilities/animations" 6 | @import "../node_modules/bulma/sass/utilities/mixins" 7 | @import "../node_modules/bulma/sass/utilities/controls" 8 | 9 | @import "../node_modules/bulma/sass/base/_all" 10 | 11 | //@import "../node_modules/bulma/sass/elements/_all" 12 | @import "../node_modules/bulma/sass/elements/button" 13 | @import "../node_modules/bulma/sass/elements/form" 14 | @import "../node_modules/bulma/sass/elements/container" 15 | @import "../node_modules/bulma/sass/elements/box" 16 | @import "../node_modules/bulma/sass/elements/content" 17 | @import "../node_modules/bulma/sass/elements/title" 18 | 19 | //@import "../node_modules/bulma/sass/components/_all" 20 | @import "../node_modules/bulma/sass/components/level.sass" 21 | @import "../node_modules/bulma/sass/components/dropdown.sass" 22 | @import "../node_modules/bulma/sass/grid/columns" 23 | 24 | // @import "../node_modules/bulma/sass/layout/_all" 25 | @import "../node_modules/bulma/sass/layout/hero" 26 | @import "../node_modules/bulma/sass/layout/section" 27 | 28 | a:hover 29 | text-decoration: underline 30 | 31 | a.button 32 | text-decoration: none 33 | 34 | @import "./transitions" 35 | @import "./datasetContentTextPreview" 36 | @import "./commentLink" 37 | @import "./navBrand" 38 | @import "./visualizationPreview" 39 | -------------------------------------------------------------------------------- /packages/web/redux/epics/forkEpic.js: -------------------------------------------------------------------------------- 1 | import { defer } from 'rxjs'; 2 | import { switchMap } from 'rxjs/operators'; 3 | import { uiRedux } from 'vizhub-ui'; 4 | import { getCsrfToken, getVisualization } from '../selectors'; 5 | 6 | const { 7 | actionTypes: { 8 | FORK_VISUALIZATION 9 | }, 10 | actionCreators: { 11 | forkError, 12 | forkSuccess 13 | }, 14 | selectors: { 15 | getFiles 16 | } 17 | } = uiRedux; 18 | 19 | export const forkEpic = (action$, state$) => 20 | action$.ofType(FORK_VISUALIZATION).pipe( 21 | switchMap(action => defer(async () => { 22 | 23 | const state = state$.value; 24 | const csrfToken = getCsrfToken(state); 25 | 26 | const visualization = Object.assign( 27 | {}, 28 | getVisualization(state), 29 | { files: getFiles(state) } 30 | ); 31 | 32 | const url = `/api/visualization/fork`; 33 | const options = { 34 | credentials: 'include', 35 | method: 'POST', 36 | headers: { 37 | 'x-csrf-token': csrfToken, 38 | 'Content-Type': 'application/json' 39 | }, 40 | body: JSON.stringify({ 41 | visualization 42 | }) 43 | }; 44 | const response = await (await fetch(url, options)).json(); 45 | 46 | return response.error 47 | ? forkError(response.error) 48 | : forkSuccess(response.id, response.userName); 49 | })) 50 | ); 51 | -------------------------------------------------------------------------------- /packages/web/server/next-auth.functions.js: -------------------------------------------------------------------------------- 1 | import { ciUser } from 'datavis-tech-entities'; 2 | 3 | require('dotenv').load(); 4 | 5 | const updateUser = async (user) => { 6 | console.log('TODO implement updateUser'); 7 | return user; 8 | }; 9 | 10 | const removeUser = async (id) => { 11 | console.log('TODO implement removeUser'); 12 | return true; 13 | }; 14 | 15 | const serializeUser = user => { 16 | if (user.id) { 17 | return Promise.resolve(user.id); 18 | } else { 19 | return Promise.reject(new Error('Unable to serialise user')); 20 | } 21 | }; 22 | 23 | module.exports = async (userController) => ({ 24 | // If a user is not found find() should return null (with no error). 25 | find: async (options) => { 26 | if (options.provider) { 27 | const user = await userController.getUser({ 28 | id: options.provider.id 29 | }); 30 | if (user) { 31 | // This object is expected by next-auth. 32 | user[options.provider.name] = {}; 33 | } 34 | return user; 35 | } 36 | return null; 37 | }, 38 | 39 | insert: userController.createUser, 40 | 41 | update: updateUser, 42 | remove: removeUser, 43 | serialize: serializeUser, 44 | deserialize: async (id) => { 45 | const user = await userController.getUser({ id }); 46 | if (user) { 47 | user.authenticated = true; 48 | } 49 | return user; 50 | }, 51 | signIn: () => Promise.resolve(ciUser) 52 | }); 53 | -------------------------------------------------------------------------------- /packages/useCases/src/interactors/deleteVisualization.ts: -------------------------------------------------------------------------------- 1 | import { Visualization, UserId, DocumentId, timestamp } from 'datavis-tech-entities'; 2 | import { i18n } from 'datavis-tech-i18n'; 3 | import { Interactor, RequestModel, ResponseModel } from '../interactor'; 4 | import { VisualizationGateway } from '../gatewayInterfaces/visualizationGateway'; 5 | import { UserGateway } from '../gatewayInterfaces/userGateway'; 6 | import { generateId } from '../utils/generateId'; 7 | 8 | export interface DeleteVisualizationRequestModel extends RequestModel { 9 | id: DocumentId, 10 | userId: UserId 11 | } 12 | 13 | export interface DeleteVisualizationResponseModel extends ResponseModel { 14 | status: string 15 | } 16 | 17 | export class DeleteVisualization implements Interactor { 18 | visualizationGateway: VisualizationGateway; 19 | 20 | constructor({ visualizationGateway }) { 21 | this.visualizationGateway = visualizationGateway; 22 | } 23 | 24 | async execute(requestModel: DeleteVisualizationRequestModel) { 25 | const { id, userId } = requestModel; 26 | 27 | if (!userId) { 28 | throw new Error(i18n('errorNoOwner')) 29 | } 30 | 31 | const visualization = await this.visualizationGateway.getVisualization({ id }); 32 | 33 | if (visualization.info.owner !== userId) { 34 | throw new Error(i18n('errorNotOwnerCantDelete')) 35 | } 36 | 37 | return await this.visualizationGateway.deleteVisualization({ id }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/useCases/src/interactors/createVisualization/index.ts: -------------------------------------------------------------------------------- 1 | import { UserId, DocumentId, timestamp } from 'datavis-tech-entities'; 2 | import { i18n } from 'datavis-tech-i18n'; 3 | import { Interactor, RequestModel, ResponseModel } from '../../interactor'; 4 | import { VisualizationGateway } from '../../gatewayInterfaces/visualizationGateway' 5 | import { generateId } from '../../utils/generateId'; 6 | import { visualizationDefaults } from './visualizationDefaults'; 7 | 8 | export interface CreateVisualizationRequestModel extends RequestModel { 9 | owner: UserId 10 | } 11 | 12 | export interface CreateVisualizationResponseModel extends ResponseModel { 13 | id: DocumentId 14 | } 15 | 16 | export class CreateVisualization implements Interactor { 17 | visualizationGateway: VisualizationGateway; 18 | 19 | constructor({ visualizationGateway }) { 20 | this.visualizationGateway = visualizationGateway; 21 | } 22 | 23 | async execute(requestModel: CreateVisualizationRequestModel) { 24 | 25 | if (!requestModel.owner) { 26 | throw new Error(i18n('errorNoOwner')) 27 | } 28 | 29 | const nowTimestamp = timestamp(); 30 | 31 | return await this.visualizationGateway.createVisualization( 32 | Object.assign({}, visualizationDefaults, { 33 | owner: requestModel.owner, 34 | id: generateId(), 35 | createdTimestamp: nowTimestamp, 36 | lastUpdatedTimestamp: nowTimestamp 37 | }) 38 | ) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/controllers/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "datavis-tech-controllers", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "clone": { 8 | "version": "1.0.4", 9 | "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", 10 | "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=" 11 | }, 12 | "deep-equal": { 13 | "version": "1.0.1", 14 | "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", 15 | "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" 16 | }, 17 | "diff-match-patch": { 18 | "version": "1.0.1", 19 | "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.1.tgz", 20 | "integrity": "sha512-A0QEhr4PxGUMEtKxd6X+JLnOTFd3BfIPSDpsc4dMvj+CbSaErDwTpoTo/nFJDMSrjxLW4BiNq+FbNisAAHhWeQ==" 21 | }, 22 | "json0-ot-diff": { 23 | "version": "1.0.3", 24 | "resolved": "https://registry.npmjs.org/json0-ot-diff/-/json0-ot-diff-1.0.3.tgz", 25 | "integrity": "sha512-aj1awKIioPEZjBhXdBcEBO8QXTe65inJhUXQLcAnkyGpxPNL8j57ppUuJTdGJCnavqra5B9igzKbPu11JXzVUQ==", 26 | "requires": { 27 | "clone": "^1.0.2", 28 | "deep-equal": "^1.0.1", 29 | "ot-json0": "^1.0.1" 30 | } 31 | }, 32 | "ot-json0": { 33 | "version": "1.1.0", 34 | "resolved": "https://registry.npmjs.org/ot-json0/-/ot-json0-1.1.0.tgz", 35 | "integrity": "sha512-wf5fci7GGpMYRDnbbdIFQymvhsbFACMHtxjivQo5KgvAHlxekyfJ9aPsRr6YfFQthQkk4bmsl5yESrZwC/oMYQ==" 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/useCases/src/interactors/getVisualization.ts: -------------------------------------------------------------------------------- 1 | import { Visualization, User, DocumentId } from 'datavis-tech-entities'; 2 | import { i18n } from 'datavis-tech-i18n'; 3 | import { Interactor, RequestModel, ResponseModel } from '../interactor'; 4 | import { GetUser } from './getUser'; 5 | import { VisualizationGateway } from '../gatewayInterfaces/visualizationGateway'; 6 | import { UserGateway } from '../gatewayInterfaces/userGateway'; 7 | 8 | export interface GetVisualizationRequestModel extends RequestModel { 9 | id: DocumentId 10 | } 11 | 12 | export interface GetVisualizationResponseModel extends ResponseModel { 13 | visualization: Visualization, 14 | user: User 15 | } 16 | 17 | export class GetVisualization implements Interactor { 18 | visualizationGateway: VisualizationGateway; 19 | getUser: GetUser; 20 | 21 | constructor({ visualizationGateway, userGateway }) { 22 | this.visualizationGateway = visualizationGateway; 23 | this.getUser = new GetUser({ userGateway }); 24 | } 25 | 26 | async execute(requestModel: GetVisualizationRequestModel) { 27 | if (!requestModel.id) { 28 | throw new Error(i18n('errorNoId')) 29 | } 30 | 31 | const visualization = await this.visualizationGateway.getVisualization({ 32 | id: requestModel.id 33 | }); 34 | 35 | const { user } = await this.getUser.execute({ 36 | id: visualization.info.owner 37 | }) 38 | 39 | return { 40 | visualization, 41 | ownerUser: user 42 | }; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/controllers/src/apiController/datasetAPIController/getDatasetController.js: -------------------------------------------------------------------------------- 1 | import { GetDataset } from 'datavis-tech-use-cases'; 2 | 3 | export const getDatasetController = (expressApp, gateways) => { 4 | const getDataset = new GetDataset(gateways); 5 | expressApp.get('/api/dataset/get/:userName/:slug', async (req, res) => { 6 | try { 7 | const { userName, slug } = req.params; 8 | const requestModel = { userName, slug }; 9 | const responseModel = await getDataset.execute(requestModel); 10 | res.json(responseModel); 11 | } catch (error) { 12 | // TODO unify error handling across the codebase. 13 | res.json({ 14 | error: { 15 | message: error.message 16 | } 17 | }); 18 | } 19 | }); 20 | 21 | expressApp.get('/:userName/datasets/:slug.:format', async (req, res) => { 22 | try { 23 | const { userName, slug } = req.params; 24 | const requestModel = { userName, slug }; 25 | const responseModel = await getDataset.execute(requestModel); 26 | res.setHeader('Content-Type', 'text/csv'); 27 | res.setHeader('Access-Control-Allow-Origin', '*'); 28 | res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'); 29 | res.send(responseModel.dataset.content.text); 30 | } catch (error) { 31 | // TODO unify error handling across the codebase. 32 | res.json({ 33 | error: { 34 | message: error.message 35 | } 36 | }); 37 | } 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /packages/web/redux/epics/index.js: -------------------------------------------------------------------------------- 1 | import { uiRedux } from 'vizhub-ui'; 2 | import { combineEpics } from 'redux-observable'; 3 | import { saveEpic } from './saveEpic'; 4 | import { startBuildEpic } from './startBuildEpic'; 5 | import { buildEpic } from './buildEpic'; 6 | import { runBuildEpic } from './runBuildEpic'; 7 | import { runNonJSEpic } from './runNonJS'; 8 | import { forkEpic } from './forkEpic'; 9 | import { forkSuccessEpic } from './forkSuccessEpic'; 10 | import { deleteVisualizationEpic } from './deleteVisualizationEpic'; 11 | import { deleteVisualizationSuccessEpic } from './deleteVisualizationSuccessEpic'; 12 | 13 | const { 14 | epics: { 15 | autoSaveEpic, 16 | promptForNewFileNameEpic, 17 | promptForNewHeightEpic, 18 | promptForRenameEpic, 19 | confirmFileDeleteEpic, 20 | confirmVisualizationDeleteEpic, 21 | updateTitleEpic, 22 | updateDescriptionEpic, 23 | } 24 | } = uiRedux; 25 | 26 | export const rootEpicForBrowser = combineEpics( 27 | startBuildEpic, 28 | buildEpic, 29 | runBuildEpic, 30 | runNonJSEpic, 31 | autoSaveEpic, 32 | saveEpic, 33 | promptForNewFileNameEpic, 34 | promptForNewHeightEpic, 35 | promptForRenameEpic, 36 | confirmFileDeleteEpic, 37 | confirmVisualizationDeleteEpic, 38 | forkEpic, 39 | forkSuccessEpic, 40 | updateTitleEpic, 41 | updateDescriptionEpic, 42 | deleteVisualizationEpic, 43 | deleteVisualizationSuccessEpic 44 | ); 45 | 46 | export const rootEpicForServer = combineEpics( 47 | updateTitleEpic, 48 | updateDescriptionEpic 49 | ); 50 | -------------------------------------------------------------------------------- /packages/useCases/src/gatewayInterfaces/visualizationGateway.ts: -------------------------------------------------------------------------------- 1 | import { 2 | UserId, 3 | DocumentId, 4 | File, 5 | VisualizationInfo, 6 | Visualization, 7 | Images 8 | } from 'datavis-tech-entities'; 9 | 10 | import { 11 | CreateVisualizationRequestModel, 12 | CreateVisualizationResponseModel, 13 | SaveVisualizationRequestModel, 14 | SaveVisualizationResponseModel, 15 | DeleteVisualizationResponseModel 16 | } from '../interactors'; 17 | 18 | export interface VisualizationGateway { 19 | 20 | // TODO pass in a visualization here. Move logic from gateway to use case. 21 | createVisualization(options: { 22 | owner: UserId, 23 | id: DocumentId, 24 | title: string, 25 | slug: string | undefined, 26 | height: number | undefined, 27 | description: string, 28 | files: File[], 29 | forkedFrom: DocumentId | undefined, 30 | createdTimestamp: number, 31 | lastUpdatedTimestamp: number, 32 | }): Promise; 33 | 34 | getVisualization({ id }): Promise; 35 | 36 | getVisualizationInfosByUserId(id: UserId): Promise<[VisualizationInfo]>; 37 | 38 | getAllVisualizationInfos(): Promise<[VisualizationInfo]>; 39 | 40 | saveVisualization(request: SaveVisualizationRequestModel): 41 | Promise; 42 | 43 | deleteVisualization({ id: DocumentId }): 44 | Promise; 45 | 46 | setImagesUpdatedTimestamp({ id: DocumentId, imagesUpdatedTimestamp: number }): 47 | Promise; 48 | } 49 | -------------------------------------------------------------------------------- /packages/gateways/src/databaseDatasetGateway.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DatasetGateway, 3 | CreateDatasetRequestModel, 4 | CreateDatasetResponseModel 5 | } from 'datavis-tech-use-cases'; 6 | 7 | import { 8 | Dataset, 9 | DatasetInfo, 10 | DatasetContent 11 | } from 'datavis-tech-entities'; 12 | 13 | export class DatabaseDatasetGateway implements DatasetGateway { 14 | database: any; 15 | 16 | constructor(database) { 17 | this.database = database; 18 | } 19 | 20 | async createDataset(options): Promise { 21 | const { 22 | owner, 23 | id, 24 | title, 25 | slug, 26 | description, 27 | text, 28 | format, 29 | sourceName, 30 | sourceUrl 31 | } = options; 32 | 33 | const dataset = new Dataset({ 34 | datasetInfo: new DatasetInfo({ 35 | id, 36 | owner, 37 | title, 38 | slug, 39 | description, 40 | format, 41 | sourceName, 42 | sourceUrl 43 | }), 44 | datasetContent: new DatasetContent({ 45 | id, 46 | text 47 | }) 48 | }); 49 | 50 | return await this.database.createDataset(dataset); 51 | } 52 | 53 | async getDataset({ owner, slug }) { 54 | return await this.database.getDataset({ owner, slug }); 55 | } 56 | 57 | async getDatasetInfosByUserId(id) { 58 | return await this.database.getDatasetInfosByUserId(id); 59 | }; 60 | 61 | //async saveDataset({ dataset }) { 62 | // return await this.database.saveDataset({ dataset }); 63 | //} 64 | } 65 | -------------------------------------------------------------------------------- /packages/web/components/organisms/navBar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { UserMenu } from '../molecules/userMenu'; 3 | import { FaGithub } from 'react-icons/fa'; 4 | 5 | export const NavBar = ({user, csrfToken, dropUp}) => ( 6 | 7 |
8 | 9 | 46 |
47 |
48 | ); 49 | -------------------------------------------------------------------------------- /packages/web/pages/visualization/forkInvitation.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { uiRedux } from 'vizhub-ui'; 3 | import { getShowForkInvitation } from '../../redux/selectors'; 4 | 5 | const { 6 | actionCreators: { 7 | forkVisualization 8 | } 9 | } = uiRedux; 10 | 11 | export const ForkInvitationPresentation = ({show, onFork, user}) => { 12 | if (!show) { 13 | return null; 14 | } 15 | const onForkLinkClick = event => { 16 | event.preventDefault(); 17 | onFork(); 18 | }; 19 | 20 | return ( 21 |
22 |
23 |
24 |

25 | { 26 | user.authenticated 27 | ? ( 28 | 29 | Fork this visualization 30 | 31 | ) 32 | : ( 33 | 34 | Sign up / Sign in 35 | 36 | ) 37 | } 38 |

39 |

40 | if you want to save your changes. 41 |

42 |
43 |
44 |
45 | ); 46 | } 47 | 48 | const mapStateToProps = state => ({ 49 | show: getShowForkInvitation(state) 50 | }); 51 | 52 | const mapDispatchToProps = dispatch => ({ 53 | onFork: () => dispatch(forkVisualization()) 54 | }); 55 | 56 | export const ForkInvitation = connect( 57 | mapStateToProps, 58 | mapDispatchToProps 59 | )(ForkInvitationPresentation); 60 | -------------------------------------------------------------------------------- /packages/web/redux/epics/saveEpic.js: -------------------------------------------------------------------------------- 1 | import { defer } from 'rxjs'; 2 | import { switchMap } from 'rxjs/operators'; 3 | import { uiRedux } from 'vizhub-ui'; 4 | import { getCsrfToken, getVisualization } from '../selectors'; 5 | 6 | const { 7 | actionTypes: { 8 | SAVE 9 | }, 10 | actionCreators: { 11 | saveSuccess, 12 | saveError 13 | }, 14 | selectors: { 15 | getFile, 16 | getFiles, 17 | getVisualizationTitle, 18 | getVisualizationHeight 19 | } 20 | } = uiRedux; 21 | 22 | export const saveEpic = (action$, state$) => 23 | action$.ofType(SAVE).pipe( 24 | switchMap(action => defer(async () => { 25 | const state = state$.value; 26 | const csrfToken = getCsrfToken(state); 27 | 28 | const visualization = getVisualization(state) 29 | 30 | visualization.content.files = getFiles(state); 31 | visualization.info.title = getVisualizationTitle(state); 32 | visualization.info.height = getVisualizationHeight(state); 33 | 34 | const readmeFile = getFile(state, 'README.md'); 35 | visualization.info.description = readmeFile ? readmeFile.text : ''; 36 | 37 | const url = `/api/visualization/save`; 38 | const options = { 39 | credentials: 'include', 40 | method: 'POST', 41 | headers: { 42 | 'x-csrf-token': csrfToken, 43 | 'Content-Type': 'application/json' 44 | }, 45 | body: JSON.stringify({ 46 | visualization 47 | }) 48 | }; 49 | const response = await (await fetch(url, options)).json(); 50 | 51 | return response.error 52 | ? saveError(response.error) 53 | : saveSuccess(); 54 | })) 55 | ); 56 | -------------------------------------------------------------------------------- /packages/useCases/src/interactors/getUserProfileData.ts: -------------------------------------------------------------------------------- 1 | import { User, VisualizationInfo, ciUser } from 'datavis-tech-entities'; 2 | import { Interactor, RequestModel, ResponseModel } from '../interactor'; 3 | import { UserGateway } from '../gatewayInterfaces/userGateway' 4 | import { VisualizationGateway } from '../gatewayInterfaces/visualizationGateway' 5 | import { DatasetGateway } from '../gatewayInterfaces/datasetGateway' 6 | 7 | export interface GetUserProfileDataRequestModel extends RequestModel { 8 | userName: string 9 | } 10 | 11 | export interface GetUserProfileDataResponseModel extends ResponseModel { 12 | user: User, 13 | visualizationInfos: [VisualizationInfo] 14 | } 15 | 16 | export class GetUserProfileData implements Interactor { 17 | userGateway: UserGateway; 18 | visualizationGateway: VisualizationGateway; 19 | datasetGateway: DatasetGateway; 20 | 21 | constructor({ userGateway, visualizationGateway, datasetGateway }) { 22 | this.userGateway = userGateway; 23 | this.visualizationGateway = visualizationGateway; 24 | this.datasetGateway = datasetGateway; 25 | } 26 | 27 | async execute(requestModel: GetUserProfileDataRequestModel) { 28 | const { userName } = requestModel; 29 | 30 | const user: User = ( 31 | userName === ciUser.userName 32 | ? ciUser 33 | : await this.userGateway.getUserByUserName(userName) 34 | ); 35 | 36 | const [visualizationInfos, datasetInfos] = await Promise.all([ 37 | this.visualizationGateway.getVisualizationInfosByUserId(user.id), 38 | this.datasetGateway.getDatasetInfosByUserId(user.id) 39 | ]); 40 | 41 | return { user, visualizationInfos, datasetInfos }; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/ui/src/visualizationView/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { VisualizationRunner } from '../visualizationRunner/index.js'; 3 | import { MdFullscreen } from 'react-icons/md'; 4 | import { Avatar } from '../atoms/avatar'; 5 | 6 | export const VisualizationView = props => { 7 | const { 8 | width, 9 | height, 10 | files, 11 | runId, 12 | title, 13 | description, 14 | ownerUser, 15 | disablePointerEvents, 16 | fullScreenUrl 17 | } = props; 18 | 19 | const pointerEvents = disablePointerEvents ? 'none' : 'auto'; 20 | 21 | return ( 22 |
23 | 29 |
30 |
31 |
32 | {title} 33 |
34 | 35 | 36 | 37 |
38 | 39 | 40 | {ownerUser.userName} 41 | 42 |
46 |
47 |
48 |
All code in VizHub is released under the MIT License.
49 |
50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /packages/web/pages/auth/callback.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TitledPage } from '../../components/atoms/titledPage'; 3 | import Router from 'next/router'; 4 | import Cookies from 'universal-cookie'; 5 | import { NextAuth } from 'next-auth/client'; 6 | import { userFromSession } from '../../utils/userFromSession'; 7 | 8 | export default class AuthCallback extends React.Component { 9 | 10 | static async getInitialProps({req}) { 11 | const session = await NextAuth.init({force: true, req: req}); 12 | 13 | const cookies = new Cookies((req && req.headers.cookie) ? req.headers.cookie : null); 14 | 15 | // If the user is signed in, we look for a redirect URL cookie and send 16 | // them to that page, so that people signing in end up back on the page they 17 | // were on before signing in. Defaults to '/'. 18 | let redirectTo = '/'; 19 | if (session.user) { 20 | 21 | // Read redirect URL to redirect to from cookies 22 | redirectTo = cookies.get('redirect_url') || redirectTo; 23 | } 24 | 25 | return { 26 | user: userFromSession(session), 27 | csrfToken: session.csrfToken, 28 | lang: 'en', 29 | redirectTo: redirectTo 30 | }; 31 | } 32 | 33 | async componentDidMount() { 34 | // Get latest session data after rendering on client *then* redirect. 35 | // The ensures client state is always updated after signing in or out. 36 | // (That's why we use a callback page) 37 | await NextAuth.init({force: true}); 38 | Router.push(this.props.redirectTo || '/'); 39 | } 40 | 41 | render() { 42 | return ( 43 | 44 |
Loading...
45 |
46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/ui/README.md: -------------------------------------------------------------------------------- 1 | [![image](https://user-images.githubusercontent.com/68416/46601539-05f7ba00-cb0b-11e8-90eb-587dc927962e.png)](https://vizhub.com/curran/5c907e49d0294538aad03ad1f41e1e28) 2 | 3 | This repository contains the core user interface components for [vizhub.com](https://vizhub.com). 4 | 5 | This is a submodule of https://github.com/datavis-tech/vizhub . 6 | 7 | See also our [VizHub Scrum Board](https://github.com/orgs/datavis-tech/projects/1). 8 | 9 | * If you want a certain feature, please give its corresponding GitHub issue a :thumbsup: 10 | * If you discover bugs or have ideas for new features, please [open an issue](https://github.com/datavis-tech/vizhub-ui/issues/new?template=upvotable.md). 11 | * If you want to contribute code changes, see [contribution guidelines](CONTRIBUTING.md). 12 | 13 | ### Medium Posts (reverse chronological order): 14 | * [VizHub is Open Source](https://medium.com/@currankelleher/vizhub-is-open-source-8c266493d967) 15 | * [VizHub 1.0](https://medium.com/@currankelleher/vizhub-1-0-9fc56a70a464) 16 | * [VizHub Beta](https://medium.com/@currankelleher/introducing-vizhub-beta-2edc4a0a1908) 17 | * [VizHub Alpha](https://medium.com/@currankelleher/introducing-vizhub-75644cb8bba6) 18 | 19 | ### YouTube videos (reverse chronological order): 20 | * [VizHub 1.0](https://www.youtube.com/watch?list=PL9yYRbwpkykvOXrZumtZWbuaXWHvjD8gi&v=WoJxanRe06k) 21 | * [VizHub Beta Demo](https://www.youtube.com/watch?v=qaOzZ7L3dJo) 22 | * [VizHub Alpha Intro](https://www.youtube.com/watch?v=5KlhI67cueI&feature=youtu.be) 23 | 24 | To get involved and stay updated: 25 | 26 | * Join the `#vizhub` channel of the [D3 Slack](https://d3-slackin.herokuapp.com/) 27 | * Follow [@datavis_tech on Twitter](https://twitter.com/datavis_tech) 28 | -------------------------------------------------------------------------------- /packages/ui/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We welcome contributions to this repository! 4 | 5 | Our typical workflow: 6 | 7 | * Open an issue if there's not one open already for the thing you want to work on. 8 | * Comment on the issue that you'd like to work on it, possibly proposing a solution. 9 | * Self-assign the issue when you start to work on it. 10 | * As soon as work starts, create a Pull Request with "WIP" in the title. 11 | * This makes Work In Progress visible, so folks know it's underway and can review it. 12 | * For the commit that closes an issue, please include `Closes #5`, replacing `5` with the issue number. 13 | * This will make it so that the issue automatically gets closed when the PR gets merged. 14 | * When the work is done, remove "WIP" from the PR title and request a review from @curran. 15 | * The work will be reviewed and, if approved, merged and deployed! 16 | 17 | ## Development Environment Setup 18 | 19 | Check out the [video tutorial: Coding an IDE with React & Redux-Observable](https://www.youtube.com/watch?v=mrXCmhGmA5I&feature=youtu.be), which covers setup of the development environment and how to navigate the codebase. 20 | 21 | The testing app was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 22 | 23 | To run locally: 24 | 25 | Fork the project and clone it to your machine. 26 | 27 | ``` 28 | cd vizhub-ui 29 | npm install 30 | npm run start 31 | ``` 32 | 33 | This will start the development server, and open a browser to [http://localhost:3000](http://localhost:3000). 34 | 35 | Code changes will be watched, recompiled, and reloaded automatically. 36 | 37 | If you change the SASS files, you'll need to run the following for it to update: 38 | 39 | ``` 40 | npm run build-css 41 | ``` 42 | -------------------------------------------------------------------------------- /packages/web/redux/epics/buildEpic.js: -------------------------------------------------------------------------------- 1 | import { of, from } from 'rxjs'; 2 | import { map, switchMap, catchError } from 'rxjs/operators'; 3 | import { bundle } from 'datavis-tech-presenters'; 4 | import { uiRedux } from 'vizhub-ui'; 5 | import { START_BUILD, BUILD_ERROR } from '../actionTypes'; 6 | import { buildFinished, buildError } from '../actionCreators'; 7 | 8 | const { 9 | selectors: { 10 | getFiles 11 | } 12 | } = uiRedux; 13 | 14 | const relativeId = id => 15 | (typeof id === 'string' && id.length > 0 && id[0] === '/') 16 | ? id.substr(1) 17 | : id; 18 | 19 | // Inspired by https://github.com/rollup/rollup/blob/master/bin/src/logging.ts 20 | const logRollupError = error => { 21 | const lines = []; 22 | 23 | let message = error.message || error; 24 | if (error.name) { 25 | message = `${error.name}: ${message}`; 26 | } 27 | 28 | lines.push(`${message.toString()}`); 29 | 30 | if (error.url) { 31 | lines.push(error.url); 32 | } 33 | 34 | if (error.loc) { 35 | lines.push(`${error.loc.file || error.id} (line ${error.loc.line})`); 36 | } else if (error.id) { 37 | lines.push(error.id); 38 | } 39 | 40 | if (error.frame) { 41 | lines.push(error.frame); 42 | } 43 | 44 | // if (error.stack) { 45 | // lines.push(error.stack); 46 | // } 47 | 48 | lines.push('(bundle.js not updated)'); 49 | 50 | console.error(lines.join('\n')); 51 | }; 52 | 53 | export const buildEpic = (action$, state$) => 54 | action$.ofType(START_BUILD).pipe( 55 | switchMap(() => from(bundle(getFiles(state$.value))).pipe( 56 | map(buildFinished), 57 | catchError(error => { 58 | if (process.browser) { 59 | logRollupError(error); 60 | } 61 | return of(buildError(error)); 62 | }) 63 | )) 64 | ); 65 | -------------------------------------------------------------------------------- /packages/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vizhub-ui", 3 | "version": "1.0.0", 4 | "description": "A platform for teaching and learning data visualization with D3.js.", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build-js": "rollup -c", 8 | "build-css": "node-sass src/css/index.sass dist/styles.css", 9 | "symlink-dist": "rm ./src/dist-symlink && ln -s ../dist/ ./src/dist-symlink", 10 | "build": "rm -rf ./dist && npm run build-js && npm run build-css", 11 | "postinstall": "npm run build && npm run symlink-dist", 12 | "start": "react-scripts start" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/datavis-tech/vizhub-ui.git" 17 | }, 18 | "keywords": [ 19 | "data", 20 | "visualization", 21 | "D3", 22 | "dataviz", 23 | "IDE", 24 | "editor", 25 | "UI" 26 | ], 27 | "dependencies": { 28 | "classnames": "^2.2.6", 29 | "codemirror": "^5.39.2", 30 | "codemirror-inlet": "^0.1.10", 31 | "lodash": "^4.17.10", 32 | "magic-sandbox": "^1.2.0", 33 | "marked": "^0.4.0", 34 | "node-sass": "^4.9.3", 35 | "react": "^16.4.2", 36 | "react-codemirror2": "^5.1.0", 37 | "react-dom": "^16.4.2", 38 | "react-icons": "^3.2.2", 39 | "react-measure": "^2.1.0", 40 | "react-redux": "^5.0.7", 41 | "react-scripts": "1.1.4", 42 | "react-split-pane": "^0.1.82", 43 | "redux": "^4.0.0", 44 | "redux-observable": "^1.0.0", 45 | "rxjs": "^6.2.2" 46 | }, 47 | "author": "Datavis Tech INC.", 48 | "license": "GPL-3.0", 49 | "bugs": { 50 | "url": "https://github.com/datavis-tech/vizhub-ui/issues" 51 | }, 52 | "homepage": "https://github.com/datavis-tech/vizhub-ui#readme", 53 | "devDependencies": { 54 | "rollup": "^0.64.1", 55 | "rollup-plugin-buble": "^0.19.2" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /graveyard/oldUseCases/test/test.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { i18n } from 'datavis-tech-i18n'; 3 | import { createVisualization } from '../src'; 4 | 5 | describe('Visualization Use Cases', () => { 6 | describe('createVisualization', () => { 7 | it('should return an error if no owner specified.', () => { 8 | assert.deepEqual(createVisualization({}), { 9 | type: 'error', 10 | message: i18n('errorNoOwner') 11 | }); 12 | }); 13 | 14 | it('should return a Visualization entity with defaults.', () => { 15 | const action = createVisualization({ owner: '754328' }); 16 | 17 | assert.equal(action.data.id.length, 32); 18 | delete action.data.id; 19 | 20 | assert.deepEqual(action, { 21 | type: 'createVisualization', 22 | data: { 23 | type: 'visualization', 24 | title: '', 25 | slug: '', 26 | description: '', 27 | files: { 28 | 'index.html': '

I AM VIZ

' 29 | }, 30 | owner: '754328' 31 | } 32 | }); 33 | }); 34 | 35 | it('should compute slug from provided title.', () => { 36 | const action = createVisualization({ 37 | owner: '754328', 38 | title: 'Foo the Great' 39 | }); 40 | assert.equal(action.data.title, 'Foo the Great'); 41 | assert.equal(action.data.slug, 'foo-the-great'); 42 | }); 43 | 44 | it('should use slug if provided.', () => { 45 | const action = createVisualization({ 46 | owner: '754328', 47 | title: 'Foo the Great', 48 | slug: 'foo' 49 | }); 50 | assert.equal(action.data.slug, 'foo'); 51 | }); 52 | // it('should override defaults with provided fields.', () => { 53 | // it('should return a Visualization entity with given data.', () => { 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /packages/ui/src/redux/actionTypes.js: -------------------------------------------------------------------------------- 1 | const prefix = name => `vizhub-ui/editor/${name}`; 2 | 3 | export const INIT_FILES = prefix('INIT_FILES'); 4 | export const CHANGE_FILE_TEXT = prefix('CHANGE_ACTIVE_FILE_TEXT'); 5 | export const SET_ACTIVE_FILE = prefix('SET_ACTIVE_FILE'); 6 | export const RUN_FILES = prefix('RUN_FILES'); 7 | export const SAVE = prefix('SAVE'); 8 | export const SAVE_SUCCESS = prefix('SAVE_SUCCESS'); 9 | export const SAVE_ERROR = prefix('SAVE_ERROR'); 10 | 11 | export const SET_HEIGHT_PROMPT = prefix('SET_HEIGHT_PROMPT'); 12 | export const SET_VISUALIZATION_WIDTH = prefix('SET_VISUALIZATION_WIDTH'); 13 | export const SET_VISUALIZATION_HEIGHT = prefix('SET_VISUALIZATION_HEIGHT'); 14 | export const SET_VISUALIZATION_TITLE = prefix('SET_VISUALIZATION_TITLE'); 15 | export const SET_VISUALIZATION_ID = prefix('SET_VISUALIZATION_ID'); 16 | export const SET_VISUALIZATION_DESCRIPTION = prefix('SET_VISUALIZATION_DESCRIPTION'); 17 | export const BUILD_FINISHED = prefix('BUILD_FINISHED'); 18 | export const CREATE_NEW_FILE = prefix('CREATE_NEW_FILE'); 19 | export const NEW_FILE_CREATED = prefix('NEW_FILE_CREATED'); 20 | export const RENAME_FILE = prefix('RENAME_FILE'); 21 | export const FILE_RENAMED = prefix('FILE_RENAMED'); 22 | export const DELETE_FILE = prefix('DELETE_FILE'); 23 | export const FILE_DELETED = prefix('FILE_DELETED'); 24 | export const FORK_VISUALIZATION = prefix('FORK_VISUALIZATION'); 25 | export const FORK_SUCCESS = prefix('FORK_SUCCESS'); 26 | export const FORK_ERROR = prefix('FORK_ERROR'); 27 | export const SET_VISUALIZATION_OWNER_USER = prefix('SET_VISUALIZATION_OWNER_USER'); 28 | export const SPLIT_PANE_DRAG_STARTED = prefix('SPLIT_PANE_DRAG_STARTED'); 29 | export const SPLIT_PANE_DRAG_FINISHED = prefix('SPLIT_PANE_DRAG_FINISHED'); 30 | export const DELETE_VISUALIZATION = prefix('DELETE_VISUALIZATION'); 31 | export const VISUALIZATION_DELETED = prefix('VISUALIZATION_DELETED'); 32 | -------------------------------------------------------------------------------- /packages/database/src/index.js: -------------------------------------------------------------------------------- 1 | import { createVisualization } from './createVisualization'; 2 | import { getVisualization } from './getVisualization'; 3 | import { saveVisualization } from './saveVisualization'; 4 | import { deleteVisualization } from './deleteVisualization'; 5 | import { createDataset } from './createDataset'; 6 | import { getDataset } from './getDataset'; 7 | import { createUser } from './createUser'; 8 | import { getUser } from './getUser'; 9 | import { getUserByUserName } from './getUserByUserName'; 10 | import { getVisualizationInfosByUserId } from './getVisualizationInfosByUserId'; 11 | import { getAllVisualizationInfos } from './getAllVisualizationInfos'; 12 | import { getDatasetInfosByUserId } from './getDatasetInfosByUserId'; 13 | import { updateImages } from './updateImages'; 14 | import { getThumbnail } from './getThumbnail'; 15 | import { getPreview } from './getPreview'; 16 | import { setImagesUpdatedTimestamp } from './setImagesUpdatedTimestamp'; 17 | 18 | export const Database = connection => ({ 19 | createVisualization: createVisualization(connection), 20 | getVisualization: getVisualization(connection), 21 | saveVisualization: saveVisualization(connection), 22 | deleteVisualization: deleteVisualization(connection), 23 | createDataset: createDataset(connection), 24 | getDataset: getDataset(connection), 25 | createUser: createUser(connection), 26 | getUser: getUser(connection), 27 | getUserByUserName: getUserByUserName(connection), 28 | getVisualizationInfosByUserId: getVisualizationInfosByUserId(connection), 29 | getAllVisualizationInfos: getAllVisualizationInfos(connection), 30 | getDatasetInfosByUserId: getDatasetInfosByUserId(connection), 31 | updateImages: updateImages(connection), 32 | getThumbnail: getThumbnail(connection), 33 | getPreview: getPreview(connection), 34 | setImagesUpdatedTimestamp: setImagesUpdatedTimestamp(connection) 35 | }); 36 | -------------------------------------------------------------------------------- /packages/useCases/src/interactors/exportVisualization/index.ts: -------------------------------------------------------------------------------- 1 | import { Visualization, DocumentId } from 'datavis-tech-entities'; 2 | import { i18n } from 'datavis-tech-i18n'; 3 | import { Interactor, RequestModel, ResponseModel } from '../../interactor'; 4 | import { VisualizationGateway } from '../../gatewayInterfaces/visualizationGateway'; 5 | import { zipFiles } from './zipFiles'; 6 | 7 | export interface ExportVisualizationRequestModel extends RequestModel { 8 | id: DocumentId 9 | } 10 | 11 | export interface ExportVisualizationResponseModel extends ResponseModel { 12 | zipFileBuffer: any, 13 | zipFileName: string 14 | } 15 | 16 | export class ExportVisualization implements Interactor { 17 | visualizationGateway: VisualizationGateway; 18 | 19 | constructor({ visualizationGateway }) { 20 | this.visualizationGateway = visualizationGateway; 21 | } 22 | 23 | async execute(requestModel: ExportVisualizationRequestModel) { 24 | if (!requestModel.id) { 25 | throw new Error(i18n('errorNoId')) 26 | } 27 | 28 | const visualization = await this.visualizationGateway.getVisualization({ 29 | id: requestModel.id 30 | }); 31 | 32 | const files = visualization.content.files.concat([ 33 | { 34 | name: 'package.json', 35 | text: `{ 36 | "scripts": { 37 | "build": "rollup -c" 38 | }, 39 | "devDependencies": { 40 | "rollup": "latest" 41 | } 42 | }` 43 | }, 44 | { 45 | name: 'rollup.config.js', 46 | text: `export default { 47 | input: 'index.js', 48 | external: ['d3'], 49 | output: { 50 | file: 'bundle.js', 51 | format: 'iife', 52 | sourcemap: true, 53 | globals: { d3: 'd3' } 54 | } 55 | };` 56 | } 57 | ]); 58 | 59 | const zipFileBuffer = zipFiles(files); 60 | const zipFileName = visualization.info.title + '.zip'; 61 | 62 | return { zipFileBuffer, zipFileName }; 63 | 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/useCases/src/interactors/createDataset/index.ts: -------------------------------------------------------------------------------- 1 | import { UserId, DocumentId, File, timestamp } from 'datavis-tech-entities'; 2 | import { i18n } from 'datavis-tech-i18n'; 3 | import { Interactor, RequestModel, ResponseModel } from '../../interactor'; 4 | import { DatasetGateway } from '../../gatewayInterfaces/datasetGateway' 5 | import { generateId } from '../../utils/generateId'; 6 | import { removeExtension } from '../../utils/removeExtension'; 7 | import { datasetDefaults } from './datasetDefaults'; 8 | 9 | export interface CreateDatasetRequestModel extends RequestModel { 10 | owner: UserId, 11 | title: string, 12 | slug: string | undefined, 13 | description: string, 14 | file: File, 15 | sourceName: string, 16 | sourceUrl: string, 17 | } 18 | 19 | export interface CreateDatasetResponseModel extends ResponseModel { 20 | slug: string 21 | } 22 | 23 | export class CreateDataset implements Interactor { 24 | datasetGateway: DatasetGateway; 25 | 26 | constructor({ datasetGateway }) { 27 | this.datasetGateway = datasetGateway; 28 | } 29 | 30 | async execute(requestModel: CreateDatasetRequestModel) { 31 | 32 | if (!requestModel.owner) { 33 | throw new Error(i18n('errorNoOwner')) 34 | } 35 | 36 | const { owner, title, file, sourceName, sourceUrl } = requestModel; 37 | const slug = file && removeExtension(file.name); 38 | const text = file && file.text; 39 | const id = generateId(); 40 | 41 | // TODO validate slug uniqueness within this owner 42 | 43 | const nowTimestamp = timestamp(); 44 | 45 | return await this.datasetGateway.createDataset( 46 | Object.assign({}, datasetDefaults, { 47 | owner, 48 | id, 49 | title, 50 | slug, 51 | text, 52 | sourceName, 53 | sourceUrl, 54 | createdTimestamp: nowTimestamp, 55 | lastUpdatedTimestamp: nowTimestamp 56 | }) 57 | ) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packages/useCases/src/interactors/forkVisualization.ts: -------------------------------------------------------------------------------- 1 | import { Visualization, UserId, DocumentId, timestamp } from 'datavis-tech-entities'; 2 | import { i18n } from 'datavis-tech-i18n'; 3 | import { Interactor, RequestModel, ResponseModel } from '../interactor'; 4 | import { VisualizationGateway } from '../gatewayInterfaces/visualizationGateway'; 5 | import { UserGateway } from '../gatewayInterfaces/userGateway'; 6 | import { generateId } from '../utils/generateId'; 7 | 8 | export interface ForkVisualizationRequestModel extends RequestModel { 9 | visualization: Visualization, 10 | owner: UserId 11 | } 12 | 13 | export interface ForkVisualizationResponseModel extends ResponseModel { 14 | id: DocumentId 15 | } 16 | 17 | export class ForkVisualization implements Interactor { 18 | visualizationGateway: VisualizationGateway; 19 | userGateway: UserGateway; 20 | 21 | constructor({ visualizationGateway, userGateway }) { 22 | this.visualizationGateway = visualizationGateway; 23 | this.userGateway = userGateway; 24 | } 25 | 26 | async execute(requestModel: ForkVisualizationRequestModel) { 27 | const { visualization, owner } = requestModel; 28 | 29 | if (!owner) { 30 | throw new Error(i18n('errorNoOwner')) 31 | } 32 | 33 | const nowTimestamp = timestamp(); 34 | 35 | const [{ id }, { userName }] = await Promise.all([ 36 | this.visualizationGateway.createVisualization({ 37 | owner, 38 | id: generateId(), 39 | title: visualization.info.title, 40 | slug: undefined, 41 | description: visualization.info.description, 42 | height: visualization.info.height, 43 | files: visualization.content.files, 44 | forkedFrom: visualization.id, 45 | createdTimestamp: nowTimestamp, 46 | lastUpdatedTimestamp: nowTimestamp 47 | }), 48 | this.userGateway.getUser(owner) 49 | ]); 50 | 51 | return { id, userName }; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/web/pages/auth/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Router from 'next/router'; 3 | import Cookies from 'universal-cookie'; 4 | import { NextAuth } from 'next-auth/client'; 5 | 6 | import Page from '../../components/page'; 7 | import { TitledPage } from '../../components/atoms/titledPage'; 8 | import { SignIn } from '../../components/molecules/signIn'; 9 | import { ActionBox } from '../../components/molecules/actionBox'; 10 | import { NavBar } from '../../components/organisms/navBar'; 11 | 12 | import { userFromSession } from '../../utils/userFromSession'; 13 | 14 | export default class extends Page { 15 | 16 | static async getInitialProps({req, res, query}) { 17 | const props = await super.getInitialProps({req}); 18 | const session = await NextAuth.init({force: true, req: req}); 19 | props.user = userFromSession(session); 20 | props.providers = await NextAuth.providers({req}); 21 | 22 | // If signed in already, redirect to account management page. 23 | if (props.user.authenticated) { 24 | if (req) { 25 | res.redirect('/account'); 26 | } else { 27 | Router.push('/account'); 28 | } 29 | } 30 | 31 | // If passed a redirect parameter, save it as a cookie 32 | if (query.redirect) { 33 | const cookies = new Cookies((req && req.headers.cookie) ? req.headers.cookie : null); 34 | cookies.set('redirect_url', query.redirect, { path: '/' }); 35 | } 36 | 37 | return props; 38 | } 39 | 40 | render() { 41 | return ( 42 | 43 | 47 | 48 | 52 | 53 | 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/useCases/saveVisualization.md: -------------------------------------------------------------------------------- 1 | # Save Visualization 2 | 3 | Actor: Student, Teacher 4 | 5 | A visualization is to be edited. 6 | 7 | ## Triggers 8 | 9 | * The create visualization flow will land a user on the edit page. 10 | * From the visualization view page, the user will be able to navigate to the edit page. 11 | 12 | ## Preconditions 13 | 14 | * The user is authenticated. 15 | * There exists a visualization to edit. 16 | * The user is the owner of the visualization to edit. 17 | 18 | ## Postconditions 19 | 20 | Changes were made to the visualization. 21 | 22 | Changes may include: 23 | 24 | * Modifying the text of existing files. 25 | * Creation and deletion of files. 26 | * Addition of code that loads a dataset. 27 | * Addition of code that loads a file from another visualization. 28 | * Editing the title of the visualization. 29 | * Editing the description of the visualization. 30 | 31 | ## Normal Course of Events 32 | 33 | * A user interface for editing visualization appears. 34 | * The user interacts with the editing interface to make the desired changes. 35 | * Changes are automatically saved, without the need to click "Save". 36 | * A notice that states "Saving..." should appear before changes are saved. 37 | * A notice that states "Saved." should appear after changes are saved. 38 | * After editing, the user will be able to navigate to the visualization view. 39 | 40 | ## Exceptional Case 41 | 42 | * If the user is not authenticated, the user should be presented with a working editor interface, but be given a warning that they must authenticate in order to save their work. 43 | * If the edit page is requested for a visualization that does not exist, the page should display a 404 not found page. 44 | * The user is not the owner of the visualization to edit, the user should be presented with a working editor interface, but be given a warning that they must fork this visualization in order to save their work. 45 | -------------------------------------------------------------------------------- /packages/web/components/molecules/signIn.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Router from 'next/router'; 3 | import { NextAuth } from 'next-auth/client'; 4 | 5 | export class SignIn extends React.Component { 6 | 7 | constructor(props) { 8 | super(props); 9 | this.signInAsCI = this.signInAsCI.bind(this); 10 | } 11 | 12 | signInAsCI() { 13 | const email = 'ci@foo.com'; 14 | const password = 'ci'; 15 | 16 | NextAuth.signin({email, password}) 17 | .then(() => Router.push('/auth/callback')); 18 | } 19 | 20 | render() { 21 | if (this.props.user.authenticated) { 22 | return null; 23 | } else { 24 | return ( 25 | 26 |

If you don't have an account, one will be created when you sign in.

27 | 28 | { 29 | process.env.NODE_ENV === 'development' 30 | ? ( 31 | 34 | ) 35 | : null 36 | } 37 |
38 | 39 |
40 | 41 |

By signing in you agree to our terms of use.

42 |
43 | ); 44 | } 45 | } 46 | } 47 | 48 | export class SignInButtons extends React.Component { 49 | render() { 50 | return ( 51 | 52 | { 53 | Object.keys(this.props.providers).map((provider, i) => { 54 | const signin = this.props.providers[provider].signin; 55 | return signin ? ( 56 | 57 | Sign in with {provider} 58 | 59 | ) : null; 60 | }) 61 | } 62 | 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/ui/src/visualizationRunner/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Measure from 'react-measure'; 3 | import { RunnerIFrame } from './runnerIframe'; 4 | import { computeScale } from './computeScale'; 5 | import { GetSrcDoc } from './getSrcDoc'; 6 | 7 | export class VisualizationRunner extends Component { 8 | constructor() { 9 | super(); 10 | this.state = { bounds: { width: -1, height: -1 } }; 11 | this.onResize = ({ bounds }) => this.setState({ bounds }); 12 | this.getSrcDoc = GetSrcDoc(); 13 | } 14 | 15 | render() { 16 | const { width, height, files, runId, measureHeight } = this.props; 17 | const boundsWidth = this.state.bounds.width; 18 | const boundsHeight = measureHeight ? this.state.bounds.height : Infinity; 19 | const scale = computeScale({ boundsWidth, boundsHeight, width, height }); 20 | const srcDoc = this.getSrcDoc(files, runId); 21 | 22 | // These properties are for vertical centering in fullscreen mode. 23 | const measureStyle = { 24 | display: 'flex', 25 | justifyContent: 'center', 26 | alignItems: 'center' 27 | } 28 | 29 | // Required for correct resize in fullscreen mode. 30 | // Breaks the visualization editor view if enabled there. 31 | if (measureHeight) { 32 | measureStyle.height = '100%'; 33 | } 34 | 35 | return ( 36 | 37 | {({ measureRef }) => 38 |
39 |
44 | 51 |
52 |
53 | } 54 |
55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/useCases/src/interactors/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | CreateVisualization, 3 | CreateVisualizationRequestModel, 4 | CreateVisualizationResponseModel 5 | } from './createVisualization'; 6 | 7 | export { 8 | GetVisualization, 9 | GetVisualizationRequestModel, 10 | GetVisualizationResponseModel 11 | } from './getVisualization'; 12 | 13 | export { 14 | ExportVisualization, 15 | ExportVisualizationRequestModel, 16 | ExportVisualizationResponseModel 17 | } from './exportVisualization'; 18 | 19 | export { 20 | SaveVisualization, 21 | SaveVisualizationRequestModel, 22 | SaveVisualizationResponseModel 23 | } from './saveVisualization'; 24 | 25 | export { 26 | CreateDataset, 27 | CreateDatasetRequestModel, 28 | CreateDatasetResponseModel 29 | } from './createDataset'; 30 | 31 | export { 32 | GetDataset, 33 | GetDatasetRequestModel, 34 | GetDatasetResponseModel 35 | } from './getDataset'; 36 | 37 | export { 38 | ForkVisualization, 39 | ForkVisualizationRequestModel, 40 | ForkVisualizationResponseModel 41 | } from './forkVisualization'; 42 | 43 | export { 44 | CreateUser, 45 | CreateUserRequestModel, 46 | CreateUserResponseModel 47 | } from './createUser'; 48 | 49 | export { 50 | GetUser, 51 | GetUserRequestModel, 52 | GetUserResponseModel 53 | } from './getUser'; 54 | 55 | export { 56 | GetUserProfileData, 57 | GetUserProfileDataRequestModel, 58 | GetUserProfileDataResponseModel 59 | } from './getUserProfileData'; 60 | 61 | export { 62 | GetAllVisualizationInfos, 63 | GetAllVisualizationInfosResponseModel 64 | } from './getAllVisualizationInfos'; 65 | 66 | export { 67 | DeleteVisualization, 68 | DeleteVisualizationRequestModel, 69 | DeleteVisualizationResponseModel 70 | } from './deleteVisualization'; 71 | 72 | export { 73 | UpdateImages 74 | } from './updateImages'; 75 | 76 | export { 77 | GetThumbnail, 78 | GetThumbnailRequestModel, 79 | GetThumbnailResponseModel 80 | } from './getThumbnail'; 81 | 82 | export { 83 | GetPreview, 84 | GetPreviewRequestModel, 85 | GetPreviewResponseModel 86 | } from './getPreview'; 87 | 88 | -------------------------------------------------------------------------------- /packages/gateways/src/databaseVisualizationGateway.ts: -------------------------------------------------------------------------------- 1 | import { 2 | VisualizationGateway, 3 | CVRequest, 4 | CVResponse 5 | } from 'datavis-tech-use-cases'; 6 | 7 | import { 8 | Visualization, 9 | VisualizationInfo, 10 | VisualizationContent 11 | } from 'datavis-tech-entities'; 12 | 13 | export class DatabaseVisualizationGateway implements VisualizationGateway { 14 | database: any; // TODO Typescript - Database 15 | 16 | constructor(database) { 17 | this.database = database; 18 | } 19 | 20 | async createVisualization(options): Promise { 21 | const { 22 | owner, 23 | id, 24 | title, 25 | slug, 26 | description, 27 | files, 28 | forkedFrom, 29 | height 30 | } = options; 31 | 32 | const visualization = new Visualization({ 33 | visualizationInfo: new VisualizationInfo({ 34 | id, 35 | owner, 36 | title, 37 | slug, 38 | description, 39 | 40 | references: [], 41 | referencedBy: [], 42 | forks: [], 43 | forkedFrom, 44 | thumbnail: undefined, 45 | height 46 | }), 47 | visualizationContent: new VisualizationContent({ 48 | id, 49 | files 50 | }) 51 | }); 52 | 53 | return await this.database.createVisualization(visualization); 54 | } 55 | 56 | async getVisualization({ id }) { 57 | return await this.database.getVisualization({ id }); 58 | } 59 | 60 | async saveVisualization({ visualization }) { 61 | return await this.database.saveVisualization({ visualization }); 62 | } 63 | 64 | async getVisualizationInfosByUserId(id) { 65 | return await this.database.getVisualizationInfosByUserId(id); 66 | }; 67 | 68 | async getAllVisualizationInfos() { 69 | return await this.database.getAllVisualizationInfos(); 70 | }; 71 | 72 | async deleteVisualization({ id }) { 73 | return await this.database.deleteVisualization({ id }); 74 | } 75 | 76 | async setImagesUpdatedTimestamp(options) { 77 | return await this.database.setImagesUpdatedTimestamp(options); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /packages/presenters/src/bundle.js: -------------------------------------------------------------------------------- 1 | import { rollup } from 'rollup/dist/rollup.browser'; 2 | import buble from 'rollup-plugin-buble'; 3 | import hypothetical from './hypothetical'; 4 | import { d3Packages } from './d3Packages'; 5 | 6 | const transformFilesToObject = files => 7 | files 8 | .filter(file => file.name.endsWith('.js')) 9 | .reduce((accumulator, file) => { 10 | accumulator['./' + file.name] = file.text; 11 | return accumulator; 12 | }, {}); 13 | 14 | const outputOptions = { 15 | format: 'iife', 16 | name: 'bundle', 17 | sourcemap: 'inline', 18 | globals: d3Packages.reduce((accumulator, packageName) => { 19 | accumulator[packageName] = 'd3'; 20 | return accumulator; 21 | }, { 22 | react: 'React' 23 | }) 24 | }; 25 | 26 | export const bundle = async (files) => { 27 | const inputOptions = { 28 | input: './index.js', 29 | plugins: [ 30 | hypothetical({ 31 | files: transformFilesToObject(files) 32 | }), 33 | buble({ 34 | // Disable most ES6 transforms, 35 | // use Buble mainly for its JSX transform. 36 | target: { 37 | chrome: 71 38 | } 39 | }) 40 | ], 41 | external: d3Packages.concat('react') 42 | }; 43 | 44 | const rollupBundle = await rollup(inputOptions); 45 | const { code, map } = await rollupBundle.generate(outputOptions); 46 | 47 | // Monkey patch magic-string internals 48 | // to support characters outside of the Latin1 range, e.g. Cyrillic. 49 | // 50 | // Related reading: 51 | // - https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/btoa#Unicode_strings 52 | // - https://github.com/Rich-Harris/magic-string/blob/3466b0230dddc95eb378ed3e0d199e36fbd1f572/src/SourceMap.js#L3 53 | const toString = map.toString.bind(map); 54 | map.toString = () => unescape(encodeURIComponent(toString())); 55 | 56 | // Inspired by https://github.com/rollup/rollup/issues/121 57 | const codeWithSourceMap = code + '\n//# sourceMappingURL=' + map.toUrl(); 58 | 59 | return [{ 60 | name: 'bundle.js', 61 | text: codeWithSourceMap 62 | }]; 63 | }; 64 | -------------------------------------------------------------------------------- /packages/web/pages/upload-dataset/body.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | import { FileUploader } from './fileUploader'; 3 | import { suggestedName } from './suggestedName'; 4 | import { AfterFileChosen } from './afterFileChosen'; 5 | import { toFileName } from './toFileName'; 6 | 7 | export class BodyAuthenticated extends Component { 8 | 9 | constructor(props) { 10 | super(props); 11 | 12 | this.state = { 13 | chosenFile: null 14 | }; 15 | 16 | this.onFileChosen = file => { 17 | this.setState({ 18 | chosenFile: file, 19 | name: suggestedName(file.name) 20 | }); 21 | }; 22 | 23 | this.onNameChange = name => { 24 | this.setState({ 25 | chosenFile: Object.assign({}, this.state.chosenFile, { 26 | name: toFileName(name) 27 | }), 28 | name 29 | }); 30 | }; 31 | 32 | this.onSourceChange = sourceName => { 33 | this.setState({ sourceName }); 34 | }; 35 | 36 | this.onSourceUrlChange= sourceUrl => { 37 | this.setState({ sourceUrl }); 38 | }; 39 | 40 | this.onUploadClick = () => { 41 | this.props.onUploadDataset({ 42 | name: this.state.name, 43 | file: this.state.chosenFile, 44 | sourceName: this.state.sourceName, 45 | sourceUrl: this.state.sourceUrl, 46 | }); 47 | }; 48 | } 49 | 50 | render() { 51 | const { chosenFile, name } = this.state; 52 | const { userName } = this.props; 53 | return ( 54 | chosenFile 55 | ? ( 56 | 65 | ) 66 | : 67 | ); 68 | } 69 | 70 | }; 71 | 72 | export const BodyNotAuthenticated = () => ( 73 |
You must first log in to upload a dataset.
74 | ); 75 | --------------------------------------------------------------------------------