├── .gitignore
├── Makefile
├── README.md
├── _config.yml
├── entitlements.mac.plist
├── logo.png
├── package-lock.json
├── package.json
├── public
└── index.html
├── scripts
└── notarize.js
├── src
├── index.tsx
├── main
│ ├── actionTypes.ts
│ ├── components
│ │ ├── CustomMenu.ts
│ │ ├── Window.ts
│ │ └── index.ts
│ ├── electron.ts
│ ├── preload.js
│ └── utilities.ts
├── react-app-env.d.ts
└── renderer
│ ├── App.tsx
│ ├── serviceWorker.ts
│ └── static
│ └── logoBase64.ts
├── tsconfig.json
└── types
└── electron-reload.d.ts
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | public
4 | dist
5 | build
6 |
7 | # dependencies
8 | /node_modules
9 | /.pnp
10 | .pnp.js
11 |
12 | # testing
13 | /coverage
14 |
15 | # production
16 | /build
17 | /dist
18 |
19 | # misc
20 | .DS_Store
21 | .env.local
22 | .env.development.local
23 | .env.test.local
24 | .env.production.local
25 |
26 | npm-debug.log*
27 | yarn-debug.log*
28 | yarn-error.log*
29 |
30 | *.p12
31 | *.plist
32 | .env
33 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | MAKEFLAGS += -j2
2 |
3 | install:
4 | npm install
5 |
6 | link:
7 | npm link @alephdata/react-ftm
8 |
9 | clean:
10 | rm -rf node_modules dist build
11 |
12 | dev:
13 | npm run start-renderer-dev & npm run start-app-dev
14 |
15 | dev-browser-only:
16 | npm run start-renderer-dev-browser-only
17 |
18 | build:
19 | npm run build
20 |
21 | release-patch:
22 | npm version patch
23 | npm run release
24 |
25 | release-minor:
26 | npm version minor
27 | npm run release
28 |
29 | release-major:
30 | npm version minor
31 | npm run release
32 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Aleph Data Desktop
2 |
3 | ***
4 |
5 | **September 2022: Aleph Data Desktop has been deprecated and will not receive any further updates. All of Data Desktop’s features are also available within Aleph. Please refer to the Aleph documentation to find out [how to create network diagrams in Aleph](https://docs.alephdata.org/guide/building-out-your-investigation/network-diagrams) and how to [import diagrams created in Data Desktop](https://docs.alephdata.org/guide/building-out-your-investigation/network-diagrams).**
6 |
7 | ***
8 |
9 | **Data Desktop** is a tool for visualizing and exploring complex networks.
10 |
11 | It is built with investigative reporting as its primary use case and utilizes the [Follow the Money](https://github.com/alephdata/followthemoney) data model, which seeks to provide a common language to describe the entities most commonly used in investigative reporting.
12 |
13 | **To get started, download the latest release for [Mac](https://github.com/alephdata/datadesktop/releases/latest/download/Aleph-Data-Desktop.dmg), [Windows](https://github.com/alephdata/datadesktop/releases/latest/download/Aleph-Data-Desktop.exe), and [Linux](https://github.com/alephdata/datadesktop/releases/latest/download/Aleph.Data.Desktop.deb).**
14 |
15 | For more information, read the [Data Desktop documentation](https://docs.alephdata.org/guide/aleph-data-desktop).
16 |
17 | Feel free to reach out with any feedback by emailing data@occrp.org or by posting in the [Aleph Slack](https://alephdata.slack.com).
18 |
--------------------------------------------------------------------------------
/_config.yml:
--------------------------------------------------------------------------------
1 | title: Aleph Data Desktop
2 | description: A tool for visualizing and exploring complex networks
3 | theme: jekyll-theme-minimal
4 |
--------------------------------------------------------------------------------
/entitlements.mac.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.cs.allow-jit
6 |
7 | com.apple.security.cs.allow-unsigned-executable-memory
8 |
9 | com.apple.security.cs.allow-dyld-environment-variables
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alephdata/datadesktop/ea79a51d3134b6c285df708ac741c5aac72d0104/logo.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@alephdata/datadesktop",
3 | "version": "2.7.0",
4 | "description": "Desktop graph visualization application",
5 | "repository": {
6 | "type": "git",
7 | "url": "git+https://github.com/alephdata/datadesktop.git"
8 | },
9 | "author": "OCCRP Data ",
10 | "license": "MIT",
11 | "bugs": {
12 | "url": "https://github.com/alephdata/datadesktop/issues"
13 | },
14 | "homepage": "./",
15 | "main": "build/electron.js",
16 | "build": {
17 | "appId": "com.electron.visdesktop",
18 | "productName": "Aleph Data Desktop",
19 | "publish": "github",
20 | "afterSign": "scripts/notarize.js",
21 | "artifactName": "${productName}.${ext}",
22 | "fileAssociations": [
23 | {
24 | "ext": "ftm",
25 | "name": "FTM"
26 | },
27 | {
28 | "ext": "vis",
29 | "name": "VIS"
30 | }
31 | ],
32 | "files": [
33 | "src/main/preload.js",
34 | "build/**/*",
35 | "node_modules/**/*",
36 | "icon.*"
37 | ],
38 | "mac": {
39 | "icon": "./logo.png",
40 | "hardenedRuntime": true,
41 | "gatekeeperAssess": false,
42 | "entitlements": "./entitlements.mac.plist",
43 | "entitlementsInherit": "./entitlements.mac.plist",
44 | "target": [
45 | {
46 | "target": "dmg",
47 | "arch": ["universal"]
48 | }
49 | ]
50 | },
51 | "linux": {
52 | "target": "deb",
53 | "executableName": "AlephDataDesktop",
54 | "icon": "./logo.png"
55 | },
56 | "win": {
57 | "target": "portable",
58 | "icon": "./logo.png"
59 | }
60 | },
61 | "dependencies": {
62 | "@babel/runtime": "^7.16.7",
63 | "electron-is-dev": "^2.0.0",
64 | "electron-log": "^4.4.5",
65 | "electron-updater": "^4.6.1",
66 | "react": "^16.11.0",
67 | "react-dom": "^16.11.0",
68 | "react-scripts": "^4.0.3",
69 | "typescript": "^4.5.5"
70 | },
71 | "scripts": {
72 | "react-start": "react-scripts start",
73 | "react-build": "react-scripts build",
74 | "react-test": "react-scripts test --env=jsdom",
75 | "react-eject": "react-scripts eject",
76 | "start-renderer-dev": "cross-env BROWSER=none npm run react-start",
77 | "start-renderer-dev-browser-only": "REACT_APP_ENV=browser_only npm run react-start",
78 | "start-app-dev": "tsc-watch ./src/main/electron.ts --outDir ./build --onSuccess \"electron ./build/electron.js\" --onFailure \"echo Compilation Failed\" --compiler typescript/bin/tsc",
79 | "electron-build": "tsc ./src/main/electron.ts --outDir ./build && electron-builder",
80 | "electron-dist": "tsc ./src/main/electron.ts --outDir ./build && electron-builder -mwl -p always",
81 | "build": "npm run react-build && npm run electron-build",
82 | "release": "npm run react-build && npm run electron-dist"
83 | },
84 | "eslintConfig": {
85 | "extends": "react-app"
86 | },
87 | "browserslist": {
88 | "production": [
89 | ">0.2%",
90 | "not dead",
91 | "not op_mini all"
92 | ],
93 | "development": [
94 | "last 1 chrome version",
95 | "last 1 firefox version",
96 | "last 1 safari version"
97 | ]
98 | },
99 | "devDependencies": {
100 | "@alephdata/react-ftm": "^2.6.6",
101 | "@blueprintjs/core": "3.52.0",
102 | "@blueprintjs/select": "3.18.11",
103 | "@types/node": "^17.0.13",
104 | "@types/react": "^17.0.2",
105 | "@types/react-dom": "^17.0.1",
106 | "cross-env": "^7.0.3",
107 | "electron": "^16.0.8",
108 | "electron-builder": "^23.1.0",
109 | "electron-notarize": "^1.1.1",
110 | "electron-reload": "^2.0.0-alpha.1",
111 | "tsc-watch": "^4.6.0"
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
21 | VIS
22 |
23 |
24 |
25 |
26 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/scripts/notarize.js:
--------------------------------------------------------------------------------
1 | // See: https://medium.com/@TwitterArchiveEraser/notarize-electron-apps-7a5f988406db
2 |
3 | require('dotenv').config();
4 | const fs = require('fs');
5 | const path = require('path');
6 | var electron_notarize = require('electron-notarize');
7 |
8 | module.exports = async function (params) {
9 | console.log(params.electronPlatformName, process.platform);
10 | // Only notarize the app on Mac OS only.
11 | if (params.electronPlatformName !== 'mac' && params.electronPlatformName !== 'darwin') {
12 | return;
13 | }
14 | console.log('afterSign hook triggered', params);
15 |
16 | // Same appId in electron-builder.
17 | let appId = 'com.electron.visdesktop'
18 |
19 | let appPath = path.join(params.appOutDir, `${params.packager.appInfo.productFilename}.app`);
20 | if (!fs.existsSync(appPath)) {
21 | throw new Error(`Cannot find application at: ${appPath}`);
22 | }
23 |
24 | console.log(`Notarizing ${appId} found at ${appPath}`);
25 |
26 | try {
27 | await electron_notarize.notarize({
28 | appBundleId: appId,
29 | appPath: appPath,
30 | appleId: process.env.APPLE_ID,
31 | appleIdPassword: process.env.APPLE_APP_PASSWORD,
32 | });
33 | } catch (error) {
34 | console.error(error);
35 | }
36 |
37 | console.log(`Done notarizing ${appId}`);
38 | };
39 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './renderer/App';
4 |
5 | import * as serviceWorker from './renderer/serviceWorker';
6 | declare global {
7 | interface Window {
8 | electron: any;
9 | }
10 | }
11 |
12 | let ipcRenderer;
13 |
14 | if (process.env.REACT_APP_ENV !== 'browser_only') {
15 | ipcRenderer = window.electron.ipcRenderer;
16 | }
17 |
18 | ReactDOM.render(, document.getElementById('root'));
19 |
20 | // If you want your app to work offline and load faster, you can change
21 | // unregister() to register() below. Note this comes with some pitfalls.
22 | // Learn more about service workers: https://bit.ly/CRA-PWA
23 | serviceWorker.unregister();
24 |
--------------------------------------------------------------------------------
/src/main/actionTypes.ts:
--------------------------------------------------------------------------------
1 | export const SAVE_FILE = 'SAVE_FILE';
2 | export const SAVE_FILE_SUCCESS = 'SAVE_FILE_SUCCESS'
3 | export const OPEN_FILE = 'OPEN_FILE'
4 | export const OPEN_FILE_SUCCESS = 'OPEN_FILE_SUCCESS'
5 | export const NEW_FILE = 'NEW_FILE'
6 |
--------------------------------------------------------------------------------
/src/main/components/CustomMenu.ts:
--------------------------------------------------------------------------------
1 | import { app, BrowserWindow, Menu } from 'electron'
2 |
3 | export const CustomMenu = (sendSaveFile: any, newFile: any, openFileDialog: any, sendExportSvg: any) => {
4 | return Menu.buildFromTemplate([
5 | {
6 | label: 'Aleph Data Desktop',
7 | submenu: [
8 | { role: 'about' },
9 | { type: 'separator' },
10 | { role: 'services' },
11 | { type: 'separator' },
12 | { role: 'hide' },
13 | { role: 'unhide' },
14 | { role: 'quit' }
15 | ]
16 | },
17 | {
18 | label: 'File',
19 | submenu: [
20 | {
21 | label: 'New',
22 | accelerator: 'cmd+N',
23 | click: () => {
24 | newFile()
25 | }
26 | },
27 | {
28 | label: 'Save',
29 | accelerator: 'cmd+S',
30 | click: () => {
31 | sendSaveFile(false)
32 | }
33 | },
34 | {
35 | label: 'Save as...',
36 | accelerator: 'Shift+cmd+S',
37 | click: () => {
38 | sendSaveFile(true)
39 | }
40 | },
41 | {
42 | label: 'Open',
43 | accelerator: 'cmd+O',
44 | click: () => {
45 | openFileDialog()
46 | }
47 | },
48 | {
49 | label: 'Export as SVG',
50 | click: () => {
51 | sendExportSvg()
52 | }
53 | }
54 | ]
55 | },
56 | {
57 | label: 'Edit',
58 | submenu: [
59 | // {label: 'Undo', role: 'undo' },
60 | // {label: 'Redo', role: 'redo' },
61 | {label: 'Cut', accelerator: "CmdOrCtrl+X", role: 'cut' },
62 | {label: 'Copy', accelerator: "CmdOrCtrl+C", role: 'copy' },
63 | {label: 'Paste', accelerator: "CmdOrCtrl+V", role:'paste' },
64 | // { role: 'delete' },
65 | { type: 'separator' },
66 | { role: 'selectAll', accelerator: "CmdOrCtrl+A", }
67 | ]
68 | },
69 | {
70 | label: 'Window',
71 | submenu: [
72 | { role: 'minimize' },
73 | { role: 'zoom' },
74 | { type: 'separator' },
75 | { role: 'front' },
76 | { type: 'separator' },
77 | { role: 'window' }
78 | ]
79 | },
80 | ])
81 | }
82 |
--------------------------------------------------------------------------------
/src/main/components/Window.ts:
--------------------------------------------------------------------------------
1 | import { app, BrowserWindow, dialog, SaveDialogOptions } from 'electron'
2 | import * as path from 'path'
3 | import * as isDev from "electron-is-dev" ;
4 | import { addFileExtension } from '../utilities'
5 | import * as fs from 'fs'
6 |
7 |
8 | import { CustomMenu } from './CustomMenu'
9 |
10 | export class Window {
11 | id: number
12 | win: BrowserWindow
13 | filePath: string | undefined
14 | hasUnsavedChanges: boolean
15 | onFocus: any
16 |
17 | constructor(props: any) {
18 | const {id, onFocus} = props
19 | const win = new BrowserWindow({
20 | width: 1000,
21 | height: 800,
22 | show: false,
23 | webPreferences: { preload: path.join(__dirname, "../../src/main/preload.js") },
24 | });
25 |
26 | win.loadURL(
27 | isDev
28 | ? "http://localhost:3000" :
29 | `file://${path.join(__dirname, "../../build/index.html")}`
30 | );
31 |
32 | if (isDev) {
33 | win.webContents.openDevTools()
34 | }
35 |
36 | this.win = win
37 | this.id = id
38 | this.hasUnsavedChanges = false
39 | this.onFocus = onFocus
40 | this.attachListeners()
41 | }
42 |
43 | attachListeners() {
44 | this.win.on('closed', () => this.win.destroy())
45 | this.win.on('focus', () => this.onFocus(this.id))
46 | this.win.once('ready-to-show', () => this.onReady())
47 | }
48 |
49 | sendSaveFile(saveAs?:boolean) {
50 | this.sendMessage('SAVE_FILE', saveAs)
51 | }
52 |
53 | receiveSaveFile(props: any) {
54 | const {graphData, saveAs} = props
55 | if (!saveAs && this.filePath) {
56 | this.writeFile(this.filePath, graphData)
57 | } else {
58 | dialog.showSaveDialog((this.win, {defaultPath: ''}) as SaveDialogOptions).then(({filePath, canceled}) => {
59 | if (!canceled && filePath) {
60 | const withExtension: string = addFileExtension(filePath, '.ftm')
61 | this.writeFile(withExtension, graphData)
62 | }
63 | })
64 | }
65 | }
66 |
67 | writeFile(filePath: string, contents: any) {
68 | fs.writeFile(filePath, contents, (err) => {
69 | if (err) {
70 | console.log(err);
71 | return;
72 | }
73 | this.filePath = filePath
74 | this.hasUnsavedChanges = false
75 | this.setTitle()
76 | });
77 | }
78 |
79 | sendOpenFile(filePath: string, data: any) {
80 | this.win.webContents.on('did-finish-load', () => {
81 | this.filePath = filePath
82 | this.hasUnsavedChanges = false
83 | this.setTitle()
84 | this.sendMessage('OPEN_FILE', data)
85 | })
86 | }
87 |
88 | sendExportSvg() {
89 | this.sendMessage('EXPORT_SVG');
90 | }
91 |
92 | receiveExportSvg(data: any) {
93 | dialog.showSaveDialog((this.win, {defaultPath: ''}) as SaveDialogOptions).then(({filePath, canceled}) => {
94 | if (!canceled && filePath) {
95 | const withExtension: string = addFileExtension(filePath, '.svg')
96 | fs.writeFile(withExtension, data, (err) => {
97 | if (err) {
98 | console.log(err);
99 | return;
100 | }
101 | });
102 | }
103 | })
104 | }
105 |
106 | setTitle() {
107 | const base = this.filePath ? path.basename(this.filePath) : 'Untitled'
108 | const title = this.hasUnsavedChanges ? `${base}*` : base
109 | this.win.setTitle(title)
110 | }
111 |
112 | sendMessage(type: string, ...contents:any[]) {
113 | this.win.webContents.send(type, ...contents);
114 | }
115 |
116 | onGraphChanged() {
117 | this.hasUnsavedChanges = true
118 | this.setTitle()
119 | }
120 |
121 | onReady() {
122 | this.setTitle()
123 | let locale = app.getLocale();
124 | locale = (locale === "en_GB" || locale === "en_US") ? "en" : locale;
125 | this.sendMessage('SET_LOCALE', locale);
126 | this.win.show()
127 | }
128 |
129 | destroy() {
130 | this.win.destroy()
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/src/main/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './CustomMenu'
2 | export * from './Window'
3 |
--------------------------------------------------------------------------------
/src/main/electron.ts:
--------------------------------------------------------------------------------
1 | import { BrowserWindow , app , ipcMain, IpcMessageEvent, Menu, dialog } from 'electron' ;
2 | import * as isDev from "electron-is-dev" ;
3 | import { autoUpdater } from 'electron-updater'
4 | import { Window, CustomMenu } from './components'
5 | import * as fs from 'fs'
6 |
7 | let appInstance: any;
8 |
9 | app.on('ready', function() {
10 | appInstance = new App({})
11 | });
12 |
13 | app.on("open-file", function(event, path) {
14 | event.preventDefault()
15 | let fileToOpen = path
16 |
17 | if (process.platform == 'win32' && process.argv.length >= 2) {
18 | fileToOpen = process.argv[1];
19 | }
20 | if (appInstance && app.isReady()) {
21 | appInstance.openFile(fileToOpen)
22 | } else {
23 | app.on('ready', function() {
24 | appInstance.openFile(fileToOpen)
25 | });
26 | }
27 | });
28 |
29 | app.on('window-all-closed', () => {
30 | if (process.platform !== "darwin") {
31 | app.quit();
32 | }
33 | });
34 |
35 | class App {
36 | private windows: Window[]
37 | private activeWindow: number
38 |
39 | constructor({ fileToOpen }: any) {
40 | this.onWindowFocus = this.onWindowFocus.bind(this)
41 | const mainWindow = new Window({id: 0, onFocus: this.onWindowFocus})
42 | if (fileToOpen) {
43 | this.openFile(fileToOpen)
44 | }
45 | const log = require("electron-log")
46 | log.transports.file.level = "debug"
47 | autoUpdater.logger = log
48 | autoUpdater.checkForUpdatesAndNotify();
49 |
50 | Menu.setApplicationMenu(CustomMenu(
51 | this.sendSaveFile.bind(this),
52 | this.newFile.bind(this),
53 | this.openFileDialog.bind(this),
54 | this.exportSvg.bind(this)
55 | ))
56 |
57 | this.windows = [mainWindow]
58 | this.activeWindow = 0
59 |
60 | this.attachListeners()
61 | }
62 |
63 | attachListeners() {
64 | ipcMain.on('SAVE_FILE_SUCCESS', this.receiveSaveFile.bind(this))
65 | ipcMain.on('GRAPH_CHANGED', this.onGraphChanged.bind(this))
66 | ipcMain.on('RECEIVE_EXPORT_SVG', this.receiveExportSvg.bind(this))
67 | }
68 |
69 | onWindowFocus(id: number) {
70 | this.activeWindow = id
71 | }
72 |
73 | sendSaveFile(saveAs?:boolean) {
74 | this.windows[this.activeWindow].sendSaveFile(saveAs)
75 | }
76 |
77 | receiveSaveFile(event: any, contents: any) {
78 | this.windows[this.activeWindow].receiveSaveFile(contents)
79 | }
80 |
81 | onGraphChanged() {
82 | this.windows[this.activeWindow].onGraphChanged()
83 | }
84 |
85 | exportSvg() {
86 | this.windows[this.activeWindow].sendExportSvg()
87 | }
88 |
89 | receiveExportSvg(event: any, data: any) {
90 | this.windows[this.activeWindow].receiveExportSvg(data)
91 | }
92 |
93 | newFile() {
94 | const newId = this.windows.length
95 | const newWindow = new Window({id: newId, onFocus:this.onWindowFocus})
96 | this.windows.push(newWindow)
97 | this.activeWindow = newId
98 | return newWindow
99 | }
100 |
101 | openFileDialog() {
102 | dialog.showOpenDialog({filters:[{name: '*',extensions:['ftm','vis']}]}).then(({filePaths}) => {
103 | if (filePaths && filePaths.length > 0) {
104 | filePaths.forEach(filePath => {
105 | this.openFile(filePath)
106 | })
107 | }
108 | });
109 | }
110 |
111 | openFile(filePath: string) {
112 | fs.readFile(filePath, 'utf-8', (err, data) => {
113 | if (err){
114 | console.log("An error ocurred reading the file :" + err.message);
115 | return;
116 | }
117 | const newWindow = this.newFile()
118 | newWindow.sendOpenFile(filePath, data)
119 | });
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/src/main/preload.js:
--------------------------------------------------------------------------------
1 | const { contextBridge, ipcRenderer } = require('electron')
2 |
3 | contextBridge.exposeInMainWorld("electron", {
4 | ipcRenderer: {
5 | sendSaveFileSuccess: (payload) => ipcRenderer.send('SAVE_FILE_SUCCESS', payload),
6 | sendReceiveExportSvg: (payload) => ipcRenderer.send('RECEIVE_EXPORT_SVG', payload),
7 | sendGraphChanged: () => ipcRenderer.send('GRAPH_CHANGED'),
8 | onSaveFile: (callback) => ipcRenderer.on('SAVE_FILE', callback),
9 | onOpenFile: (callback) => ipcRenderer.on('OPEN_FILE', callback),
10 | onExportSvg: (callback) => ipcRenderer.on('EXPORT_SVG', callback),
11 | onSetLocale: (callback) => ipcRenderer.on('SET_LOCALE', callback),
12 | },
13 | });
--------------------------------------------------------------------------------
/src/main/utilities.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'path'
2 |
3 | export const addFileExtension = (filePath: string, extension: string) => {
4 | const currExtension = path.extname(filePath)
5 | if (currExtension === '') {
6 | return filePath + extension
7 | } else {
8 | return currExtension === extension ? filePath : filePath.replace(currExtension, extension)
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/renderer/App.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { EntityManager, exportSvg, GraphConfig, GraphLayout, GraphLogo, Viewport, NetworkDiagram } from '@alephdata/react-ftm';
3 | import logoBase64 from './static/logoBase64';
4 |
5 | const logo = new GraphLogo({
6 | text: "Data Desktop",
7 | image: logoBase64,
8 | });
9 |
10 | const config = new GraphConfig({ editorTheme: "dark", toolbarPosition: 'top', logo });
11 |
12 | interface IAppProps {
13 | ipcRenderer: any
14 | }
15 |
16 | interface IAppState {
17 | layout: GraphLayout,
18 | locale?: string,
19 | viewport: Viewport
20 | }
21 |
22 | export default class App extends React.Component {
23 | entityManager: EntityManager
24 | saveTimeout: any
25 | svgRef: React.RefObject
26 |
27 | constructor(props: any) {
28 | super(props)
29 | this.attachListeners()
30 |
31 | const storedGraphData = localStorage.getItem('storedGraphData')
32 |
33 | if (storedGraphData && !props.ipcRenderer) {
34 | const parsed = JSON.parse(storedGraphData)
35 | this.state = {
36 | // @ts-ignore
37 | layout: GraphLayout.fromJSON(config, parsed.layout),
38 | viewport: Viewport.fromJSON(config, parsed.viewport),
39 | }
40 | this.entityManager = EntityManager.fromJSON({}, parsed.entities);
41 | } else {
42 | this.state = {
43 | // @ts-ignore
44 | layout: new GraphLayout(config),
45 | viewport: new Viewport(config)
46 | }
47 | this.entityManager = new EntityManager();
48 | }
49 |
50 | this.updateLayout = this.updateLayout.bind(this);
51 | this.updateViewport = this.updateViewport.bind(this);
52 | this.saveFile = this.saveFile.bind(this);
53 | this.openFile = this.openFile.bind(this);
54 | this.exportSvg = this.exportSvg.bind(this);
55 |
56 | this.svgRef = React.createRef();
57 | }
58 |
59 | attachListeners() {
60 | const { ipcRenderer } = this.props;
61 |
62 | if (ipcRenderer) {
63 | ipcRenderer.onSaveFile((event: any, saveAs: boolean) => this.saveFile(saveAs));
64 | ipcRenderer.onOpenFile((event: any, data: any) => this.openFile(data));
65 | ipcRenderer.onExportSvg((event: any) => this.exportSvg());
66 | ipcRenderer.onSetLocale((event: any, locale: any) => this.setLocale(locale));
67 | }
68 | }
69 |
70 | saveFile(saveAs: boolean) {
71 | const { ipcRenderer } = this.props;
72 | const { layout, viewport } = this.state;
73 | const graphData = JSON.stringify({
74 | entities: this.entityManager.toJSON(),
75 | layout: layout.toJSON(),
76 | viewport: viewport.toJSON()
77 | })
78 |
79 | ipcRenderer.sendSaveFileSuccess({ graphData, saveAs });
80 | }
81 |
82 | openFile(data: any) {
83 | const parsed = JSON.parse(data);
84 | // supports legacy .vis files with layout.entities
85 | const { entities, ...layout } = parsed.layout;
86 | this.entityManager = EntityManager.fromJSON({}, entities || parsed.entities);
87 | this.setState({
88 | // @ts-ignore
89 | layout: GraphLayout.fromJSON(config, layout),
90 | viewport: Viewport.fromJSON(config, parsed.viewport),
91 | })
92 | }
93 |
94 | setLocale(locale: string) {
95 | this.setState({ locale });
96 | }
97 |
98 | exportSvg() {
99 | const { ipcRenderer } = this.props;
100 | const { layout, viewport } = this.state;
101 |
102 | const data = exportSvg(layout, viewport, this.svgRef.current);
103 | ipcRenderer.sendReceiveExportSvg(data);
104 | }
105 |
106 | updateLayout(layout: GraphLayout, historyModified: boolean = false) {
107 | const { ipcRenderer } = this.props;
108 |
109 | this.setState({'layout': layout})
110 |
111 | if (historyModified) {
112 | if (ipcRenderer) {
113 | ipcRenderer.sendGraphChanged();
114 | } else {
115 | this.saveToLocalStorage({ layout });
116 | }
117 | }
118 | }
119 |
120 | updateViewport(viewport: Viewport) {
121 | const { ipcRenderer } = this.props;
122 | this.setState({'viewport': viewport})
123 | if (!ipcRenderer) {
124 | this.saveToLocalStorage({ viewport });
125 | }
126 | }
127 |
128 | saveToLocalStorage({ layout, viewport }: { layout?: GraphLayout, viewport?: Viewport }) {
129 | const graphData = JSON.stringify({
130 | entities: this.entityManager.toJSON(),
131 | layout: layout ? layout.toJSON() : this.state.layout.toJSON(),
132 | viewport: viewport ? viewport.toJSON() : this.state.viewport.toJSON()
133 | })
134 | localStorage.setItem('storedGraphData', graphData)
135 | }
136 |
137 | render() {
138 | const { layout, locale, viewport } = this.state;
139 |
140 | return (
141 |
152 | )
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/src/renderer/serviceWorker.ts:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | type Config = {
24 | onSuccess?: (registration: ServiceWorkerRegistration) => void;
25 | onUpdate?: (registration: ServiceWorkerRegistration) => void;
26 | };
27 |
28 | export function register(config?: Config) {
29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
30 | // The URL constructor is available in all browsers that support SW.
31 | const publicUrl = new URL(
32 | (process as { env: { [key: string]: string } }).env.PUBLIC_URL,
33 | window.location.href
34 | );
35 | if (publicUrl.origin !== window.location.origin) {
36 | // Our service worker won't work if PUBLIC_URL is on a different origin
37 | // from what our page is served on. This might happen if a CDN is used to
38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
39 | return;
40 | }
41 |
42 | window.addEventListener('load', () => {
43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
44 |
45 | if (isLocalhost) {
46 | // This is running on localhost. Let's check if a service worker still exists or not.
47 | checkValidServiceWorker(swUrl, config);
48 |
49 | // Add some additional logging to localhost, pointing developers to the
50 | // service worker/PWA documentation.
51 | navigator.serviceWorker.ready.then(() => {
52 | console.log(
53 | 'This web app is being served cache-first by a service ' +
54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
55 | );
56 | });
57 | } else {
58 | // Is not localhost. Just register service worker
59 | registerValidSW(swUrl, config);
60 | }
61 | });
62 | }
63 | }
64 |
65 | function registerValidSW(swUrl: string, config?: Config) {
66 | navigator.serviceWorker
67 | .register(swUrl)
68 | .then(registration => {
69 | registration.onupdatefound = () => {
70 | const installingWorker = registration.installing;
71 | if (installingWorker == null) {
72 | return;
73 | }
74 | installingWorker.onstatechange = () => {
75 | if (installingWorker.state === 'installed') {
76 | if (navigator.serviceWorker.controller) {
77 | // At this point, the updated precached content has been fetched,
78 | // but the previous service worker will still serve the older
79 | // content until all client tabs are closed.
80 | console.log(
81 | 'New content is available and will be used when all ' +
82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
83 | );
84 |
85 | // Execute callback
86 | if (config && config.onUpdate) {
87 | config.onUpdate(registration);
88 | }
89 | } else {
90 | // At this point, everything has been precached.
91 | // It's the perfect time to display a
92 | // "Content is cached for offline use." message.
93 | console.log('Content is cached for offline use.');
94 |
95 | // Execute callback
96 | if (config && config.onSuccess) {
97 | config.onSuccess(registration);
98 | }
99 | }
100 | }
101 | };
102 | };
103 | })
104 | .catch(error => {
105 | console.error('Error during service worker registration:', error);
106 | });
107 | }
108 |
109 | function checkValidServiceWorker(swUrl: string, config?: Config) {
110 | // Check if the service worker can be found. If it can't reload the page.
111 | fetch(swUrl)
112 | .then(response => {
113 | // Ensure service worker exists, and that we really are getting a JS file.
114 | const contentType = response.headers.get('content-type');
115 | if (
116 | response.status === 404 ||
117 | (contentType != null && contentType.indexOf('javascript') === -1)
118 | ) {
119 | // No service worker found. Probably a different app. Reload the page.
120 | navigator.serviceWorker.ready.then(registration => {
121 | registration.unregister().then(() => {
122 | window.location.reload();
123 | });
124 | });
125 | } else {
126 | // Service worker found. Proceed as normal.
127 | registerValidSW(swUrl, config);
128 | }
129 | })
130 | .catch(() => {
131 | console.log(
132 | 'No internet connection found. App is running in offline mode.'
133 | );
134 | });
135 | }
136 |
137 | export function unregister() {
138 | if ('serviceWorker' in navigator) {
139 | navigator.serviceWorker.ready.then(registration => {
140 | registration.unregister();
141 | });
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/src/renderer/static/logoBase64.ts:
--------------------------------------------------------------------------------
1 | const logo = "data:image/png;base64,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";
2 |
3 | export default logo;
4 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "noEmit": true,
20 | "jsx": "react-jsx",
21 | "noFallthroughCasesInSwitch": true
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/types/electron-reload.d.ts:
--------------------------------------------------------------------------------
1 | export = main;
2 | declare const main: any;
3 |
--------------------------------------------------------------------------------