', methods=['POST'])
28 | @jwt_required()
29 | @identify_user
30 | def create_spark_app(spark_app_id):
31 | logging.info(f"Creating spark app with id: {spark_app_id}")
32 | data = request.get_json()
33 | notebook_path = data.get('notebookPath')
34 | return SparkApp.create_spark_app(spark_app_id=spark_app_id, notebook_path=notebook_path)
--------------------------------------------------------------------------------
/server/tests/models/test_user_model.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from flask_cors import CORS
3 | from run import create_app
4 | from database import db
5 | from app.models.user import UserModel
6 |
7 | class UserModelTestCase(unittest.TestCase):
8 |
9 | def setUp(self):
10 | self.app = create_app()
11 | self.client = self.app.test_client()
12 | with self.app.app_context():
13 | db.create_all()
14 |
15 | def tearDown(self):
16 | with self.app.app_context():
17 | db.session.remove()
18 | db.drop_all()
19 |
20 | def test_user_model(self):
21 | with self.app.app_context():
22 | user = UserModel(name='testuser', email='testuser@example.com')
23 | password = 'test_password'
24 | user.set_password(password)
25 | db.session.add(user)
26 | db.session.commit()
27 | assert user.id is not None
28 | assert user.name == 'testuser'
29 | assert user.email == 'testuser@example.com'
30 |
31 | def test_password_setter(self):
32 | with self.app.app_context():
33 | user = UserModel(name='testuser', email='testuser@example.com')
34 | password = 'test_password'
35 | user.set_password(password)
36 | db.session.add(user)
37 | db.session.commit()
38 | assert user.password_hash is not None
39 |
40 | def test_check_password(self):
41 | with self.app.app_context():
42 | user = UserModel(name='testuser', email='testuser@example.com')
43 | password = 'test_password'
44 | user.set_password(password)
45 | db.session.add(user)
46 | db.session.commit()
47 | assert user.check_password(password)
48 | assert not user.check_password('wrong_password')
--------------------------------------------------------------------------------
/server/tests/models/test_directory_model.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from flask_cors import CORS
3 | from run import create_app
4 | from database import db
5 | from app.models.directory import DirectoryModel
6 | from app.models.user import UserModel
7 |
8 | class DirectoryModelTestCase(unittest.TestCase):
9 |
10 | def setUp(self):
11 | self.app = create_app()
12 | self.client = self.app.test_client()
13 | with self.app.app_context():
14 | db.create_all()
15 |
16 | def tearDown(self):
17 | with self.app.app_context():
18 | db.session.remove()
19 | db.drop_all()
20 |
21 | def test_directory_model(self):
22 | with self.app.app_context():
23 | # Create user first
24 | user = UserModel(name='testuser', email='testuser@example.com')
25 | password = 'test_password'
26 | user.set_password(password)
27 | db.session.add(user)
28 | db.session.commit()
29 |
30 | # Create directory
31 | directory = DirectoryModel(name='Test Directory', path='/path/to/directory', user_id=user.id)
32 | db.session.add(directory)
33 | db.session.commit()
34 |
35 | self.assertIsNotNone(directory.id)
36 | self.assertEqual(directory.name, 'Test Directory')
37 | self.assertEqual(directory.path, '/path/to/directory')
38 |
39 | directory_dict = directory.to_dict()
40 | self.assertEqual(directory_dict, {
41 | 'id': directory.id,
42 | 'name': 'Test Directory',
43 | 'path': '/path/to/directory',
44 | 'user_id': user.id
45 | })
--------------------------------------------------------------------------------
/server/tests/models/test_notebook_model.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from flask_cors import CORS
3 | from run import create_app
4 | from database import db
5 | from app.models.notebook import NotebookModel
6 | from app.models.user import UserModel
7 |
8 | class NotebookModelTestCase(unittest.TestCase):
9 | def setUp(self):
10 | self.app = create_app()
11 | self.client = self.app.test_client()
12 | with self.app.app_context():
13 | db.create_all()
14 |
15 | def tearDown(self):
16 | with self.app.app_context():
17 | db.session.remove()
18 | db.drop_all()
19 |
20 | def test_notebook_model(self):
21 | with self.app.app_context():
22 | # Create user first
23 | user = UserModel(name='testuser', email='testuser@example.com')
24 | password = 'test_password'
25 | user.set_password(password)
26 | db.session.add(user)
27 | db.session.commit()
28 |
29 | # Create notebook
30 | notebook = NotebookModel(name='Test Notebook', path='/path/to/notebook', user_id=user.id)
31 | db.session.add(notebook)
32 | db.session.commit()
33 |
34 | self.assertIsNotNone(notebook.id)
35 | self.assertEqual(notebook.name, 'Test Notebook')
36 | self.assertEqual(notebook.path, '/path/to/notebook')
37 |
38 | notebook_dict = notebook.to_dict()
39 | self.assertEqual(notebook_dict, {
40 | 'id': notebook.id,
41 | 'name': 'Test Notebook',
42 | 'path': '/path/to/notebook',
43 | 'user_id': user.id
44 | })
45 |
46 | if __name__ == '__main__':
47 | unittest.main()
--------------------------------------------------------------------------------
/server/app/services/kernel.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from flask import Response
3 | import requests
4 | import json
5 | from flask import current_app as app
6 |
7 | logger = logging.getLogger(__name__)
8 |
9 | class Kernel:
10 |
11 | @staticmethod
12 | def get_kernel_by_id(kernel_id):
13 | try:
14 | response = requests.get(app.config['JUPYTER_KERNEL_API_PATH'] + f"/{kernel_id}")
15 | except Exception as e:
16 | logger.error(f"Met exception getting all kernels: {e}")
17 | return Response(
18 | response=json.dumps({'message': 'Error getting all kernels from Jupyter Server: ' + str(e)}),
19 | status=404)
20 |
21 | if response.status_code != 200:
22 | logger.error(f"Error getting kernel: {response.content}")
23 | return Response(
24 | response=json.dumps({'message': 'Error getting kernel'}),
25 | status=404)
26 |
27 | return Response(
28 | response=response,
29 | status=200,
30 | mimetype='application/json'
31 | )
32 |
33 | @staticmethod
34 | def restart_kernel(kernel_id):
35 | path = app.config['JUPYTER_KERNEL_API_PATH'] + f"/{kernel_id}/restart"
36 | try:
37 | response = requests.post(path)
38 | except Exception as e:
39 | logger.error(f"Met exception restarting kernel: {e}")
40 | return Response(
41 | response=json.dumps({'message': 'Error restarting kernel: ' + str(e)}),
42 | status=404)
43 |
44 | if response.status_code != 200:
45 | return Response(
46 | response=json.dumps({'message': 'Error restarting kernel'}),
47 | status=404)
48 |
49 | return Response(
50 | response=response.content,
51 | status=200,
52 | mimetype='application/json'
53 | )
--------------------------------------------------------------------------------
/webapp/src/components/notebook/content/cell/header/MoreButton.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { CgMoreVerticalAlt } from "react-icons/cg";
3 | import { Menu, MenuItem } from '@mui/material';
4 |
5 |
6 | const MoreButton = ({
7 | cell,
8 | index,
9 | handleCopyCell,
10 | handelRunAllAboveCells
11 | }) => {
12 |
13 | const [anchorEl, setAnchorEl] = useState(null);
14 |
15 | const handleMoreClicked = (event, cell, cellIndex) => {
16 | console.log('More clicked:', cell, cellIndex);
17 | setAnchorEl(event.currentTarget);
18 | }
19 |
20 | const handleClose = () => {
21 | setAnchorEl(null);
22 | };
23 |
24 | return (
25 |
26 | handleMoreClicked(event, cell, index)}
28 | onMouseEnter={(e) => {
29 | e.currentTarget.style.color = 'black';
30 | }}
31 | onMouseLeave={(e) => {
32 | e.currentTarget.style.color = 'grey';
33 | }}
34 | style={{
35 | color: 'grey',
36 | fontSize: '20px',
37 | marginTop: '10px',
38 | marginBottom: 0,
39 | marginLeft: '10px',
40 | marginRight: 0
41 | }}
42 | />
43 |
49 | {
51 | handleCopyCell(index)
52 | handleClose()}}
53 | >Copy Cell
54 | {
56 | handelRunAllAboveCells(index)
57 | handleClose()}}
58 | >Run All Above
59 |
60 |
61 | );
62 | }
63 |
64 | export default MoreButton;
--------------------------------------------------------------------------------
/webapp/src/components/sidebar/workspace/item/Item.js:
--------------------------------------------------------------------------------
1 | import { Typography, ListItem, ListItemIcon, ListItemText, Box } from '@mui/material';
2 | import MoreButton from './MoreButton';
3 | import NotebookModel from '../../../../models/NotebookModel';
4 |
5 | const Item = ({
6 | file,
7 | index,
8 | currentPath,
9 | handleDirectoryClick,
10 | onExistinNotebookClick,
11 | closeWorkspaceDrawer,
12 | IconComponent,
13 | setRefreshKey
14 | }) => {
15 | return (
16 |
17 | {
21 | if (file.type === 'directory') {
22 | handleDirectoryClick(file.path)
23 | } else if (file.type === 'notebook') {
24 | onExistinNotebookClick(file.path)
25 | closeWorkspaceDrawer();
26 | }}}>
27 |
28 |
29 |
30 |
31 |
43 | {NotebookModel.getNameWithoutExtension(file.name)}
44 |
45 |
46 |
47 |
51 |
52 | );
53 | }
54 |
55 | export default Item;
--------------------------------------------------------------------------------
/.github/workflows/build-examples.yml:
--------------------------------------------------------------------------------
1 | name: Build Examples
2 |
3 | # Controls when the workflow will run
4 | on:
5 | push:
6 | paths:
7 | - 'examples/**'
8 |
9 | pull_request:
10 | paths:
11 | - 'examples/**'
12 |
13 | # Allows you to run this workflow manually from the Actions tab
14 | workflow_dispatch:
15 | inputs:
16 | deploy-example:
17 | type: choice
18 | description: Example to Deploy
19 | options:
20 | - None
21 | - WordCount
22 |
23 | jobs:
24 | build-examples:
25 | runs-on: ubuntu-latest
26 |
27 | steps:
28 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
29 | - uses: actions/checkout@v3
30 |
31 | # Runs a set of commands using the runners shell
32 | - name: Maven Package
33 | run: |
34 | cd examples/user_0@gmail.com/word-count
35 | mvn clean package
36 |
37 | version=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)
38 | timestamp=$(date +"%Y%m%d%H%M%S")
39 | version_with_timestamp="${version}-${timestamp}"
40 | echo "Version with timestamp: $version_with_timestamp"
41 |
42 | echo "VERSION=$version" >> $GITHUB_ENV
43 | echo "VERSION_WITH_TIMESTAMP=$version_with_timestamp" >> $GITHUB_ENV
44 |
45 | - name: Log in to Docker Hub
46 | uses: docker/login-action@v1
47 | with:
48 | username: ${{ secrets.DOCKERHUB_USERNAME }}
49 | password: ${{ secrets.DOCKERHUB_PASSWORD }}
50 |
51 | - name: Docker Build & Push
52 | run: |
53 | cd examples/user_0@gmail.com/word-count
54 | docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/word-count:$VERSION_WITH_TIMESTAMP --build-arg VERSION=$VERSION .
55 | docker push ${{ secrets.DOCKERHUB_USERNAME }}/word-count:$VERSION_WITH_TIMESTAMP
56 |
57 |
--------------------------------------------------------------------------------
/server/config.py:
--------------------------------------------------------------------------------
1 | class Config(object):
2 | DEBUG = False
3 | TESTING = False
4 | SQLALCHEMY_DATABASE_URI = 'postgresql://user:password@localhost/production_db'
5 | JUPYTER_SERVER_PATH = 'http://localhost:8888'
6 | JUPYTER_CONTENT_API_PATH = JUPYTER_SERVER_PATH + '/api/contents'
7 | JUPYTER_SESSION_API_PATH = JUPYTER_SERVER_PATH + '/api/sessions'
8 | JUPYTER_KERNEL_API_PATH = JUPYTER_SERVER_PATH + '/api/kernels'
9 | JUPYTER_DEFAULT_PATH = 'work'
10 |
11 | class ProductionConfig(Config):
12 | pass
13 |
14 | class DevelopmentConfig(Config):
15 | DEBUG = True
16 | SQLALCHEMY_DATABASE_URI = 'postgresql://server:password-server@localhost:5432/server_db'
17 | JUPYTER_SERVER_PATH = 'http://localhost:8888'
18 | JUPYTER_CONTENT_API_PATH = JUPYTER_SERVER_PATH + '/api/contents'
19 | JUPYTER_SESSION_API_PATH = JUPYTER_SERVER_PATH + '/api/sessions'
20 | JUPYTER_KERNEL_API_PATH = JUPYTER_SERVER_PATH + '/api/kernels'
21 | JUPYTER_DEFAULT_PATH = 'work'
22 |
23 | class TestingConfig(Config):
24 | TESTING = True
25 | SQLALCHEMY_DATABASE_URI = 'postgresql://server:password-server@postgres:5432/server_db'
26 | JUPYTER_SERVER_PATH = 'http://notebook:8888'
27 | JUPYTER_CONTENT_API_PATH = JUPYTER_SERVER_PATH + '/api/contents'
28 | JUPYTER_SESSION_API_PATH = JUPYTER_SERVER_PATH + '/api/sessions'
29 | JUPYTER_KERNEL_API_PATH = JUPYTER_SERVER_PATH + '/api/kernels'
30 | JUPYTER_DEFAULT_PATH = 'work'
31 |
32 | class IntegrationTestingConfig(Config):
33 | TESTING = True
34 | SQLALCHEMY_DATABASE_URI = 'postgresql://server:password-server@localhost:5432/server_db'
35 | JUPYTER_SERVER_PATH = 'http://localhost:8888'
36 | JUPYTER_CONTENT_API_PATH = JUPYTER_SERVER_PATH + '/api/contents'
37 | JUPYTER_SESSION_API_PATH = JUPYTER_SERVER_PATH + '/api/sessions'
38 | JUPYTER_KERNEL_API_PATH = JUPYTER_SERVER_PATH + '/api/kernels'
39 | JUPYTER_DEFAULT_PATH = 'work'
40 |
--------------------------------------------------------------------------------
/webapp/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 | You need to enable JavaScript to run this app.
32 |
33 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/webapp/src/models/SessionModel.js:
--------------------------------------------------------------------------------
1 | class SessionModel {
2 | constructor() {
3 | }
4 |
5 | static async getSession(notebookPath = '') {
6 | try {
7 | const response = await fetch("http://localhost:5002/session/" + notebookPath, {
8 | method: 'GET',
9 | headers: {
10 | 'Content-Type': 'application/json',
11 | }
12 | });
13 |
14 | if (response.status === 404) {
15 | console.log('Session not found');
16 | return null;
17 | }
18 |
19 | const session = await response.json();
20 | console.log('Session:', session);
21 | // The kernel ID is in the 'id' property of the 'kernel' object
22 | const kernelId = session.kernel.id;
23 |
24 | return kernelId;
25 | } catch (error) {
26 | console.error('Failed to get session:', error);
27 | return null;
28 | }
29 | };
30 |
31 | static async createSession(notebookPath = '') {
32 | try {
33 | const response = await fetch("http://localhost:5002/session", {
34 | method: 'POST',
35 | headers: {
36 | 'Content-Type': 'application/json',
37 | },
38 | body: JSON.stringify({
39 | 'notebookPath': notebookPath
40 | })
41 | });
42 |
43 | if (!response.ok) {
44 | throw new Error(`HTTP error! status: ${response.status}`);
45 | }
46 |
47 | // The response will contain the session data
48 | const session = await response.json();
49 | console.log('Session created:', session);
50 | // The kernel ID is in the 'id' property of the 'kernel' object
51 | const kernelId = session.kernel.id;
52 |
53 | // Return the kernal ID
54 | return kernelId;
55 | } catch (error) {
56 | console.error('Failed to create session:', error);
57 | }
58 | };
59 |
60 | }
61 |
62 | export default SessionModel;
--------------------------------------------------------------------------------
/webapp/src/components/notebook/content/cell/result/CodeResult.js:
--------------------------------------------------------------------------------
1 | import { Card, Typography } from '@mui/material';
2 | import ReactMarkdown from 'react-markdown';
3 | import gfm from 'remark-gfm';
4 | import StringUtils from '../../../../../utils/StringUtils';
5 |
6 |
7 | function CodeResult(index, output) {
8 | const isTable = StringUtils.isJupyterTable(output.text);
9 |
10 | return (
11 |
23 |
39 | {
40 | isTable ? (
41 | ,
46 | th: ({node, ...props}) => ,
47 | td: ({node, ...props}) => ,
48 | }} />
49 | ) : (
50 | output.text
51 | )
52 | }
53 |
54 |
55 | );
56 | }
57 |
58 | export default CodeResult;
--------------------------------------------------------------------------------
/server/tests/models/test_spark_app_model.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from flask_cors import CORS
3 | from run import create_app
4 | from database import db
5 | from app.models.spark_app import SparkAppModel
6 | from app.models.notebook import NotebookModel
7 | from app.models.user import UserModel
8 | import datetime
9 |
10 | class SparkAppModelTestCase(unittest.TestCase):
11 |
12 | def setUp(self):
13 | self.app = create_app()
14 | self.client = self.app.test_client()
15 | with self.app.app_context():
16 | db.create_all()
17 |
18 | def tearDown(self):
19 | with self.app.app_context():
20 | db.session.remove()
21 | db.drop_all()
22 |
23 | def test_spark_app_model(self):
24 | with self.app.app_context():
25 | # Create user first
26 | user = UserModel(name='testuser', email='testuser@example.com')
27 | password = 'test_password'
28 | user.set_password(password)
29 | db.session.add(user)
30 | db.session.commit()
31 |
32 | # Create notebook
33 | notebook = NotebookModel(name='Test Notebook', path='Test Path', user_id=user.id)
34 | db.session.add(notebook)
35 | db.session.commit()
36 |
37 | spark_app = SparkAppModel(
38 | spark_app_id='spark_app0000',
39 | notebook_id=notebook.id,
40 | user_id=user.id,
41 | created_at='2021-01-01 00:00:00')
42 | db.session.add(spark_app)
43 | db.session.commit()
44 |
45 | spark_app_dict = spark_app.to_dict()
46 | self.assertEqual(spark_app_dict['spark_app_id'], 'spark_app0000')
47 | self.assertEqual(spark_app_dict['notebook_id'], notebook.id)
48 | self.assertEqual(spark_app_dict['user_id'], user.id)
49 | self.assertEqual(spark_app_dict['status'], None)
50 | self.assertEqual(spark_app_dict['created_at'], '2021-01-01 00:00:00')
--------------------------------------------------------------------------------
/webapp/src/models/SparkAppConfigModel.js:
--------------------------------------------------------------------------------
1 | import config from '../config';
2 |
3 | class SparkAppConfigModel {
4 | constructor() {
5 | }
6 |
7 | static async getSparkAppConfigByNotebookPath(notebookPath) {
8 | const response = await fetch(`${config.serverBaseUrl}/spark_app/${notebookPath}/config`);
9 | if (!response.ok) {
10 | throw new Error('Failed to fetch Spark app config');
11 | } else {
12 | const data = await response.json();
13 | console.log('Spark app config:', data);
14 |
15 | const data_transformed = {};
16 |
17 | const driverMemory = data['spark.driver.memory'];
18 | const driverMemoryUnit = driverMemory.slice(-1);
19 | const driverValue = driverMemory.slice(0, -1);
20 | data_transformed.driver_memory = driverValue;
21 | data_transformed.driver_memory_unit = driverMemoryUnit;
22 |
23 | const driverCores = data['spark.driver.cores'];
24 | data_transformed.driver_cores = driverCores;
25 |
26 | const executorMemory = data['spark.executor.memory'];
27 | const memoryUnit = executorMemory.slice(-1);
28 | const memoryValue = executorMemory.slice(0, -1);
29 | data_transformed.executor_memory = memoryValue;
30 | data_transformed.executor_memory_unit = memoryUnit;
31 |
32 | const executorCores = data['spark.executor.cores'];
33 | data_transformed.executor_cores = executorCores;
34 |
35 | const executorInstances = data['spark.executor.instances'];
36 | data_transformed.executor_instances = executorInstances;
37 |
38 | return data_transformed;
39 | }
40 | }
41 |
42 | static async updateSparkAppConfig(notebookPath, sparkAppConfig) {
43 | const response = await fetch(`${config.serverBaseUrl}/spark_app/${notebookPath}/config`, {
44 | method: 'POST',
45 | headers: {
46 | 'Content-Type': 'application/json',
47 | },
48 | body: JSON.stringify(sparkAppConfig),
49 | });
50 |
51 | if (!response.ok) {
52 | throw new Error('Failed to update Spark app config');
53 | }
54 | }
55 | }
56 |
57 | export default SparkAppConfigModel;
--------------------------------------------------------------------------------
/webapp/src/components/sidebar/workspace/header/CreateFolderDialog.js:
--------------------------------------------------------------------------------
1 | import { Button, Dialog, DialogActions, TextField, DialogContent, DialogTitle } from '@mui/material';
2 |
3 |
4 | const CreateFolderDialog = ({
5 | createFolderDialogOpen,
6 | setCreateFolderDialogOpen,
7 | folderName,
8 | setFolderName,
9 | handleCreateClose,
10 | handleCreateFolder
11 | }) => {
12 |
13 | return (
14 |
23 | Create Folder
24 |
25 | setFolderName(e.target.value)}
46 | />
47 |
48 |
49 | {
52 | setCreateFolderDialogOpen(false);
53 | handleCreateClose();}}>Cancel
54 | {
57 | handleCreateFolder();
58 | }}>Create
59 |
60 |
61 | );
62 | }
63 |
64 | export default CreateFolderDialog;
--------------------------------------------------------------------------------
/webapp/src/components/sidebar/workspace/header/CreateNotebookDialog.js:
--------------------------------------------------------------------------------
1 | import { Button, Dialog, DialogActions, TextField, DialogContent, DialogTitle } from '@mui/material';
2 |
3 |
4 | const CreateNotebookDialog = ({
5 | createNotebookDialogOpen,
6 | setCreateNotebookDialogOpen,
7 | notebookName,
8 | setNotebookName,
9 | handleCreateClose,
10 | handleCreateNotebook
11 | }) => {
12 |
13 | return (
14 |
23 | Create Notebook
24 |
25 | setNotebookName(e.target.value)}
46 | />
47 |
48 |
49 | {
52 | setCreateNotebookDialogOpen(false);
53 | handleCreateClose();}}>Cancel
54 | {
57 | handleCreateNotebook();
58 | }}>Create
59 |
60 |
61 | );
62 | }
63 |
64 | export default CreateNotebookDialog;
--------------------------------------------------------------------------------
/webapp/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "webapp",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@coreui/icons": "^3.0.1",
7 | "@coreui/icons-react": "^2.2.1",
8 | "@coreui/react": "^5.1.0",
9 | "@datalayer/icons-react": "^0.3.0",
10 | "@emotion/react": "^11.11.4",
11 | "@emotion/styled": "^11.11.5",
12 | "@jupyter/web-components": "^0.16.1",
13 | "@jupyterlab/ui-components": "^4.2.2",
14 | "@mui/lab": "^5.0.0-alpha.170",
15 | "@mui/material": "^5.3.0",
16 | "@mui/x-tree-view": "^7.7.0",
17 | "@testing-library/jest-dom": "^5.17.0",
18 | "@testing-library/user-event": "^13.5.0",
19 | "ace-builds": "^1.34.2",
20 | "ansi_up": "^6.0.2",
21 | "ansi-to-html": "^0.7.2",
22 | "brace": "^0.11.1",
23 | "buffer": "^6.0.3",
24 | "crypto-browserify": "^3.12.0",
25 | "path-browserify": "^1.0.1",
26 | "process": "^0.11.10",
27 | "querystring-es3": "^0.2.1",
28 | "react": "^18.3.1",
29 | "react-ace": "^11.0.1",
30 | "react-app-rewired": "^2.2.1",
31 | "react-dom": "^18.3.1",
32 | "react-icons": "^4.3.1",
33 | "react-markdown": "^9.0.1",
34 | "react-router-dom": "^6.23.1",
35 | "react-scripts": "5.0.1",
36 | "remark-gfm": "^4.0.0",
37 | "web-vitals": "^2.1.4"
38 | },
39 | "scripts": {
40 | "start": "react-app-rewired start",
41 | "build": "react-app-rewired build",
42 | "test": "react-app-rewired test",
43 | "eject": "react-scripts eject"
44 | },
45 | "eslintConfig": {
46 | "extends": [
47 | "react-app",
48 | "react-app/jest"
49 | ]
50 | },
51 | "browserslist": {
52 | "production": [
53 | ">0.2%",
54 | "not dead",
55 | "not op_mini all"
56 | ],
57 | "development": [
58 | "last 1 chrome version",
59 | "last 1 firefox version",
60 | "last 1 safari version"
61 | ]
62 | },
63 | "devDependencies": {
64 | "@babel/core": "^7.24.7",
65 | "@babel/preset-env": "^7.24.7",
66 | "@babel/preset-react": "^7.24.7",
67 | "@testing-library/react": "^16.0.0",
68 | "jest": "^27.5.1",
69 | "react-app-rewired": "^2.2.1"
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/server/run.py:
--------------------------------------------------------------------------------
1 | from flask import Flask, Response
2 | from flask_cors import CORS
3 | from database import db
4 | import os
5 | import json
6 | from app.routes.notebook import notebook_blueprint
7 | from app.routes.directory import directory_blueprint
8 | from app.routes.session import session_blueprint
9 | from app.routes.kernel import kernel_blueprint
10 | from app.routes.spark_app import spark_app_blueprint
11 | from app.routes.login import login_blueprint
12 | from flask_jwt_extended import JWTManager
13 | from config import DevelopmentConfig, IntegrationTestingConfig, TestingConfig
14 |
15 | def create_app():
16 | app = Flask(__name__)
17 | if os.environ.get('ENV', 'development') == 'development':
18 | app.config.from_object(DevelopmentConfig)
19 | elif os.environ.get('ENV', 'development') == 'testing':
20 | app.config.from_object(TestingConfig)
21 | elif os.environ.get('ENV', 'development') == 'integration':
22 | app.config.from_object(IntegrationTestingConfig)
23 |
24 | # Set the secret key for JWT
25 | try:
26 | from app_secrets import JWT_SECRET_KEY
27 | except ImportError:
28 | JWT_SECRET_KEY = os.environ.get('JWT_SECRET_KEY', 'default_secret_key')
29 |
30 | app.config['JWT_SECRET_KEY'] = JWT_SECRET_KEY
31 | jwt = JWTManager(app)
32 | @jwt.expired_token_loader
33 | def my_expired_token_callback(jwt_header, jwt_payload):
34 | return Response(
35 | response=json.dumps({'message': 'Token has expired'}),
36 | status=401)
37 |
38 | db.init_app(app)
39 |
40 | allowed_origins = ["http://localhost:5001", "http://localhost:3000"]
41 | CORS(app, resources={
42 | r"/*": {"origins": allowed_origins}
43 | })
44 |
45 | return app
46 |
47 | app = create_app()
48 |
49 | app.register_blueprint(notebook_blueprint)
50 | app.register_blueprint(directory_blueprint)
51 | app.register_blueprint(session_blueprint)
52 | app.register_blueprint(kernel_blueprint)
53 | app.register_blueprint(spark_app_blueprint)
54 | app.register_blueprint(login_blueprint)
55 |
56 |
57 |
58 | if __name__ == '__main__':
59 | app.run(debug=True, host='0.0.0.0', port=5002)
60 |
--------------------------------------------------------------------------------
/webapp/src/components/notebook/content/cell/CellSideButtons.js:
--------------------------------------------------------------------------------
1 | import { MdDeleteOutline, MdArrowDropUp, MdArrowDropDown, MdArrowRight } from "react-icons/md";
2 | import { Box, Select, MenuItem, Typography, Card, CardHeader, CardContent, TextField, IconButton, CircularProgress } from '@mui/material';
3 |
4 |
5 | function CellSideButtons(
6 | notebookState,
7 | index,
8 | handleDeleteCell,
9 | handleMoveCell) {
10 | return (
11 |
12 | handleDeleteCell(index)}
15 | style={{
16 | height: 20,
17 | marginTop: 10,
18 | marginLeft: 0,
19 | marginRight: 0 }}>
20 | {
23 | e.currentTarget.style.color = 'black';
24 | }}
25 | onMouseLeave={(e) => {
26 | e.currentTarget.style.color = 'grey';
27 | }}
28 | style={{
29 | color: 'grey' }}/>
30 |
31 |
32 | {index !== 0 &&
33 | handleMoveCell(index, index-1)}
36 | onMouseEnter={(e) => {
37 | e.currentTarget.style.color = 'black';
38 | }}
39 | onMouseLeave={(e) => {
40 | e.currentTarget.style.color = 'grey';
41 | }}
42 | style={{ marginLeft: 0, marginTop: 2, marginBottom: 2 }}>
43 |
45 | }
46 | {index !== notebookState.content.cells.length - 1 &&
47 | handleMoveCell(index, index+1)}
50 | onMouseEnter={(e) => {
51 | e.currentTarget.style.color = 'black';
52 | }}
53 | onMouseLeave={(e) => {
54 | e.currentTarget.style.color = 'grey';
55 | }}
56 | style={{ marginLeft: 0, marginTop: 2, marginBottom: 2 }}>
57 |
59 | }
60 |
61 | );
62 | }
63 |
64 | export default CellSideButtons;
--------------------------------------------------------------------------------
/bin/connect_gcp.sh:
--------------------------------------------------------------------------------
1 | # Authenticate with Google Cloud
2 | echo "Authenticating with Google Cloud..."
3 |
4 | # Check if already logged in
5 | CURRENT_ACCOUNT=$(gcloud config get-value account)
6 |
7 | if [ -z "$CURRENT_ACCOUNT" ]; then
8 | echo "No active account, authenticating with Google Cloud..."
9 | gcloud auth login
10 | else
11 | echo "Already logged in as $CURRENT_ACCOUNT"
12 | fi
13 |
14 | gcloud config set project $GCP_PROJECT_ID
15 |
16 | # Check if the cluster already exists
17 | if gcloud container clusters describe $GKE_CLUSTER_NAME --zone $GKE_CLUSTER_ZONE --project $GKE_PROJECT_ID > /dev/null 2>&1; then
18 | echo "Cluster $GKE_CLUSTER_NAME already exists."
19 | else
20 | echo "Creating cluster $GKE_CLUSTER_NAME..."
21 | gcloud container clusters create $GKE_CLUSTER_NAME \
22 | --zone $GKE_CLUSTER_ZONE \
23 | --project $GCP_PROJECT_ID \
24 | --num-nodes $GKE_CLUSTER_NUM_NODES \
25 | --machine-type $GKE_CLUSTER_MACHINE_TYPE \
26 | --workload-pool=$GCP_PROJECT_ID.svc.id.goog
27 | fi
28 |
29 | # Connect to the cluster
30 | echo "Getting credentials for cluster $GKE_CLUSTER_NAME..."
31 | gcloud container clusters get-credentials $GKE_CLUSTER_NAME --zone $GKE_CLUSTER_ZONE --project $GCP_PROJECT_ID
32 |
33 | # Now kubectl is configured to use your GKE cluster
34 | echo "Connected to GKE cluster: $GKE_CLUSTER_NAME"
35 |
36 | # Get GKE endpoint info
37 | export KUBERNETES_API_SERVER_HOST=$(gcloud container clusters describe $GKE_CLUSTER_NAME --zone $GKE_CLUSTER_ZONE --format='value(endpoint)')
38 | echo "Kubernetes API server host: $KUBERNETES_API_SERVER_HOST"
39 |
40 | # Check if the bucket already exists
41 | if gsutil ls -b "gs://$BUCKET_NAME" >/dev/null 2>&1; then
42 | echo "Bucket gs://$BUCKET_NAME already exists."
43 | else
44 | echo "Bucket gs://$BUCKET_NAME does not exist. Creating the bucket."
45 | gsutil mb -l $BUCKET_LOCATION -c $BUCKET_STORAGE_CLASS "gs://$BUCKET_NAME"
46 | fi
47 |
48 | # Create event-logs folder
49 | if gsutil ls -b "gs://$BUCKET_NAME/event-logs" >/dev/null 2>&1; then
50 | echo "Bucket gs://$BUCKET_NAME/event-logs already exists."
51 | else
52 | echo "Folder gs://$BUCKET_NAME/event-logs does not exist. Creating the folder."
53 | gsutil cp -r ./resources/event-logs gs://$BUCKET_NAME/event-logs
54 | fi
--------------------------------------------------------------------------------
/webapp/src/components/notebook/content/cell/result/TextResult.js:
--------------------------------------------------------------------------------
1 | import { Typography, Card } from '@mui/material';
2 |
3 |
4 | function TextResult(output) {
5 | return (
6 |
22 | { output.data['text/html'] ?
23 |
35 |
41 |
42 | :
43 |
55 |
71 | {output.data['text/plain']}
72 |
73 |
74 | }
75 |
76 | );
77 | }
78 |
79 | export default TextResult;
--------------------------------------------------------------------------------
/webapp/src/assets/spark-start.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/webapp/src/components/sidebar/create/CreateSidebar.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Typography, Drawer, List, ListItem, ListItemIcon, ListItemText } from '@mui/material';
3 | import { CgNotes } from "react-icons/cg";
4 |
5 | function CreateSidebar({
6 | itemHeight,
7 | openCreateDrawer,
8 | closeCreateDrawer,
9 | handleToggleCreateDrawer,
10 | createButtonRef,
11 | onNewNotebookClick }) {
12 | return (
13 |
37 |
39 | {
41 | closeCreateDrawer()
42 | onNewNotebookClick()
43 | }}
44 | sx={{
45 | '&:hover': {
46 | backgroundColor: '#555',
47 | },
48 | '&:hover .MuiTypography-root': {
49 | color: 'white',
50 | },
51 | marginTop: '-5px',
52 | }}>
53 |
54 |
57 |
58 |
59 |
65 | Notebook
66 |
67 |
68 |
69 |
70 |
71 | )
72 | }
73 |
74 | export default CreateSidebar;
--------------------------------------------------------------------------------
/server/app/services/user.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from flask import Response
3 | import requests
4 | import json
5 | from flask import current_app as app
6 | from database import db
7 | from app.models.user import UserModel
8 |
9 | logger = logging.getLogger(__name__)
10 |
11 | class User:
12 |
13 | @staticmethod
14 | def get_mock_user():
15 | mock_user = UserModel.query.filter_by(name='testuser0').first()
16 | if mock_user is None:
17 | mock_user = UserModel(name='testuser0', email='testuser0@example.com')
18 | password = 'test_password'
19 | mock_user.set_password(password)
20 | db.session.add(mock_user)
21 | db.session.commit()
22 |
23 | return mock_user
24 |
25 | @staticmethod
26 | def get_user_by_name(name):
27 | user = UserModel.query.filter_by(name=name).first()
28 | return user
29 |
30 | @staticmethod
31 | def get_user_by_email(email):
32 | user = UserModel.query.filter_by(email=email).first()
33 | return user
34 |
35 | @staticmethod
36 | def create_user(name, email, password):
37 | user = UserModel(name=name, email=email)
38 | user.set_password(password)
39 | db.session.add(user)
40 | db.session.commit()
41 | return user
42 |
43 | @staticmethod
44 | def delete_user(name):
45 | user = UserModel.query.filter_by(name=name).first()
46 | db.session.delete(user)
47 | db.session.commit()
48 | return Response(
49 | response=json.dumps({'message': 'User deleted successfully'}),
50 | status=200
51 | )
52 |
53 | @staticmethod
54 | def update_user(name, email, password):
55 | user = UserModel.query.filter_by(name=name).first()
56 | user.email = email
57 | user.set_password(password)
58 | db.session.commit()
59 | return Response(
60 | response=json.dumps({'message': 'User updated successfully'}),
61 | status=200
62 | )
63 |
64 | @staticmethod
65 | def get_all_users():
66 | users = UserModel.query.all()
67 | return users
68 |
69 | @staticmethod
70 | def validate_user_by_name(name, password):
71 | user = UserModel.query.filter_by(name=name).first()
72 | if user is None:
73 | return False
74 | return user.check_password(password)
75 |
76 | @staticmethod
77 | def validate_user_by_email(email, password):
78 | user = UserModel.query.filter_by(email=email).first()
79 | if user is None:
80 | return False
81 | return user.check_password(password)
82 |
--------------------------------------------------------------------------------
/examples/user_0@gmail.com/demo.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "isExecuted": true,
6 | "metadata": {},
7 | "source": [
8 | "# Demo Notebook\n",
9 | "\n",
10 | "- This is just a demo notebook\n",
11 | "- For testing only"
12 | ]
13 | },
14 | {
15 | "cell_type": "code",
16 | "isExecuted": false,
17 | "lastExecutionResult": "success",
18 | "lastExecutionTime": "2024-12-10 10:26:03",
19 | "metadata": {},
20 | "outputs": [
21 | {
22 | "data": {
23 | "text/html": [
24 | "\n",
25 | " \n",
26 | "
Spark Session Information \n",
27 | "
Config: {'spark.driver.memory': '1g', 'spark.driver.cores': 1, 'spark.executor.memory': '1g', 'spark.executor.cores': 1, 'spark.executor.instances': 1, 'spark.dynamicAllocation.enabled': False}
\n",
28 | "
Application ID: app-20241210080310-0003
\n",
29 | "
Spark UI: http://localhost:18080/history/app-20241210080310-0003
\n",
30 | "
\n",
31 | " "
32 | ],
33 | "text/plain": [
34 | "Custom Spark Session (App ID: app-20241210080310-0003) - UI: http://0edb0a63b2fb:4040"
35 | ]
36 | },
37 | "execution_count": 11,
38 | "metadata": {},
39 | "output_type": "execute_result"
40 | }
41 | ],
42 | "source": [
43 | "spark = create_spark(\"work/user_0@gmail.com/demo.ipynb\")\n",
44 | "spark"
45 | ]
46 | },
47 | {
48 | "cell_type": "code",
49 | "execution_count": null,
50 | "isExecuted": true,
51 | "lastExecutionResult": "success",
52 | "lastExecutionTime": "2024-12-10 12:27:14",
53 | "metadata": {},
54 | "outputs": [],
55 | "source": [
56 | "spark.stop()"
57 | ]
58 | }
59 | ],
60 | "metadata": {
61 | "kernelspec": {
62 | "display_name": "Python 3 (ipykernel)",
63 | "language": "python",
64 | "name": "python3"
65 | },
66 | "language_info": {
67 | "codemirror_mode": {
68 | "name": "ipython",
69 | "version": 3
70 | },
71 | "file_extension": ".py",
72 | "mimetype": "text/x-python",
73 | "name": "python",
74 | "nbconvert_exporter": "python",
75 | "pygments_lexer": "ipython3",
76 | "version": "3.11.6"
77 | },
78 | "uuid": "647a82de-693a-48f0-ae3a-64a771d83da5"
79 | },
80 | "nbformat": 4,
81 | "nbformat_minor": 4
82 | }
83 |
--------------------------------------------------------------------------------
/docker/notebook/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM jupyter/all-spark-notebook:spark-3.5.0
2 |
3 | USER root
4 |
5 | # Install necessary packages for Google Cloud SDK
6 | RUN apt-get update -y && \
7 | apt-get install -y curl gcc python3-dev apt-transport-https lsb-release gnupg && \
8 | rm -rf /var/lib/apt/lists/*
9 |
10 | # Install pyspark
11 | RUN pip install pyspark
12 |
13 | # Add Google Cloud SDK to the sources list
14 | RUN echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] http://packages.cloud.google.com/apt cloud-sdk main" | tee -a /etc/apt/sources.list.d/google-cloud-sdk.list
15 |
16 | # Import Google's public key
17 | RUN curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key --keyring /usr/share/keyrings/cloud.google.gpg add -
18 |
19 | # Install Google Cloud SDK
20 | RUN apt-get update -y && \
21 | apt-get install -y google-cloud-sdk && \
22 | rm -rf /var/lib/apt/lists/*
23 |
24 | # Install the Google Cloud Storage Python library
25 | RUN pip install --upgrade google-cloud-storage \
26 | kubernetes
27 |
28 | # GCS connector
29 | RUN wget -P /usr/local/spark/jars/ \
30 | https://repo1.maven.org/maven2/com/google/cloud/bigdataoss/gcs-connector/hadoop3-2.2.6/gcs-connector-hadoop3-2.2.6-shaded.jar
31 |
32 | # Create a new directory for the IPython profile
33 | RUN mkdir -p /home/jovyan/.custom_ipython_profile/profile_default/startup/ && \
34 | chown -R jovyan:users /home/jovyan/.custom_ipython_profile && \
35 | chmod -R 775 /home/jovyan/.custom_ipython_profile
36 |
37 | # Copy the custom IPython profile to the new directory
38 | COPY startup.py /home/jovyan/.custom_ipython_profile/profile_default/startup/
39 |
40 | # Copy the save hook script and configuration file into the container
41 | COPY gcs_save_hook.py /home/jovyan/.jupyter/
42 | COPY jupyter_notebook_config.py /home/jovyan/.jupyter/
43 |
44 | # Switch back to the jovyan user
45 | USER jovyan
46 |
47 | # Set environment variable to use the custom IPython profile directory
48 | ENV IPYTHONDIR=/home/jovyan/.custom_ipython_profile
49 |
50 | # Set JUPYTER_CONFIG_DIR to point to the directory with the config file
51 | ENV JUPYTER_CONFIG_DIR /home/jovyan/.jupyter/
52 |
53 | # Add the JUPYTER_CONFIG_DIR to the PYTHONPATH
54 | ENV PYTHONPATH "${PYTHONPATH}:${JUPYTER_CONFIG_DIR}"
55 |
56 | ENV HOME_DIR="/home/jovyan"
57 | ENV BUCKET_NAME="data-platform-bucket-20231126"
58 | ENV NAMESPACE="spark-dev"
59 | ENV SERVICE_ACCOUNT="spark"
60 | ENV EXECUTOR_IMAGE="wenyixu101/spark:3.5.0-python3.11"
61 | ENV WEBUI_SERVICE_NAME="notebook-spark-ui"
62 |
63 | CMD ["jupyter", "notebook", "--ip='0.0.0.0'", "--port=8888", "--no-browser", "--allow-root", "--NotebookApp.token=''", "--NotebookApp.password=''"]
64 |
65 |
66 |
--------------------------------------------------------------------------------
/webapp/src/components/notebook/content/Runs.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import SparkModel from '../../../models/SparkModel';
3 | import NotebookModel from '../../../models/NotebookModel';
4 | import { Link, Button, Box, Typography, Card, CardHeader, CardContent, List, ListItem, ListItemText } from '@mui/material';
5 | import config from '../../../config';
6 |
7 |
8 | function Runs({
9 | notebook,
10 | contentType
11 | }) {
12 | const [sparkApps, setSparkApps] = useState([]);
13 |
14 | useEffect(() => {
15 | const fetchSparkApps = async () => {
16 | const data = await NotebookModel.getSparkApps(notebook.path);
17 | console.log('data:', data);
18 | setSparkApps(data);
19 | };
20 |
21 | fetchSparkApps();
22 | }, [contentType, notebook]);
23 |
24 | return (
25 |
30 |
35 |
41 | Associated Spark Applications
42 |
43 | }
44 | sx={{
45 | backgroundColor: '#f5f5f5',
46 | borderBottom: 1,
47 | borderBottomColor: '#f5f5f5',
48 | }}/>
49 |
50 | {sparkApps ? (
51 |
52 | {sparkApps.map((app, index) => (
53 |
54 | { window.open(`${config.sparkUiBaseUrl}/history/${app.spark_app_id}/jobs/`) }}
56 | sx={{ textTransform: 'none' }}>
57 |
58 |
59 |
60 |
61 |
67 | Created at: {app.created_at}
68 |
69 |
70 | ))}
71 |
72 | )
73 | : ('No Spark Applications associated with this notebook.')}
74 |
75 |
76 |
77 | );
78 | }
79 |
80 | export default Runs;
81 |
--------------------------------------------------------------------------------
/server/app/services/session.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from flask import Response
3 | import requests
4 | import json
5 | from flask import current_app as app
6 |
7 |
8 | logger = logging.getLogger(__name__)
9 |
10 |
11 | class Session:
12 |
13 | @staticmethod
14 | def get_all_sessions():
15 | try:
16 | response = requests.get(app.config['JUPYTER_SESSION_API_PATH'])
17 | except Exception as e:
18 | logger.error(f"Met exception getting all sessions: {e}")
19 | return Response(
20 | response=json.dumps({'message': 'Error getting all sessions from Jupyter Server: ' + str(e)}),
21 | status=404)
22 |
23 | return Response(
24 | response=response,
25 | status=200,
26 | mimetype='application/json'
27 | )
28 |
29 | @staticmethod
30 | def get_session_by_path(notebook_path: str) -> None:
31 | logger.info(f"Getting session for {notebook_path}")
32 |
33 | all_sessions = Session.get_all_sessions()
34 | if all_sessions.status_code != 200:
35 | return Response(
36 | response=json.dumps({'message': 'Error getting all sessions'}),
37 | status=404)
38 |
39 | sessions = json.loads(all_sessions.data.decode('utf-8'))
40 | session = [x for x in sessions if x["path"] == notebook_path]
41 |
42 | if len(session) == 0:
43 | return Response(
44 | response=json.dumps({'message': 'Session not found'}),
45 | status=404)
46 | elif len(session) > 1:
47 | return Response(
48 | response=json.dumps({'message': 'Multiple sessions found'}),
49 | status=404)
50 | else:
51 | return Response(
52 | response=json.dumps(session[0]),
53 | status=200,
54 | mimetype='application/json'
55 | )
56 |
57 |
58 | @staticmethod
59 | def create_session(notebook_path: str) -> None:
60 | logger.info(f"Creating session for {notebook_path}")
61 | jupyter_api_path = app.config['JUPYTER_SESSION_API_PATH']
62 |
63 | data = {
64 | "notebook": {
65 | "path": notebook_path
66 | },
67 | "kernel": {
68 | "id": None,
69 | "name": "python3"
70 | }
71 | }
72 |
73 | try:
74 | response = requests.post(
75 | jupyter_api_path,
76 | json=data
77 | )
78 | except Exception as e:
79 | logger.error(f"Met exception creating session: {e}")
80 | return Response(
81 | response=json.dumps({'message': 'Error creating session in Jupyter Server: ' + str(e)}),
82 | status=404)
83 |
84 | return Response(
85 | response=response.content,
86 | status=200,
87 | mimetype='application/json'
88 | )
89 |
90 |
91 |
92 |
93 |
--------------------------------------------------------------------------------
/webapp/src/components/sidebar/workspace/item/RenameDialog.js:
--------------------------------------------------------------------------------
1 | import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, TextField } from '@mui/material';
2 | import { useState } from 'react';
3 | import NotebookModel from '../../../../models/NotebookModel';
4 |
5 | const RenameDialog = ({
6 | file,
7 | renameDialogOpen,
8 | setRenameDialogOpen,
9 | handleMoreClose,
10 | handleRename
11 | }) => {
12 |
13 | const [newName, setNewName] = useState('');
14 |
15 | const handleInputChange = (event) => {
16 | setNewName(event.target.value);
17 | };
18 |
19 | return (
20 |
28 |
29 | Rename
30 |
31 |
32 |
36 | Please enter the new name for: {NotebookModel.getNameWithoutExtension(file.name)}
37 |
38 |
60 |
61 |
62 | {
65 | setRenameDialogOpen(false)}}>
66 | Cancel
67 |
68 | {
71 | setRenameDialogOpen(false);
72 | handleMoreClose();
73 | file.type === 'directory' ?
74 | handleRename(file, newName) :
75 | handleRename(file, NotebookModel.getNameWithExtension(newName));
76 | }}>
77 | Confirm
78 |
79 |
80 |
81 | );
82 | };
83 |
84 | export default RenameDialog;
--------------------------------------------------------------------------------
/webapp/src/components/notebook/content/cell/header/CellHeader.js:
--------------------------------------------------------------------------------
1 | import { Box, Typography, CardHeader, CircularProgress } from '@mui/material';
2 | import { CgCheck, CgDanger } from "react-icons/cg";
3 | import { CellStatus } from '../CellStatus';
4 | import RunButton from './RunButton';
5 | import TypeSelect from './TypeSelect';
6 | import MoreButton from './MoreButton';
7 | import { CellExecuteResultType } from '../CellExecuteResultType';
8 |
9 |
10 | function CellHeader({
11 | cell,
12 | index,
13 | cellStatus,
14 | handleRunCell,
15 | handleChangeCellType,
16 | handleCopyCell,
17 | handelRunAllAboveCells
18 | }) {
19 | return (
20 |
22 |
23 | {
24 | cellStatus === CellStatus.BUSY ||
25 | cellStatus === CellStatus.INITIALIZING ||
26 | cellStatus === CellStatus.WAITING ?
27 | :
28 |
32 | }
33 |
34 | {
35 | (cellStatus === CellStatus.BUSY ||
36 | cellStatus === CellStatus.INITIALIZING ||
37 | cellStatus === CellStatus.WAITING) ?
38 |
42 | {cellStatus}
43 | :
44 | cell.lastExecutionResult === null ? null :
45 | (cell.lastExecutionResult === CellExecuteResultType.SUCCESS ?
46 | :
47 | (cell.lastExecutionResult === CellExecuteResultType.ERROR ?
48 | : null))
49 | }
50 |
51 | { (cellStatus === CellStatus.BUSY ||
52 | cellStatus === CellStatus.INITIALIZING ||
53 | cellStatus === CellStatus.WAITING) ? null :
54 |
58 | {cell.lastExecutionTime}
59 |
60 | }
61 |
62 |
63 |
64 |
68 |
73 |
74 | }
75 | sx={{ bgcolor: '#f2f2f2', height: '5px' }}/>
76 | )
77 | }
78 |
79 | export default CellHeader;
--------------------------------------------------------------------------------
/webapp/src/components/notebook/header/move/MoveDialog.js:
--------------------------------------------------------------------------------
1 | import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from '@mui/material';
2 | import { useEffect, useState } from 'react';
3 | import { TreeView, TreeItem } from '@mui/x-tree-view';
4 | import { SimpleTreeView } from '@mui/x-tree-view/SimpleTreeView';
5 | import NotebookModel from '../../../../models/NotebookModel';
6 | import DirectoryModel from '../../../../models/DirectoryModel';
7 | import config from '../../../../config';
8 |
9 |
10 | const MoveDialog = ({
11 | notebook,
12 | moveDialogOpen,
13 | setMoveDialogOpen,
14 | }) => {
15 |
16 | const jupyterBaseUrl= `${config.jupyterBaseUrl}`
17 | const baseUrl = `${jupyterBaseUrl}/api/contents`
18 | const directoryUrl = `/work`
19 |
20 | const [directories, setDirectories] = useState([]);
21 | const [destinationDirectory, setDestinationDirectory] = useState('work');
22 |
23 | useEffect(() => {
24 | const fetchDirectories = async () => {
25 | const items = await DirectoryModel.getSubDirectories(directoryUrl);
26 | setDirectories(items);
27 | };
28 |
29 | fetchDirectories();
30 | }, []);
31 |
32 | const renderTree = (node) => (
33 | {
35 | setDestinationDirectory(node.path)
36 | }}
37 | itemId={node.path}
38 | label={node.name}
39 | key={node.id}
40 | disabled={node.type !== 'directory'}>
41 | {
42 | node.type === 'directory' ?
43 | node.children.map((child) => renderTree(child))
44 | : null
45 | }
46 |
47 | );
48 |
49 |
50 |
51 | return (
52 |
62 |
63 | Please choose the destination folder
64 |
65 |
66 |
70 | {directories.map((directory) => (
71 | renderTree(directory)
72 | ))}
73 |
74 |
75 |
76 | {
79 | setMoveDialogOpen(false)}}>
80 | Cancel
81 |
82 | {
85 | NotebookModel.moveNotebook(notebook.path, destinationDirectory + '/' + notebook.name);
86 | setMoveDialogOpen(false);
87 | }}>
88 | Move
89 |
90 |
91 |
92 | );
93 | }
94 |
95 | export default MoveDialog;
--------------------------------------------------------------------------------
/server/tests/services/test_kernel_service.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from flask_cors import CORS
3 | from flask import g
4 | from run import create_app
5 | from database import db
6 | from app.models.user import UserModel
7 | from app.services.kernel import Kernel
8 | from app.services.notebook import Notebook
9 | from app.services.session import Session
10 | import json
11 |
12 | class KernelServiceTestCase(unittest.TestCase):
13 |
14 | def setUp(self):
15 | self.app = create_app()
16 | self.client = self.app.test_client()
17 | with self.app.app_context():
18 | db.create_all()
19 |
20 | def tearDown(self):
21 | with self.app.app_context():
22 | db.session.remove()
23 | db.drop_all()
24 |
25 | def test_get_kernel_by_id(self):
26 | with self.app.app_context():
27 | # Get non-exist kernel
28 | response_0 = Kernel.get_kernel_by_id('kernel_id')
29 | self.assertEqual(response_0.status_code, 404)
30 |
31 | # Create User
32 | user = UserModel(name='testuser', email='testuser@example.com')
33 | password = 'test_password'
34 | user.set_password(password)
35 | g.user = user
36 |
37 | # Create Notebook
38 | response_1 = Notebook.create_notebook_with_init_cells(notebook_name='Notebook_1.ipynb', notebook_path='')
39 | self.assertEqual(response_1.status_code, 200)
40 |
41 | notebook_1 = json.loads(response_1.data.decode('utf-8'))
42 | notebook_path_1 = notebook_1['path']
43 |
44 | # Create Session
45 | response_2 = Session.create_session(notebook_path_1)
46 | self.assertEqual(response_2.status_code, 200)
47 | session = json.loads(response_2.data.decode('utf-8'))
48 | kernelId = session['kernel']['id']
49 |
50 | # Get kernel
51 | response_3 = Kernel.get_kernel_by_id(kernelId)
52 | self.assertEqual(response_3.status_code, 200)
53 |
54 | def test_restart_kernel(self):
55 | with self.app.app_context():
56 | user = UserModel(name='testuser0', email='testuser0@example.com')
57 | password = 'test_password'
58 | user.set_password(password)
59 | db.session.add(user)
60 | db.session.commit()
61 | g.user = user
62 |
63 | # Restart non-exist kernel
64 | response_0 = Kernel.restart_kernel('kernel_id')
65 | self.assertEqual(response_0.status_code, 404)
66 |
67 | # Create Notebook
68 | response_1 = Notebook.create_notebook_with_init_cells(notebook_name='Notebook_1.ipynb', notebook_path='')
69 | self.assertEqual(response_1.status_code, 200)
70 |
71 | notebook_1 = json.loads(response_1.data.decode('utf-8'))
72 | notebook_path_1 = notebook_1['path']
73 |
74 | # Create Session
75 | response_2 = Session.create_session(notebook_path_1)
76 | self.assertEqual(response_2.status_code, 200)
77 | session = json.loads(response_2.data.decode('utf-8'))
78 | kernelId = session['kernel']['id']
79 |
80 | # Restart kernel
81 | response_3 = Kernel.restart_kernel(kernelId)
82 | self.assertEqual(response_3.status_code, 200)
83 |
--------------------------------------------------------------------------------
/webapp/src/components/sidebar/workspace/header/CreateButton.js:
--------------------------------------------------------------------------------
1 | import { Button } from '@mui/material';
2 | import Menu from '@mui/material/Menu';
3 | import MenuItem from '@mui/material/MenuItem';
4 | import { CgChevronDown } from "react-icons/cg";
5 | import CreateNotebookDialog from './CreateNotebookDialog';
6 | import CreateFolderDialog from './CreateFolderDialog';
7 |
8 |
9 | const CreateButton = ({
10 | setRefreshKey,
11 | anchorEl,
12 | handleCreateClick,
13 | handleCreateClose,
14 | createNotebookDialogOpen,
15 | setCreateNotebookDialogOpen,
16 | notebookName,
17 | setNotebookName,
18 | handleCreateNotebook,
19 | createFolderDialogOpen,
20 | setCreateFolderDialogOpen,
21 | folderName,
22 | setFolderName,
23 | handleCreateFolder,
24 | }) => {
25 | return (
26 |
27 | }
29 | onClick={handleCreateClick}
30 | style={{
31 | fontFamily: 'Roboto',
32 | color: 'lightgrey',
33 | marginRight: '20px',
34 | marginTop: '18px',
35 | border: '1px solid grey',
36 | width: '100px',
37 | height: '30px'
38 | }}>Create
39 |
40 |
41 |
55 |
56 | {/* Create Folder */}
57 | {
59 | setCreateFolderDialogOpen(true);
60 | setRefreshKey(oldKey => oldKey + 1);
61 | }}
62 | style={{ color: 'lightgrey' }}>Folder
63 |
64 |
72 |
73 | {/* Create Notebook */}
74 | {
76 | setCreateNotebookDialogOpen(true);
77 | setRefreshKey(oldKey => oldKey + 1);
78 | }}
79 | style={{ color: 'lightgrey' }}>Notebook
80 |
81 |
89 |
90 |
91 | );
92 | }
93 |
94 | export default CreateButton;
--------------------------------------------------------------------------------
/server/tests/routes/test_spark_app_route.py:
--------------------------------------------------------------------------------
1 | # import unittest
2 | # import json
3 | # from flask_cors import CORS
4 | # from flask import g
5 | # from database import db
6 | # from run import create_app
7 | # from app.routes.spark_app import spark_app_blueprint
8 | # from app.routes.login import login_blueprint
9 | # from app.services.directory import Directory
10 | # from app.models.user import UserModel
11 | # from app.services.user import User
12 | # from app.models.spark_app import SparkAppModel
13 | # from app.models.notebook import NotebookModel
14 |
15 | # class SparkAppRouteTestCase(unittest.TestCase):
16 |
17 | # def setUp(self):
18 | # self.app = create_app()
19 | # self.app.register_blueprint(spark_app_blueprint)
20 | # self.app.register_blueprint(login_blueprint)
21 | # self.client = self.app.test_client()
22 | # with self.app.app_context():
23 | # db.create_all()
24 | # user = UserModel(name='test_user', email='test_email')
25 | # user.set_password('test_password')
26 | # db.session.add(user)
27 | # db.session.commit()
28 |
29 | # def tearDown(self):
30 | # with self.app.app_context():
31 | # db.session.remove()
32 | # db.drop_all()
33 |
34 | # def login_and_get_token(self):
35 | # with self.app.app_context():
36 | # response = self.client.post('/login', auth=('test_user', 'test_password'))
37 | # return json.loads(response.data)['access_token']
38 |
39 | # # def test_create_spark_app(self):
40 | # # with self.app.app_context():
41 | # # # Create Notebook
42 | # # notebook = NotebookModel(name='Test Notebook', path='/path/to/notebook', user_id=1)
43 | # # db.session.add(notebook)
44 | # # db.session.commit()
45 |
46 | # # # Create Spark App
47 | # # spark_app_id = 'app_0001'
48 | # # path = f'/spark-app/app_0001'
49 |
50 | # # # data = {
51 | # # # 'notebookPath': notebook.path
52 | # # # }
53 |
54 | # # # token = self.login_and_get_token()
55 | # # # headers = {
56 | # # # 'Authorization': f'Bearer {token}',
57 | # # # }
58 |
59 | # # response = self.client.post(
60 | # # path,
61 | # # # headers=headers,
62 | # # # json=json.dumps(data),
63 | # # )
64 |
65 | # # print(response.data)
66 | # # # self.assertEqual(response.status_code, 200)
67 | # # # self.assertEqual(json.loads(response.data)['spark_app_id'], spark_app_id)
68 | # # # self.assertEqual(json.loads(response.data)['notebook_id'], notebook.id)
69 | # # # self.assertEqual(json.loads(response.data)['user_id'], notebook.user_id)
70 |
71 | # def test_get_spark_app_config_by_notebook_path(self):
72 | # with self.app.app_context():
73 | # token = self.login_and_get_token()
74 | # headers = {
75 | # 'Authorization': f'Bearer {token}',
76 | # }
77 |
78 | # # response = self.client.get('/spark-app/path_to_notebook/config', headers=headers)
79 | # # print(response.data)
80 |
81 | # response = self.client.get('/spark-app')
82 | # print(response.data)
83 |
--------------------------------------------------------------------------------
/server/tests/models/test_spark_app_config_model.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from flask_cors import CORS
3 | from run import create_app
4 | from database import db
5 | from app.models.spark_app_config import SparkAppConfigModel
6 | from app.models.notebook import NotebookModel
7 | from app.models.user import UserModel
8 |
9 | class SparkAppConfigModelTestCase(unittest.TestCase):
10 |
11 | def setUp(self):
12 | self.app = create_app()
13 | self.client = self.app.test_client()
14 | with self.app.app_context():
15 | db.create_all()
16 |
17 | def tearDown(self):
18 | with self.app.app_context():
19 | db.session.remove()
20 | db.drop_all()
21 |
22 | def test_spark_app_config_model(self):
23 | with self.app.app_context():
24 | # Create user
25 | user = UserModel(name='testuser', email='testuser@example.com')
26 | password = 'test_password'
27 | user.set_password(password)
28 | db.session.add(user)
29 | db.session.commit()
30 |
31 | self.assertEqual(user.id, 1)
32 |
33 | # Create notebook
34 | notebook = NotebookModel(name = 'test_notebook', path='test_notebook', user_id=1)
35 | db.session.add(notebook)
36 | db.session.commit()
37 |
38 | self.assertEqual(notebook.id, 1)
39 |
40 | # Create spark app config
41 | spark_app_config = SparkAppConfigModel(
42 | notebook_id=1,
43 | driver_memory='1g',
44 | driver_memory_overhead='1g',
45 | driver_cores=1,
46 | executor_memory='1g',
47 | executor_memory_overhead='1g',
48 | executor_memory_fraction=1.0,
49 | executor_cores=1,
50 | executor_instances=1,
51 | dynamic_allocation_enabled=True,
52 | executor_instances_min=1,
53 | executor_instances_max=1,
54 | shuffle_service_enabled=True,
55 | executor_idle_timeout=1,
56 | queue='test_queue'
57 | )
58 | db.session.add(spark_app_config)
59 | db.session.commit()
60 |
61 | spark_app_config_dict = spark_app_config.to_dict()
62 | self.assertEqual(spark_app_config_dict['notebook_id'], 1)
63 | self.assertEqual(spark_app_config_dict['driver_memory'], '1g')
64 | self.assertEqual(spark_app_config_dict['driver_memory_overhead'], '1g')
65 | self.assertEqual(spark_app_config_dict['driver_cores'], 1)
66 | self.assertEqual(spark_app_config_dict['executor_memory'], '1g')
67 | self.assertEqual(spark_app_config_dict['executor_memory_overhead'], '1g')
68 | self.assertEqual(spark_app_config_dict['executor_memory_fraction'], 1.0)
69 | self.assertEqual(spark_app_config_dict['executor_cores'], 1)
70 | self.assertEqual(spark_app_config_dict['executor_instances'], 1)
71 | self.assertEqual(spark_app_config_dict['dynamic_allocation_enabled'], True)
72 | self.assertEqual(spark_app_config_dict['executor_instances_min'], 1)
73 | self.assertEqual(spark_app_config_dict['executor_instances_max'], 1)
74 | self.assertEqual(spark_app_config_dict['shuffle_service_enabled'], True)
75 | self.assertEqual(spark_app_config_dict['executor_idle_timeout'], 1)
76 | self.assertEqual(spark_app_config_dict['queue'], 'test_queue')
--------------------------------------------------------------------------------
/docker/postgres/init.sql:
--------------------------------------------------------------------------------
1 | CREATE USER server WITH PASSWORD 'password-server';
2 |
3 | CREATE DATABASE server_db;
4 |
5 | \c server_db
6 |
7 | CREATE TABLE users (
8 | id SERIAL PRIMARY KEY,
9 | name VARCHAR(100) NOT NULL,
10 | password_hash VARCHAR(255) NOT NULL,
11 | email VARCHAR(100) NOT NULL UNIQUE
12 | );
13 |
14 | CREATE TABLE notebooks (
15 | id SERIAL PRIMARY KEY,
16 | name VARCHAR(100) NOT NULL,
17 | path VARCHAR(100) NOT NULL,
18 | user_id INT REFERENCES users(id)
19 | );
20 |
21 | CREATE TABLE directories (
22 | id SERIAL PRIMARY KEY,
23 | name VARCHAR(100) NOT NULL,
24 | path VARCHAR(100) NOT NULL,
25 | user_id INT REFERENCES users(id)
26 | );
27 |
28 | CREATE TABLE spark_apps (
29 | spark_app_id VARCHAR(100) PRIMARY KEY,
30 | notebook_id INT REFERENCES notebooks(id),
31 | user_id INT REFERENCES users(id),
32 | status VARCHAR(100),
33 | created_at TIMESTAMP
34 | );
35 |
36 | CREATE TABLE spark_app_config (
37 | id SERIAL PRIMARY KEY,
38 | notebook_id INT REFERENCES notebooks(id),
39 | driver_memory VARCHAR(100),
40 | driver_memory_overhead VARCHAR(100),
41 | driver_cores INT,
42 | executor_memory VARCHAR(100),
43 | executor_memory_overhead VARCHAR(100),
44 | executor_memory_fraction FLOAT,
45 | executor_cores INT,
46 | executor_instances INT,
47 | dynamic_allocation_enabled BOOLEAN,
48 | executor_instances_min INT,
49 | executor_instances_max INT,
50 | shuffle_service_enabled BOOLEAN,
51 | executor_idle_timeout INT,
52 | queue VARCHAR(100)
53 | );
54 |
55 | GRANT ALL PRIVILEGES ON TABLE users TO server;
56 | GRANT ALL PRIVILEGES ON SEQUENCE users_id_seq TO server;
57 |
58 | GRANT ALL PRIVILEGES ON TABLE notebooks TO server;
59 | GRANT ALL PRIVILEGES ON SEQUENCE notebooks_id_seq TO server;
60 |
61 | GRANT ALL PRIVILEGES ON TABLE directories TO server;
62 | GRANT ALL PRIVILEGES ON SEQUENCE directories_id_seq TO server;
63 |
64 | GRANT ALL PRIVILEGES ON TABLE spark_apps TO server;
65 |
66 | GRANT ALL PRIVILEGES ON TABLE spark_app_config TO server;
67 | GRANT ALL PRIVILEGES ON SEQUENCE spark_app_config_id_seq TO server;
68 |
69 | -- Add some initial data
70 | -- user_0 -12345A
71 | INSERT INTO users (name, password_hash, email) VALUES
72 | ('user_0', 'scrypt:32768:8:1$1k6HpQA8N58PkDz7$db383b0d69d7a2f6893116b1955da70cb217173dc44ce169acf57cfe6a79f63118ad7515563a0b4f8f39dda49510d061acdba26be8f7c8786c161dd54d7a91c1', 'user_0@gmail.com'),
73 | ('user_1', 'pbkdf2:sha256:150000$3Z6Z6Z6Z$e3', 'user_1@gmail.com');
74 |
75 | INSERT INTO notebooks (name, path, user_id) VALUES
76 | ('demo.ipynb', 'work/user_0@gmail.com/demo.ipynb', 1),
77 | ('notebook.ipynb', 'work/user_0@gmail.com/notebook.ipynb', 1),
78 | ('quickstart.ipynb', 'work/user_0@gmail.com/quickstart.ipynb', 1),
79 | ('sg-resale-flat-prices.ipynb', 'work/user_0@gmail.com/sg-resale-flat-prices/sg-resale-flat-prices.ipynb', 1);
80 |
81 | INSERT INTO directories (name, path, user_id) VALUES
82 | ('user_0@gmail.com', '/work/user_0@gmail.com', 1),
83 | ('word-count', '/work/user_0@gmail.com/word-count', 1),
84 | ('sg-resale-flat-prices', '/work/user_0@gmail.com/sg-resale-flat-prices', 1),
85 | ('output', '/work/user_0@gmail.com/sg-resale-flat-prices/output', 1),
86 | ('user_1@gmail.com', '/work/user_0@gmail.com', 1);
87 |
88 |
--------------------------------------------------------------------------------
/webapp/src/components/sidebar/workspace/WorkspaceSidebar.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useRef, useEffect } from 'react';
2 | import { Drawer, List } from '@mui/material';
3 | import { CgFileDocument, CgFolder } from "react-icons/cg";
4 | import WorkspaceSidebarHeader from './header/WorkspaceSidebarHeader';
5 | import Back from './Back';
6 | import Item from './item/Item';
7 |
8 | function WorkspaceSidebar({
9 | openWorkspaceDrawer,
10 | closeWorkspaceDrawer,
11 | handleToggleWorkspaceDrawer,
12 | onExistinNotebookClick,
13 | handleDirectoryClick,
14 | currentPath,
15 | setCurrentPath,
16 | setRefreshKey,
17 | workspaceFiles,
18 | rootPath}) {
19 |
20 | const workspaceSidebarWidth = 300;
21 |
22 | const handleBackClick = () => {
23 | const parentPath = currentPath.split('/').slice(0, -1).join('/');
24 | if (parentPath === 'work') {
25 | setCurrentPath(rootPath);
26 | } else {
27 | setCurrentPath(parentPath);
28 | }
29 | };
30 |
31 | return (
32 |
54 |
55 |
61 |
62 |
67 | {currentPath && (
68 |
69 | )}
70 | {workspaceFiles.map((file, index) => {
71 | if (file.type === 'file') {
72 | return null; // Do not render anything for regular files
73 | }
74 |
75 | let IconComponent;
76 | if (file.type === 'notebook') {
77 | IconComponent = CgFileDocument;
78 | } else if (file.type === 'directory') {
79 | IconComponent = CgFolder;
80 | }
81 |
82 | return (
83 | -
93 | );
94 | })}
95 |
96 |
97 | )
98 | }
99 |
100 | export default WorkspaceSidebar;
--------------------------------------------------------------------------------
/server/app/routes/notebook.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, jsonify, request, g
2 | from app.services.notebook import Notebook
3 | from flask_jwt_extended import jwt_required
4 | from app.auth.auth import identify_user
5 | import logging
6 |
7 | notebook_blueprint = Blueprint('notebook', __name__)
8 |
9 | logging.basicConfig(level=logging.INFO)
10 |
11 | @notebook_blueprint.route('/notebook')
12 | def notebook():
13 | return jsonify(
14 | {
15 | "message": "notebook endpoint"
16 | }
17 | )
18 |
19 | @notebook_blueprint.route('/notebook/all', methods=['GET'])
20 | @jwt_required()
21 | @identify_user
22 | def get_all_notebooks():
23 | logging.info(f"Getting all notebooks by user: {g.user.name}")
24 | return Notebook.get_all_notebooks()
25 |
26 | @notebook_blueprint.route('/notebook/', methods=['GET'])
27 | @jwt_required()
28 | @identify_user
29 | def get_notebook_by_path(notebook_path):
30 | logging.info(f"Getting notebook with path: {notebook_path} by user: {g.user.name}")
31 | return Notebook.get_notebook_by_path(notebook_path=notebook_path)
32 |
33 | @notebook_blueprint.route('/notebook', methods=['POST'])
34 | @jwt_required()
35 | @identify_user
36 | def create_notebook():
37 | data = request.get_json()
38 | notebook_name = data.get('name', None)
39 | notebook_path = data.get('path', None)
40 | logging.info(f"Creating notebook with name: {notebook_name} and path: {notebook_path} by user {g.user.name}")
41 | return Notebook.create_notebook_with_init_cells(notebook_name=notebook_name, notebook_path=notebook_path)
42 |
43 | @notebook_blueprint.route('/notebook/', methods=['PUT'])
44 | @jwt_required()
45 | @identify_user
46 | def update_notebook(notebook_path):
47 | data = request.get_json()
48 | content = data.get('content', None)
49 | logging.info(f"Updating notebook with path: {notebook_path} by user: {g.user.name}")
50 | return Notebook.update_notebook(notebook_path=notebook_path, content=content)
51 |
52 | @notebook_blueprint.route('/notebook/', methods=['DELETE'])
53 | @jwt_required()
54 | @identify_user
55 | def delete_notebook(notebook_path):
56 | logging.info(f"Deleting notebook with path: {notebook_path}")
57 | return Notebook.delete_notebook_by_path(notebook_path=notebook_path)
58 |
59 | @notebook_blueprint.route('/notebook/', methods=['PATCH'])
60 | @jwt_required()
61 | @identify_user
62 | def rename_or_move_notebook(notebook_path):
63 | data = request.get_json()
64 | if 'newName' in data:
65 | logging.info(f"Renaming notebook with path: {notebook_path} to {data['newName']}")
66 | new_notebook_name = data.get('newName', None)
67 | return Notebook.rename_notebook_by_path(notebook_path=notebook_path, new_notebook_name=new_notebook_name)
68 | elif 'newPath' in data:
69 | logging.info(f"Moving notebook with path: {notebook_path} to {data['newPath']}")
70 | new_notebook_path = data.get('newPath', None)
71 | return Notebook.move_notebook(notebook_path=notebook_path, new_notebook_path=new_notebook_path)
72 |
73 | @notebook_blueprint.route('/notebook/spark_app/', methods=['GET'])
74 | @jwt_required()
75 | @identify_user
76 | def get_spark_app_by_notebook_path(notebook_path):
77 | logging.info(f"Get spark apps by notebook path: {notebook_path}")
78 | return Notebook.get_spark_app_by_notebook_path(notebook_path)
79 |
80 |
81 |
82 |
--------------------------------------------------------------------------------
/webapp/src/components/notebook/header/NotebookKernel.js:
--------------------------------------------------------------------------------
1 | import { Tooltip, Box, Button, Menu, MenuItem } from '@mui/material';
2 | import { useState } from 'react';
3 | import { JupyterKernelIcon } from '@datalayer/icons-react';
4 | import { VscTriangleDown } from "react-icons/vsc";
5 | import LoadingButton from '@mui/lab/LoadingButton';
6 | import KernelModel from '../../../models/KernelModel'
7 | import config from '../../../config';
8 |
9 | const NotebookKernel = ({
10 | kernelId,
11 | setSparkAppId,
12 | clearOutputs
13 | }) => {
14 |
15 | const [anchorEl, setAnchorEl] = useState(null);
16 | const [menuOpen, setMenuOpen] = useState(false);
17 | const [isRestarting, setIsRestarting] = useState(false);
18 |
19 | const handleRestartKernel = async () => {
20 | try {
21 | setIsRestarting(true);
22 | setMenuOpen(false);
23 | setSparkAppId(null);
24 | await KernelModel.restartKernel(kernelId);
25 | setIsRestarting(false);
26 | } catch (error) {
27 | console.error('Failed to restart kernel:', error);
28 | }
29 | }
30 |
31 | const handleRestartKernelAndClearOutputs = async () => {
32 | clearOutputs();
33 | handleRestartKernel();
34 | }
35 |
36 | return (
37 |
41 | {
42 | kernelId === null ?
43 | }
47 | color="error"
48 | style={{ fontSize: '10px', padding: '3px 6px' }}>
49 | Not Connected
50 | :
51 |
52 | isRestarting ?
53 |
58 | Restarting...
59 | :
60 |
61 |
62 | }
66 | endIcon={ }
72 | onClick={(e) => { setMenuOpen(true); setAnchorEl(e.currentTarget);}}
73 | style={{ fontSize: '10px', padding: '3px 6px' }}>
74 | Connected
75 |
76 |
77 | }
78 |
79 | setMenuOpen(false)}
82 | anchorEl={anchorEl}
83 | >
84 |
90 | Restart Kernel
91 |
92 |
98 | Restart Kernel and Clear Outputs
99 |
100 |
101 |
102 | );
103 | }
104 |
105 | export default NotebookKernel;
--------------------------------------------------------------------------------
/server/app/models/spark_app_config.py:
--------------------------------------------------------------------------------
1 | from database import db
2 |
3 | class SparkAppConfigModel(db.Model):
4 |
5 | __tablename__ = 'spark_app_config'
6 |
7 | id = db.Column(db.Integer, primary_key=True, nullable=False)
8 | notebook_id = db.Column(db.Integer, db.ForeignKey('notebooks.id'), nullable=False)
9 | driver_memory = db.Column(db.String, nullable=True)
10 | driver_memory_overhead = db.Column(db.String, nullable=True)
11 | driver_cores = db.Column(db.Integer, nullable=True)
12 |
13 | executor_memory = db.Column(db.String, nullable=True)
14 | executor_memory_overhead = db.Column(db.String, nullable=True)
15 | executor_memory_fraction = db.Column(db.Float, nullable=True)
16 | executor_cores = db.Column(db.Integer, nullable=True)
17 | executor_instances = db.Column(db.Integer, nullable=True)
18 |
19 | dynamic_allocation_enabled = db.Column(db.Boolean, nullable=True)
20 | executor_instances_min = db.Column(db.Integer, nullable=True)
21 | executor_instances_max = db.Column(db.Integer, nullable=True)
22 |
23 | shuffle_service_enabled = db.Column(db.Boolean, nullable=True)
24 | executor_idle_timeout = db.Column(db.Integer, nullable=True)
25 | queue = db.Column(db.String, nullable=True)
26 |
27 | def __init__(self, notebook_id, driver_memory=None, driver_memory_overhead=None, driver_cores=None,
28 | executor_memory=None, executor_memory_overhead=None, executor_memory_fraction=None,
29 | executor_cores=None, executor_instances=None, dynamic_allocation_enabled=None,
30 | executor_instances_min=None, executor_instances_max=None, shuffle_service_enabled=None,
31 | executor_idle_timeout=None, queue=None):
32 | self.notebook_id = notebook_id
33 | self.driver_memory = driver_memory
34 | self.driver_memory_overhead = driver_memory_overhead
35 | self.driver_cores = driver_cores
36 | self.executor_memory = executor_memory
37 | self.executor_memory_overhead = executor_memory_overhead
38 | self.executor_memory_fraction = executor_memory_fraction
39 | self.executor_cores = executor_cores
40 | self.executor_instances = executor_instances
41 | self.dynamic_allocation_enabled = dynamic_allocation_enabled
42 | self.executor_instances_min = executor_instances_min
43 | self.executor_instances_max = executor_instances_max
44 | self.shuffle_service_enabled = shuffle_service_enabled
45 | self.executor_idle_timeout = executor_idle_timeout
46 | self.queue = queue
47 |
48 | def to_dict(self):
49 | return {
50 | 'notebook_id': self.notebook_id,
51 | 'driver_memory': self.driver_memory,
52 | 'driver_memory_overhead': self.driver_memory_overhead,
53 | 'driver_cores': self.driver_cores,
54 | 'executor_memory': self.executor_memory,
55 | 'executor_memory_overhead': self.executor_memory_overhead,
56 | 'executor_memory_fraction': self.executor_memory_fraction,
57 | 'executor_cores': self.executor_cores,
58 | 'executor_instances': self.executor_instances,
59 | 'dynamic_allocation_enabled': self.dynamic_allocation_enabled,
60 | 'executor_instances_min': self.executor_instances_min,
61 | 'executor_instances_max': self.executor_instances_max,
62 | 'shuffle_service_enabled': self.shuffle_service_enabled,
63 | 'executor_idle_timeout': self.executor_idle_timeout,
64 | 'queue': self.queue
65 | }
66 |
--------------------------------------------------------------------------------
/webapp/src/models/DirectoryModel.js:
--------------------------------------------------------------------------------
1 | import config from '../config';
2 |
3 | class DirectoryModel {
4 | constructor(path, files) {
5 | this.path = path;
6 | this.files = files;
7 | console.log('Directory:', this);
8 | }
9 |
10 | getNotebooks() {
11 | return this.files.filter(file => file.type === 'notebook');
12 | }
13 |
14 | getDirectories() {
15 | return this.files.filter(file => file.type === 'directory');
16 | }
17 |
18 | isUniqueNotebookName(name) {
19 | if (name.endsWith('.ipynb')) {
20 | return this.getNotebooks().every(notebook => notebook.name !== name);
21 | } else {
22 | return this.getNotebooks().every(notebook => notebook.name !== (name + '.ipynb'));
23 | }
24 | }
25 | isUniqueFolderName(name) {
26 | return this.getDirectories().every(directory => directory.name !== name);
27 | }
28 |
29 | static async getChildren(path = '') {
30 | const response = await fetch(`${config.serverBaseUrl}/directory/` + path);
31 | if (!response.ok) {
32 | throw new Error('Failed to fetch files');
33 | } else {
34 | const data = await response.json();
35 | return data.content;
36 | }
37 | }
38 |
39 | static async getSubDirectories(path = '') {
40 | const items = await this.getChildren(path);
41 | const promises = items.map(async (item) => {
42 | if (item.type === 'directory') {
43 | item.children = await this.getSubDirectories(`${path}/${item.name}`);
44 | }
45 | return item;
46 | });
47 | return Promise.all(promises);
48 | }
49 |
50 | static async createDirectory(path = '', directoryName = '') {
51 | console.log("Creating directory at path:", path + '/' + directoryName);
52 | const response = await fetch(`${config.serverBaseUrl}/directory`, {
53 | method: 'POST',
54 | headers: {
55 | 'Content-Type': 'application/json',
56 | },
57 | body: JSON.stringify({
58 | 'directoryPath': path + '/' + directoryName,
59 | })
60 | });
61 |
62 | if (!response.ok) {
63 | throw new Error('Failed to create directory');
64 | } else {
65 | const data = await response.json();
66 | return data;
67 | }
68 | };
69 |
70 | static async renameDirectory(oldPath='', newPath='') {
71 | console.log("Renaming item at path:", oldPath, "to", newPath);
72 | const response = await fetch(`${config.serverBaseUrl}/directory/` + oldPath, {
73 | method: 'PATCH',
74 | headers: {
75 | 'Content-Type': 'application/json',
76 | },
77 | body: JSON.stringify({
78 | newPath: newPath
79 | }),
80 | });
81 | }
82 |
83 | static async deleteDirectory(item = '') {
84 | const itemPath = item.path;
85 |
86 | let folderItems = [];
87 | await DirectoryModel.getChildren(itemPath)
88 | .then((data) => {
89 | folderItems = data;
90 | })
91 | if (folderItems.length > 0) {
92 | alert('Directory is not empty');
93 | } else {
94 | console.log("Deleting item at path:", itemPath);
95 | try {
96 | const response = await fetch(`${config.serverBaseUrl}/directory/` + itemPath, {
97 | method: 'DELETE',
98 | });
99 | if (!response.ok) {
100 | throw new Error('Failed to delete directory');
101 | }
102 | } catch (error) {
103 | alert(`Failed to delete directory: ${error.message}`);
104 | }
105 | }
106 | }
107 |
108 | }
109 |
110 | export default DirectoryModel;
--------------------------------------------------------------------------------
/webapp/src/components/notebook/content/Code.js:
--------------------------------------------------------------------------------
1 | import Cell from './cell/Cell';
2 | import NotebookModel from '../../../models/NotebookModel';
3 | import NotebookToolbar from './NotebookToolbar';
4 | import config from '../../../config';
5 | import { Box, Button, Tooltip, Card } from '@mui/material';
6 |
7 | const Code = ({
8 | notebook,
9 | notebookState,
10 | cellStatuses,
11 | setCellStatus,
12 | cellExecutedStatuses,
13 | setCellExecutedStatus,
14 | handleChangeCell,
15 | handleDeleteCell,
16 | handleChangeCellType,
17 | handleMoveCell,
18 | handleRunCodeCell,
19 | handleCopyCell,
20 | handleCreateCell,
21 | kernelId,
22 | setKernelId,
23 | runAllCells,
24 | saveNotebook,
25 | deleteNotebook,
26 | createSparkSession
27 | }) => {
28 |
29 | const jupyterBaseUrl= `${config.jupyterBaseUrl}`
30 | const baseUrl = `${jupyterBaseUrl}/api/contents/`
31 |
32 | return (
33 |
34 |
35 |
41 |
42 |
48 | {notebookState.content &&
49 | notebookState.content.cells &&
50 | notebookState.content.cells.map((cell, index) => (
51 |
52 | | setCellStatus(index, status)}
59 | cellExecutedStatus={cellExecutedStatuses[index]}
60 | setCellExecutedStatus={executed => setCellExecutedStatus(index, executed)}
61 | handleChangeCell={handleChangeCell}
62 | handleDeleteCell={handleDeleteCell}
63 | handleChangeCellType={handleChangeCellType}
64 | handleMoveCell={handleMoveCell}
65 | handleRunCodeCell={handleRunCodeCell}
66 | handleCopyCell={handleCopyCell}
67 | handelRunAllAboveCells={
68 | (index) => NotebookModel.runAllAboveCells(
69 | index,
70 | jupyterBaseUrl,
71 | notebookState,
72 | kernelId,
73 | setKernelId,
74 | cellStatuses,
75 | setCellStatus,
76 | cellExecutedStatuses,
77 | setCellExecutedStatus)}/>
78 |
82 |
85 | handleCreateCell('code', index + 1)}>
86 | + Code
87 |
88 |
89 |
92 | handleCreateCell('markdown', index + 1)}>
93 | + Markdown
94 |
95 |
96 |
97 | |
98 | ))}
99 |
100 | );
101 | }
102 |
103 | export default Code;
--------------------------------------------------------------------------------
/webapp/src/components/sidebar/workspace/item/MoreButton.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { CgMoreVerticalAlt } from "react-icons/cg";
3 | import { Menu, MenuItem } from '@mui/material';
4 | import config from '../../../../config';
5 | import DirectoryModel from '../../../../models/DirectoryModel';
6 | import NotebookModel from '../../../../models/NotebookModel';
7 | import DeleteDialog from './DeleteDialog';
8 | import RenameDialog from './RenameDialog';
9 |
10 | const MoreButton = ({
11 | file,
12 | currentPath,
13 | setRefreshKey
14 | }) => {
15 | const baseUrl = `${config.jupyterBaseUrl}/api/contents/`
16 |
17 | const [anchorEl, setAnchorEl] = useState(null);
18 | const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
19 | const [renameDialogOpen, setRenameDialogOpen] = useState(false);
20 |
21 | const handleMoreClicked = (event, file) => {
22 | console.log('More clicked:', file);
23 | setAnchorEl(event.currentTarget);
24 | }
25 |
26 | const handleMoreClose = () => {
27 | setAnchorEl(null);
28 | };
29 |
30 | const handleDelete = async (baseUrl, file) => {
31 | console.log('Delete:', file);
32 | try {
33 | await DirectoryModel.deleteDirectory(file);
34 | } catch (error) {
35 | console.error('Failed to delete item:', error);
36 | }
37 | setRefreshKey(oldKey => oldKey + 1);
38 | }
39 |
40 | const handleRename = async (file, newName) => {
41 | try {
42 | if (file.type === 'notebook') {
43 | await NotebookModel.renameNotebook(currentPath + '/' + file.name, newName);
44 | setRefreshKey(oldKey => oldKey + 1);
45 | } else {
46 | await DirectoryModel.renameDirectory(currentPath + '/' + file.name, currentPath + '/' + newName);
47 | setRefreshKey(oldKey => oldKey + 1);
48 | }
49 | } catch (error) {
50 | console.error('Failed to rename item:', error);
51 | }
52 | }
53 |
54 | return (
55 |
56 |
58 | handleMoreClicked(event, file)}
59 | onMouseEnter={(e) => {
60 | e.currentTarget.style.color = 'white';
61 | }}
62 | onMouseLeave={(e) => {
63 | e.currentTarget.style.color = 'lightgrey';
64 | }}
65 | style={{
66 | color: 'lightgrey',
67 | fontSize: '20px',
68 | marginTop: '12px',
69 | marginBottom: 0,
70 | marginLeft: '10px',
71 | marginRight: '10px'
72 | }}
73 | />
74 |
81 |
82 | {/* Delete Button */}
83 | {
86 | setDeleteDialogOpen(true);
87 | }}>
88 | Delete
89 |
90 |
97 |
98 | {/* Rename Button */}
99 | {
102 | setRenameDialogOpen(true);
103 | }}>
104 | Rename
105 |
106 |
112 |
113 |
114 | );
115 | }
116 |
117 | export default MoreButton;
--------------------------------------------------------------------------------
/webapp/src/components/auth/LoginForm.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { TextField, Button, Paper, Typography, Container, Snackbar } from '@mui/material';
3 | import Alert from '@mui/material/Alert';
4 | import config from '../../config';
5 |
6 | function LoginForm({ onLogin }) {
7 | const [username, setUsername] = useState('');
8 | const [password, setPassword] = useState('');
9 | const [error, setError] = useState(false);
10 |
11 | const handleSubmit = async event => {
12 | event.preventDefault();
13 |
14 | const credentials = btoa(`${username}:${password}`);
15 | const response = await fetch(`${config.serverBaseUrl}/login`, {
16 | method: 'POST',
17 | headers: {
18 | 'Content-Type': 'application/json',
19 | 'Authorization': `Basic ${credentials}`,
20 | }
21 | });
22 |
23 | if (response.ok) {
24 | // The user is logged in
25 | // You might want to save the username and password in the session storage here
26 | // And redirect the user to the home page
27 | console.log('Logged in successfully');
28 | const data = await response.json();
29 | const token = data.access_token;
30 | const useremail = data.email;
31 | sessionStorage.setItem('token', token);
32 | onLogin(username, useremail, password);
33 | } else {
34 | // The login failed
35 | // You might want to show an error message here
36 | console.error('Failed to log in: ', response.text, response.status);
37 | setError(true);
38 | }
39 | };
40 |
41 | return (
42 |
43 |
52 |
53 | Sign In
54 |
55 |
96 |
97 |
98 | setError(false)}>
99 | Failed to log in
100 |
101 |
102 | );
103 | }
104 |
105 | export default LoginForm;
--------------------------------------------------------------------------------
/webapp/src/components/notebook/content/NotebookToolbar.js:
--------------------------------------------------------------------------------
1 | import { VscSave, VscRunAll, VscTrash, VscFlame } from "react-icons/vsc";
2 | import Tooltip from '@mui/material/Tooltip';
3 | import { Card, IconButton } from '@mui/material';
4 | import MoveButton from "../header/move/MoveButton";
5 |
6 | const NotebookToolbar = ({
7 | notebook,
8 | runAllCells,
9 | saveNotebook,
10 | deleteNotebook,
11 | createSparkSession
12 | }) => {
13 | const headerIconSize = 13;
14 |
15 | return (
16 |
30 |
31 | {/* Save Button */}
32 |
33 |
39 | {
42 | e.currentTarget.style.color = 'black';
43 | }}
44 | onMouseLeave={(e) => {
45 | e.currentTarget.style.color = 'black';
46 | }}
47 | style={{ color: 'black' }}/>
48 |
49 |
50 |
51 | {/* Run All Button */}
52 |
53 |
56 | runAllCells()}
57 | aria-label="run"
58 | sx={{
59 | width: 'auto',
60 | mt: 0.5 }}>
61 | {
64 | e.currentTarget.style.color = 'black';
65 | }}
66 | onMouseLeave={(e) => {
67 | e.currentTarget.style.color = 'black';
68 | }}
69 | style={{ color: 'black' }}/>
70 |
71 |
72 |
73 |
76 |
77 | {/* New Spark Button */}
78 |
79 |
86 | {
89 | e.currentTarget.style.color = 'black';
90 | }}
91 | onMouseLeave={(e) => {
92 | e.currentTarget.style.color = 'black';
93 | }}
94 | style={{ color: 'black' }}/>
95 |
96 |
97 |
98 | {/* Delete Button */}
99 |
100 |
106 | {
109 | e.currentTarget.style.color = 'black';
110 | }}
111 | onMouseLeave={(e) => {
112 | e.currentTarget.style.color = 'black';
113 | }}
114 | style={{ color: 'black' }}/>
115 |
116 |
117 | {/* */}
118 |
119 | )
120 | }
121 |
122 | export default NotebookToolbar;
--------------------------------------------------------------------------------
/webapp/src/components/sidebar/workspace/header/WorkspaceSidebarHeader.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { Typography, Box } from '@mui/material';
3 | import config from '../../../../config';
4 | import CreateButton from './CreateButton';
5 | import DirectoryModel from '../../../../models/DirectoryModel';
6 | import NotebookModel from '../../../../models/NotebookModel';
7 |
8 |
9 | const WorkspaceSidebarHeader = ({
10 | rootPath,
11 | currentPath,
12 | setCurrentPath,
13 | setRefreshKey,
14 | workspaceFiles
15 | }) => {
16 | const baseUrl = `${config.jupyterBaseUrl}/api/contents/`
17 |
18 | const [anchorEl, setAnchorEl] = useState(null);
19 |
20 | const [createNotebookDialogOpen, setCreateNotebookDialogOpen] = useState(false);
21 | const [notebookName, setNotebookName] = useState('');
22 |
23 | const [createFolderDialogOpen, setCreateFolderDialogOpen] = useState(false);
24 | const [folderName, setFolderName] = useState('');
25 |
26 | const handleCreateClick = (event) => {
27 | setAnchorEl(event.currentTarget);
28 | };
29 |
30 | const handleCreateClose = () => {
31 | setAnchorEl(null);
32 | };
33 |
34 | const handleCreateFolder = () => {
35 | const directoryModel = new DirectoryModel(currentPath, workspaceFiles);
36 | if (directoryModel.isUniqueFolderName(folderName)) {
37 | console.log('Creating folder:', folderName);
38 | DirectoryModel.createDirectory(`${currentPath}`, folderName);
39 | setCreateFolderDialogOpen(false);
40 | handleCreateClose();
41 | setRefreshKey(oldKey => oldKey + 1);
42 | } else {
43 | console.error('Folder name already exists:', folderName);
44 | alert('Folder name already exists. Please choose a different name.');
45 | }
46 | };
47 |
48 | const handleCreateNotebook = () => {
49 | const directoryModel = new DirectoryModel(currentPath, workspaceFiles);
50 | if (directoryModel.isUniqueNotebookName(notebookName)) {
51 | console.log('Creating notebook:', notebookName);
52 | NotebookModel.createNotebook(`${currentPath}`, notebookName);
53 | setCreateNotebookDialogOpen(false);
54 | handleCreateClose();
55 | setRefreshKey(oldKey => oldKey + 1);
56 | } else {
57 | console.error('Notebook name already exists:', notebookName);
58 | alert('Notebook name already exists. Please choose a different name.');
59 | }
60 | }
61 |
62 | return (
63 |
64 |
65 |
76 | Workspace
77 |
78 |
79 |
94 |
95 |
96 |
101 |
102 | {currentPath.replace(rootPath, '') === '' ? '/' : currentPath.replace(rootPath, '')}
103 |
104 |
105 |
106 | );
107 | }
108 |
109 | export default WorkspaceSidebarHeader;
--------------------------------------------------------------------------------
/webapp/src/models/SparkModel.js:
--------------------------------------------------------------------------------
1 | import config from '../config';
2 |
3 |
4 | class SparkModel {
5 | constructor() {
6 | }
7 |
8 | static isSparkInfo(html) {
9 | const parser = new DOMParser();
10 | const doc = parser.parseFromString(html, 'text/html');
11 |
12 | // Check if it's a Spark info div
13 | const sparkInfo = doc.querySelector('div');
14 | if (!sparkInfo || !sparkInfo.textContent.includes('Spark Session Information')) {
15 | return false;
16 | }
17 |
18 | // Verify it has an Application ID that starts with 'app-'
19 | const appIdElement = Array.from(doc.querySelectorAll('p'))
20 | .find(p => p.textContent.includes('Application ID:'));
21 | if (!appIdElement) {
22 | return false;
23 | }
24 |
25 | const appId = appIdElement.textContent.split(': ')[1];
26 | return appId && appId.startsWith('app-');
27 | }
28 |
29 | static async storeSparkInfo(sparkAppId, notebookPath) {
30 | console.log('Attempting to store spark info for:', sparkAppId);
31 | const token = sessionStorage.getItem('token');
32 |
33 | if (!sparkAppId.startsWith('app-')) {
34 | console.log('Not a valid Spark application ID:', sparkAppId);
35 | return;
36 | }
37 |
38 | try {
39 | const checkResponse = await fetch(`${config.serverBaseUrl}/spark_app/${sparkAppId}/status`, {
40 | headers: {
41 | 'Authorization': `Bearer ${token}`,
42 | 'Content-Type': 'application/json'
43 | }
44 | });
45 |
46 | if (checkResponse.ok) {
47 | console.log('Spark app ID already exists:', sparkAppId);
48 | return;
49 | }
50 | } catch (error) {
51 | console.log('Status check failed:', error);
52 | }
53 |
54 | const response = await fetch(`${config.serverBaseUrl}/spark_app/${sparkAppId}`, {
55 | method: 'POST',
56 | headers: {
57 | 'Content-Type': 'application/json',
58 | 'Authorization': `Bearer ${token}`
59 | },
60 | body: JSON.stringify({
61 | notebookPath: notebookPath,
62 | }),
63 | });
64 |
65 | if (!response.ok) {
66 | throw new Error(`Failed to store Spark application id: ${response.status}`);
67 | }
68 | }
69 |
70 | static extractSparkAppId(html) {
71 | const parser = new DOMParser();
72 | const doc = parser.parseFromString(html, 'text/html');
73 | const appIdTag = Array.from(doc.querySelectorAll('p'))
74 | .find(p => p.textContent.includes('Application ID:'));
75 |
76 | if (!appIdTag) return null;
77 |
78 | const appId = appIdTag.textContent.split(': ')[1];
79 | console.log('Extracted Spark app ID:', appId);
80 | return appId && appId.startsWith('app-') ? appId : null;
81 | }
82 |
83 | static async createSparkSession(notebookPath) {
84 | try {
85 | // Create a cell with Spark initialization code
86 | const sparkInitCode = `spark = create_spark("${notebookPath}")
87 | spark`;
88 |
89 | return {
90 | initializationCode: sparkInitCode
91 | };
92 | } catch (error) {
93 | console.error('Error creating Spark session:', error);
94 | throw error;
95 | }
96 | }
97 |
98 | static async getSparkAppByNotebookPath(notebookPath) {
99 | const token = sessionStorage.getItem('token');
100 | try {
101 | const response = await fetch(`${config.serverBaseUrl}/notebook/spark_app/${notebookPath}`, {
102 | headers: {
103 | 'Authorization': `Bearer ${token}`,
104 | 'Content-Type': 'application/json'
105 | }
106 | });
107 |
108 | if (!response.ok) {
109 | return null;
110 | }
111 |
112 | const sparkApps = await response.json();
113 | return sparkApps.length > 0 ? sparkApps[0] : null;
114 | } catch (error) {
115 | console.error('Failed to fetch Spark app:', error);
116 | return null;
117 | }
118 | }
119 | }
120 |
121 | export default SparkModel;
122 |
123 |
124 |
--------------------------------------------------------------------------------
/webapp/src/components/sidebar/account/AccountSidebar.js:
--------------------------------------------------------------------------------
1 | import { Typography, Drawer, List, ListItem, ListItemIcon, ListItemText } from '@mui/material';
2 | import { BsPerson, BsEnvelope, BsBoxArrowLeft } from "react-icons/bs";
3 |
4 |
5 | function AccountSidebar({
6 | itemHeight,
7 | accountButtonRef,
8 | openAccountDrawer,
9 | handleToggleAccountDrawer,
10 | username,
11 | useremail,
12 | logout
13 | }) {
14 | return (
15 |
41 |
42 |
44 | {
46 | }}
47 | sx={{
48 | // '&:hover': {
49 | // backgroundColor: '#555',
50 | // },
51 | // '&:hover .MuiTypography-root': {
52 | // color: 'white',
53 | // },
54 | marginTop: '-5px',
55 | }}>
56 |
57 |
62 |
63 |
64 |
70 | {username}
71 |
72 |
73 |
74 |
75 | {
77 | }}
78 | sx={{
79 | marginTop: '-12px',
80 | }}>
81 |
82 |
87 |
88 |
89 |
95 | {useremail}
96 |
97 |
98 |
99 |
100 | {
102 | logout();
103 | }}
104 | sx={{
105 | marginTop: '-12px',
106 | }}>
107 |
108 |
113 |
114 |
115 |
121 | Log out
122 |
123 |
124 |
125 |
126 |
127 | );
128 | }
129 |
130 | export default AccountSidebar;
--------------------------------------------------------------------------------
/webapp/src/components/notebook/header/NotebookHeader.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Box, AppBar, Tabs, Tab } from '@mui/material';
3 | import NotebookTitle from './NotebookTitle';
4 | import NotebookKernel from './NotebookKernel';
5 | import SparkApplicationId from './SparkApplicationId';
6 | import { ContentType } from '../content/ContentType';
7 |
8 | function NotebookHeader({
9 | setContentType,
10 | kernelId,
11 | sparkAppId,
12 | setSparkAppId,
13 | isNameEditing,
14 | currentName,
15 | isNotebookModified,
16 | handleClickNotebookName,
17 | handleChangeNotebookName,
18 | handleSaveNotebookName,
19 | clearOutputs}) {
20 |
21 | const [value, setValue] = useState(0);
22 |
23 | const handleChange = (event, newValue) => {
24 | setValue(newValue);
25 |
26 | if (newValue === 0) {
27 | setContentType(ContentType.CODE);
28 | } else if (newValue === 1) {
29 | setContentType(ContentType.Config);
30 | } else if (newValue === 2) {
31 | setContentType(ContentType.Runs);
32 | }
33 | };
34 |
35 | const a11yProps = (index) => {
36 | return {
37 | id: `simple-tab-${index}`,
38 | 'aria-controls': `simple-tabpanel-${index}`,
39 | };
40 | };
41 |
42 | return (
43 |
50 |
58 |
62 |
63 |
64 |
72 |
73 |
74 |
75 |
80 |
82 |
83 |
87 |
88 |
89 |
90 |
95 |
102 |
107 |
112 |
117 |
118 |
119 |
120 |
121 |
122 | );
123 | }
124 |
125 | export default NotebookHeader;
--------------------------------------------------------------------------------
/examples/user_0@gmail.com/word-count/pom.xml:
--------------------------------------------------------------------------------
1 |
2 | 4.0.0
3 | com.example
4 | word-count
5 | 1.0-SNAPSHOT
6 |
7 |
8 | 1.8
9 | 1.8
10 | 2.12.18
11 | 2.12
12 | 3.5.0
13 | hadoop3-2.2.0
14 | 30.1-jre:compile
15 |
16 |
17 |
18 |
19 |
20 | org.apache.spark
21 | spark-core_${scala.compat.version}
22 | ${spark.version}
23 |
24 |
25 | com.google.guava
26 | guava
27 |
28 |
29 |
30 |
31 | org.apache.spark
32 | spark-sql_${scala.compat.version}
33 | ${spark.version}
34 |
35 |
36 | com.google.guava
37 | guava
38 |
39 |
40 |
41 |
42 | com.google.cloud.bigdataoss
43 | gcs-connector
44 | ${gcs.connector.version}
45 |
51 |
52 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | net.alchim31.maven
65 | scala-maven-plugin
66 | 4.5.6
67 |
68 |
69 |
70 | compile
71 | testCompile
72 |
73 |
74 |
75 |
76 |
77 |
78 | org.apache.maven.plugins
79 | maven-shade-plugin
80 | 3.2.4
81 |
82 |
83 | package
84 |
85 | shade
86 |
87 |
88 |
89 |
90 |
91 | com.google.common
92 | shaded.com.google.common
93 |
94 |
95 |
96 |
97 |
98 | *:*
99 |
100 | META-INF/*.SF
101 | META-INF/*.DSA
102 | META-INF/*.RSA
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
--------------------------------------------------------------------------------
/server/tests/services/test_directory_service.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from flask_cors import CORS
3 | from flask import g
4 | from run import create_app
5 | from database import db
6 | from app.models.directory import DirectoryModel
7 | from app.services.user import User
8 | from app.services.directory import Directory
9 | import json
10 |
11 | class DirectoryServiceTestCase(unittest.TestCase):
12 |
13 | def setUp(self):
14 | self.app = create_app()
15 | self.client = self.app.test_client()
16 | with self.app.app_context():
17 | db.create_all()
18 |
19 | def tearDown(self):
20 | with self.app.app_context():
21 | db.session.remove()
22 | db.drop_all()
23 |
24 | def test_create_directory(self):
25 | with self.app.app_context():
26 |
27 | response_0 = Directory.get_content_by_path('work')
28 | self.assertEqual(response_0.status_code, 200)
29 |
30 | g.user = User.get_mock_user()
31 |
32 | # Create directory
33 | response_1 = Directory.create_directory('work/test_create_directory')
34 | self.assertEqual(response_1.status_code, 201)
35 | directoryFromDB = DirectoryModel.query.filter_by(path='work/test_create_directory').first()
36 | self.assertIsNotNone(directoryFromDB)
37 | self.assertEqual(directoryFromDB.name, 'test_create_directory')
38 |
39 | # Check if created directory could be detected
40 | response_2 = Directory.get_content_by_path('work')
41 | self.assertEqual(response_2.status_code, 200)
42 | self.assertEqual(len([x for x in json.loads(response_2.data)['content'] if x['name'] == 'test_create_directory']), 1)
43 |
44 | def test_delete_directory_by_path(self):
45 | with self.app.app_context():
46 |
47 | g.user = User.get_mock_user()
48 |
49 | # Create directory
50 | response_0 = Directory.create_directory('work/test_delete_directory_by_path')
51 | directoryFromDB = DirectoryModel.query.filter_by(path='work/test_delete_directory_by_path').first()
52 | self.assertIsNotNone(directoryFromDB)
53 | self.assertEqual(directoryFromDB.name, 'test_delete_directory_by_path')
54 |
55 | response_1 = Directory.get_content_by_path('work')
56 | self.assertEqual(response_1.status_code, 200)
57 | self.assertEqual(len([x for x in json.loads(response_1.data)['content'] if x['name'] == 'test_delete_directory_by_path']), 1)
58 |
59 | # Delete directory
60 | response_2 = Directory.delete_directory_by_path('work/test_delete_directory_by_path')
61 | self.assertEqual(response_2.status_code, 200)
62 |
63 | # Check if deleted directory could not be detected
64 | response_3 = Directory.get_content_by_path('work')
65 | self.assertEqual(response_3.status_code, 200)
66 | self.assertEqual(len([x for x in json.loads(response_3.data)['content'] if x['name'] == 'test_delete_directory_by_path']), 0)
67 |
68 | def test_rename_directory_by_path(self):
69 | with self.app.app_context():
70 |
71 | g.user = User.get_mock_user()
72 |
73 | response_0 = Directory.get_content_by_path('work')
74 | contents = json.loads(response_0.data)['content']
75 | content_0 = [x for x in contents if x['name'] == 'updated_name']
76 | self.assertEqual(response_0.status_code, 200)
77 | self.assertEqual(content_0, [])
78 |
79 | # Create directory
80 | response_0 = Directory.create_directory('work/original_name')
81 | directoryFromDB = DirectoryModel.query.filter_by(path='work/original_name').first()
82 | self.assertIsNotNone(directoryFromDB)
83 | self.assertEqual(directoryFromDB.name, 'original_name')
84 |
85 | # Rename directory
86 | response_1 = Directory.rename_directory_by_path('work/original_name', 'work/updated_name')
87 | self.assertEqual(response_1.status_code, 200)
88 |
89 | directoryFromDB = DirectoryModel.query.filter_by(path='work/updated_name').first()
90 | self.assertIsNotNone(directoryFromDB)
91 | self.assertEqual(directoryFromDB.name, 'updated_name')
92 |
93 | # Check if renamed directory could be detected
94 | response_2 = Directory.get_content_by_path('work')
95 | contents = json.loads(response_2.data)['content']
96 | content_original = [x for x in contents if x['name'] == 'original_name']
97 | content_updated = [x for x in contents if x['name'] == 'updated_name']
98 |
99 | self.assertEqual(len(content_original), 0)
100 | self.assertEqual(len(content_updated), 1)
101 | self.assertEqual(content_updated[0]['name'], 'updated_name')
102 | self.assertEqual(content_updated[0]['path'], 'work/updated_name')
103 |
104 |
--------------------------------------------------------------------------------