mirror of
https://github.com/jupyter/notebook.git
synced 2024-12-27 04:20:22 +08:00
Merge pull request #5001 from ellisonbg/dashboard-dirs
Add directory navigation to dashboard
This commit is contained in:
commit
cd7c1e6fae
@ -22,7 +22,6 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import stat
|
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
try:
|
try:
|
||||||
@ -42,10 +41,7 @@ except ImportError:
|
|||||||
from IPython.config import Application
|
from IPython.config import Application
|
||||||
from IPython.utils.path import filefind
|
from IPython.utils.path import filefind
|
||||||
from IPython.utils.py3compat import string_types
|
from IPython.utils.py3compat import string_types
|
||||||
|
from IPython.html.utils import is_hidden
|
||||||
# UF_HIDDEN is a stat flag not defined in the stat module.
|
|
||||||
# It is used by BSD to indicate hidden files.
|
|
||||||
UF_HIDDEN = getattr(stat, 'UF_HIDDEN', 32768)
|
|
||||||
|
|
||||||
#-----------------------------------------------------------------------------
|
#-----------------------------------------------------------------------------
|
||||||
# Top-level handlers
|
# Top-level handlers
|
||||||
@ -269,28 +265,9 @@ class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
|
|||||||
"""
|
"""
|
||||||
abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
|
abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
|
||||||
abs_root = os.path.abspath(root)
|
abs_root = os.path.abspath(root)
|
||||||
self.forbid_hidden(abs_root, abs_path)
|
if is_hidden(abs_path, abs_root):
|
||||||
|
raise web.HTTPError(404)
|
||||||
return abs_path
|
return abs_path
|
||||||
|
|
||||||
def forbid_hidden(self, absolute_root, absolute_path):
|
|
||||||
"""Raise 403 if a file is hidden or contained in a hidden directory.
|
|
||||||
|
|
||||||
Hidden is determined by either name starting with '.'
|
|
||||||
or the UF_HIDDEN flag as reported by stat
|
|
||||||
"""
|
|
||||||
inside_root = absolute_path[len(absolute_root):]
|
|
||||||
if any(part.startswith('.') for part in inside_root.split(os.sep)):
|
|
||||||
raise web.HTTPError(403)
|
|
||||||
|
|
||||||
# check UF_HIDDEN on any location up to root
|
|
||||||
path = absolute_path
|
|
||||||
while path and path.startswith(absolute_root) and path != absolute_root:
|
|
||||||
st = os.stat(path)
|
|
||||||
if getattr(st, 'st_flags', 0) & UF_HIDDEN:
|
|
||||||
raise web.HTTPError(403)
|
|
||||||
path = os.path.dirname(path)
|
|
||||||
|
|
||||||
return absolute_path
|
|
||||||
|
|
||||||
|
|
||||||
def json_errors(method):
|
def json_errors(method):
|
||||||
|
@ -29,6 +29,7 @@ from .nbmanager import NotebookManager
|
|||||||
from IPython.nbformat import current
|
from IPython.nbformat import current
|
||||||
from IPython.utils.traitlets import Unicode, Dict, Bool, TraitError
|
from IPython.utils.traitlets import Unicode, Dict, Bool, TraitError
|
||||||
from IPython.utils import tz
|
from IPython.utils import tz
|
||||||
|
from IPython.html.utils import is_hidden
|
||||||
|
|
||||||
#-----------------------------------------------------------------------------
|
#-----------------------------------------------------------------------------
|
||||||
# Classes
|
# Classes
|
||||||
@ -108,7 +109,26 @@ class FileNotebookManager(NotebookManager):
|
|||||||
path = path.strip('/')
|
path = path.strip('/')
|
||||||
os_path = self.get_os_path(path=path)
|
os_path = self.get_os_path(path=path)
|
||||||
return os.path.isdir(os_path)
|
return os.path.isdir(os_path)
|
||||||
|
|
||||||
|
def is_hidden(self, path):
|
||||||
|
"""Does the API style path correspond to a hidden directory or file?
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
path : string
|
||||||
|
The path to check. This is an API path (`/` separated,
|
||||||
|
relative to base notebook-dir).
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
exists : bool
|
||||||
|
Whether the path is hidden.
|
||||||
|
|
||||||
|
"""
|
||||||
|
path = path.strip('/')
|
||||||
|
os_path = self.get_os_path(path=path)
|
||||||
|
return is_hidden(os_path, self.notebook_dir)
|
||||||
|
|
||||||
def get_os_path(self, name=None, path=''):
|
def get_os_path(self, name=None, path=''):
|
||||||
"""Given a notebook name and a URL path, return its file system
|
"""Given a notebook name and a URL path, return its file system
|
||||||
path.
|
path.
|
||||||
@ -153,6 +173,47 @@ class FileNotebookManager(NotebookManager):
|
|||||||
nbpath = self.get_os_path(name, path=path)
|
nbpath = self.get_os_path(name, path=path)
|
||||||
return os.path.isfile(nbpath)
|
return os.path.isfile(nbpath)
|
||||||
|
|
||||||
|
# TODO: Remove this after we create the contents web service and directories are
|
||||||
|
# no longer listed by the notebook web service.
|
||||||
|
def list_dirs(self, path):
|
||||||
|
"""List the directories for a given API style path."""
|
||||||
|
path = path.strip('/')
|
||||||
|
os_path = self.get_os_path('', path)
|
||||||
|
if not os.path.isdir(os_path) or is_hidden(os_path, self.notebook_dir):
|
||||||
|
raise web.HTTPError(404, u'directory does not exist: %r' % os_path)
|
||||||
|
dir_names = os.listdir(os_path)
|
||||||
|
dirs = []
|
||||||
|
for name in dir_names:
|
||||||
|
os_path = self.get_os_path(name, path)
|
||||||
|
if os.path.isdir(os_path) and not is_hidden(os_path, self.notebook_dir):
|
||||||
|
try:
|
||||||
|
model = self.get_dir_model(name, path)
|
||||||
|
except IOError:
|
||||||
|
pass
|
||||||
|
dirs.append(model)
|
||||||
|
dirs = sorted(dirs, key=lambda item: item['name'])
|
||||||
|
return dirs
|
||||||
|
|
||||||
|
# TODO: Remove this after we create the contents web service and directories are
|
||||||
|
# no longer listed by the notebook web service.
|
||||||
|
def get_dir_model(self, name, path=''):
|
||||||
|
"""Get the directory model given a directory name and its API style path"""
|
||||||
|
path = path.strip('/')
|
||||||
|
os_path = self.get_os_path(name, path)
|
||||||
|
if not os.path.isdir(os_path):
|
||||||
|
raise IOError('directory does not exist: %r' % os_path)
|
||||||
|
info = os.stat(os_path)
|
||||||
|
last_modified = tz.utcfromtimestamp(info.st_mtime)
|
||||||
|
created = tz.utcfromtimestamp(info.st_ctime)
|
||||||
|
# Create the notebook model.
|
||||||
|
model ={}
|
||||||
|
model['name'] = name
|
||||||
|
model['path'] = path
|
||||||
|
model['last_modified'] = last_modified
|
||||||
|
model['created'] = created
|
||||||
|
model['type'] = 'directory'
|
||||||
|
return model
|
||||||
|
|
||||||
def list_notebooks(self, path):
|
def list_notebooks(self, path):
|
||||||
"""Returns a list of dictionaries that are the standard model
|
"""Returns a list of dictionaries that are the standard model
|
||||||
for all notebooks in the relative 'path'.
|
for all notebooks in the relative 'path'.
|
||||||
@ -170,10 +231,7 @@ class FileNotebookManager(NotebookManager):
|
|||||||
"""
|
"""
|
||||||
path = path.strip('/')
|
path = path.strip('/')
|
||||||
notebook_names = self.get_notebook_names(path)
|
notebook_names = self.get_notebook_names(path)
|
||||||
notebooks = []
|
notebooks = [self.get_notebook_model(name, path, content=False) for name in notebook_names]
|
||||||
for name in notebook_names:
|
|
||||||
model = self.get_notebook_model(name, path, content=False)
|
|
||||||
notebooks.append(model)
|
|
||||||
notebooks = sorted(notebooks, key=lambda item: item['name'])
|
notebooks = sorted(notebooks, key=lambda item: item['name'])
|
||||||
return notebooks
|
return notebooks
|
||||||
|
|
||||||
@ -207,6 +265,7 @@ class FileNotebookManager(NotebookManager):
|
|||||||
model['path'] = path
|
model['path'] = path
|
||||||
model['last_modified'] = last_modified
|
model['last_modified'] = last_modified
|
||||||
model['created'] = created
|
model['created'] = created
|
||||||
|
model['type'] = 'notebook'
|
||||||
if content:
|
if content:
|
||||||
with io.open(os_path, 'r', encoding='utf-8') as f:
|
with io.open(os_path, 'r', encoding='utf-8') as f:
|
||||||
try:
|
try:
|
||||||
@ -223,7 +282,7 @@ class FileNotebookManager(NotebookManager):
|
|||||||
|
|
||||||
if 'content' not in model:
|
if 'content' not in model:
|
||||||
raise web.HTTPError(400, u'No notebook JSON data provided')
|
raise web.HTTPError(400, u'No notebook JSON data provided')
|
||||||
|
|
||||||
# One checkpoint should always exist
|
# One checkpoint should always exist
|
||||||
if self.notebook_exists(name, path) and not self.list_checkpoints(name, path):
|
if self.notebook_exists(name, path) and not self.list_checkpoints(name, path):
|
||||||
self.create_checkpoint(name, path)
|
self.create_checkpoint(name, path)
|
||||||
|
@ -69,8 +69,18 @@ class NotebookHandler(IPythonHandler):
|
|||||||
nbm = self.notebook_manager
|
nbm = self.notebook_manager
|
||||||
# Check to see if a notebook name was given
|
# Check to see if a notebook name was given
|
||||||
if name is None:
|
if name is None:
|
||||||
# List notebooks in 'path'
|
# TODO: Remove this after we create the contents web service and directories are
|
||||||
notebooks = nbm.list_notebooks(path)
|
# no longer listed by the notebook web service. This should only handle notebooks
|
||||||
|
# and not directories.
|
||||||
|
dirs = nbm.list_dirs(path)
|
||||||
|
notebooks = []
|
||||||
|
index = []
|
||||||
|
for nb in nbm.list_notebooks(path):
|
||||||
|
if nb['name'].lower() == 'index.ipynb':
|
||||||
|
index.append(nb)
|
||||||
|
else:
|
||||||
|
notebooks.append(nb)
|
||||||
|
notebooks = index + dirs + notebooks
|
||||||
self.finish(json.dumps(notebooks, default=date_default))
|
self.finish(json.dumps(notebooks, default=date_default))
|
||||||
return
|
return
|
||||||
# get and return notebook representation
|
# get and return notebook representation
|
||||||
|
@ -82,7 +82,24 @@ class NotebookManager(LoggingConfigurable):
|
|||||||
Whether the path does indeed exist.
|
Whether the path does indeed exist.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def is_hidden(self, path):
|
||||||
|
"""Does the API style path correspond to a hidden directory or file?
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
path : string
|
||||||
|
The path to check. This is an API path (`/` separated,
|
||||||
|
relative to base notebook-dir).
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
exists : bool
|
||||||
|
Whether the path is hidden.
|
||||||
|
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
def _notebook_dir_changed(self, name, old, new):
|
def _notebook_dir_changed(self, name, old, new):
|
||||||
"""Do a bit of validation of the notebook dir."""
|
"""Do a bit of validation of the notebook dir."""
|
||||||
if not os.path.isabs(new):
|
if not os.path.isabs(new):
|
||||||
@ -112,6 +129,26 @@ class NotebookManager(LoggingConfigurable):
|
|||||||
"""
|
"""
|
||||||
return basename
|
return basename
|
||||||
|
|
||||||
|
# TODO: Remove this after we create the contents web service and directories are
|
||||||
|
# no longer listed by the notebook web service.
|
||||||
|
def list_dirs(self, path):
|
||||||
|
"""List the directory models for a given API style path."""
|
||||||
|
raise NotImplementedError('must be implemented in a subclass')
|
||||||
|
|
||||||
|
# TODO: Remove this after we create the contents web service and directories are
|
||||||
|
# no longer listed by the notebook web service.
|
||||||
|
def get_dir_model(self, name, path=''):
|
||||||
|
"""Get the directory model given a directory name and its API style path.
|
||||||
|
|
||||||
|
The keys in the model should be:
|
||||||
|
* name
|
||||||
|
* path
|
||||||
|
* last_modified
|
||||||
|
* created
|
||||||
|
* type='directory'
|
||||||
|
"""
|
||||||
|
raise NotImplementedError('must be implemented in a subclass')
|
||||||
|
|
||||||
def list_notebooks(self, path=''):
|
def list_notebooks(self, path=''):
|
||||||
"""Return a list of notebook dicts without content.
|
"""Return a list of notebook dicts without content.
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ from IPython.html.utils import url_path_join
|
|||||||
from ..filenbmanager import FileNotebookManager
|
from ..filenbmanager import FileNotebookManager
|
||||||
from ..nbmanager import NotebookManager
|
from ..nbmanager import NotebookManager
|
||||||
|
|
||||||
|
|
||||||
class TestFileNotebookManager(TestCase):
|
class TestFileNotebookManager(TestCase):
|
||||||
|
|
||||||
def test_nb_dir(self):
|
def test_nb_dir(self):
|
||||||
@ -67,7 +68,7 @@ class TestNotebookManager(TestCase):
|
|||||||
try:
|
try:
|
||||||
os.makedirs(os_path)
|
os.makedirs(os_path)
|
||||||
except OSError:
|
except OSError:
|
||||||
print("Directory already exists.")
|
print("Directory already exists: %r" % os_path)
|
||||||
|
|
||||||
def test_create_notebook_model(self):
|
def test_create_notebook_model(self):
|
||||||
with TemporaryDirectory() as td:
|
with TemporaryDirectory() as td:
|
||||||
|
@ -21,6 +21,12 @@ from IPython.utils import py3compat
|
|||||||
from IPython.utils.data import uniq_stable
|
from IPython.utils.data import uniq_stable
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: Remove this after we create the contents web service and directories are
|
||||||
|
# no longer listed by the notebook web service.
|
||||||
|
def notebooks_only(nb_list):
|
||||||
|
return [nb for nb in nb_list if nb['type']=='notebook']
|
||||||
|
|
||||||
|
|
||||||
class NBAPI(object):
|
class NBAPI(object):
|
||||||
"""Wrapper for notebook API calls."""
|
"""Wrapper for notebook API calls."""
|
||||||
def __init__(self, base_url):
|
def __init__(self, base_url):
|
||||||
@ -125,25 +131,25 @@ class APITest(NotebookTestBase):
|
|||||||
os.unlink(pjoin(nbdir, 'inroot.ipynb'))
|
os.unlink(pjoin(nbdir, 'inroot.ipynb'))
|
||||||
|
|
||||||
def test_list_notebooks(self):
|
def test_list_notebooks(self):
|
||||||
nbs = self.nb_api.list().json()
|
nbs = notebooks_only(self.nb_api.list().json())
|
||||||
self.assertEqual(len(nbs), 1)
|
self.assertEqual(len(nbs), 1)
|
||||||
self.assertEqual(nbs[0]['name'], 'inroot.ipynb')
|
self.assertEqual(nbs[0]['name'], 'inroot.ipynb')
|
||||||
|
|
||||||
nbs = self.nb_api.list('/Directory with spaces in/').json()
|
nbs = notebooks_only(self.nb_api.list('/Directory with spaces in/').json())
|
||||||
self.assertEqual(len(nbs), 1)
|
self.assertEqual(len(nbs), 1)
|
||||||
self.assertEqual(nbs[0]['name'], 'inspace.ipynb')
|
self.assertEqual(nbs[0]['name'], 'inspace.ipynb')
|
||||||
|
|
||||||
nbs = self.nb_api.list(u'/unicodé/').json()
|
nbs = notebooks_only(self.nb_api.list(u'/unicodé/').json())
|
||||||
self.assertEqual(len(nbs), 1)
|
self.assertEqual(len(nbs), 1)
|
||||||
self.assertEqual(nbs[0]['name'], 'innonascii.ipynb')
|
self.assertEqual(nbs[0]['name'], 'innonascii.ipynb')
|
||||||
self.assertEqual(nbs[0]['path'], u'unicodé')
|
self.assertEqual(nbs[0]['path'], u'unicodé')
|
||||||
|
|
||||||
nbs = self.nb_api.list('/foo/bar/').json()
|
nbs = notebooks_only(self.nb_api.list('/foo/bar/').json())
|
||||||
self.assertEqual(len(nbs), 1)
|
self.assertEqual(len(nbs), 1)
|
||||||
self.assertEqual(nbs[0]['name'], 'baz.ipynb')
|
self.assertEqual(nbs[0]['name'], 'baz.ipynb')
|
||||||
self.assertEqual(nbs[0]['path'], 'foo/bar')
|
self.assertEqual(nbs[0]['path'], 'foo/bar')
|
||||||
|
|
||||||
nbs = self.nb_api.list('foo').json()
|
nbs = notebooks_only(self.nb_api.list('foo').json())
|
||||||
self.assertEqual(len(nbs), 4)
|
self.assertEqual(len(nbs), 4)
|
||||||
nbnames = { normalize('NFC', n['name']) for n in nbs }
|
nbnames = { normalize('NFC', n['name']) for n in nbs }
|
||||||
expected = [ u'a.ipynb', u'b.ipynb', u'name with spaces.ipynb', u'unicodé.ipynb']
|
expected = [ u'a.ipynb', u'b.ipynb', u'name with spaces.ipynb', u'unicodé.ipynb']
|
||||||
@ -231,7 +237,7 @@ class APITest(NotebookTestBase):
|
|||||||
self.assertEqual(resp.status_code, 204)
|
self.assertEqual(resp.status_code, 204)
|
||||||
|
|
||||||
for d in self.dirs + ['/']:
|
for d in self.dirs + ['/']:
|
||||||
nbs = self.nb_api.list(d).json()
|
nbs = notebooks_only(self.nb_api.list(d).json())
|
||||||
self.assertEqual(len(nbs), 0)
|
self.assertEqual(len(nbs), 0)
|
||||||
|
|
||||||
def test_rename(self):
|
def test_rename(self):
|
||||||
@ -240,7 +246,7 @@ class APITest(NotebookTestBase):
|
|||||||
self.assertEqual(resp.json()['name'], 'z.ipynb')
|
self.assertEqual(resp.json()['name'], 'z.ipynb')
|
||||||
assert os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'z.ipynb'))
|
assert os.path.isfile(pjoin(self.notebook_dir.name, 'foo', 'z.ipynb'))
|
||||||
|
|
||||||
nbs = self.nb_api.list('foo').json()
|
nbs = notebooks_only(self.nb_api.list('foo').json())
|
||||||
nbnames = set(n['name'] for n in nbs)
|
nbnames = set(n['name'] for n in nbs)
|
||||||
self.assertIn('z.ipynb', nbnames)
|
self.assertIn('z.ipynb', nbnames)
|
||||||
self.assertNotIn('a.ipynb', nbnames)
|
self.assertNotIn('a.ipynb', nbnames)
|
||||||
|
25
IPython/html/static/style/ipython.min.css
vendored
25
IPython/html/static/style/ipython.min.css
vendored
@ -28,22 +28,29 @@ div.traceback-wrapper{text-align:left;max-width:800px;margin:auto}
|
|||||||
.center-nav{display:inline-block;margin-bottom:-4px}
|
.center-nav{display:inline-block;margin-bottom:-4px}
|
||||||
.alternate_upload{background-color:none;display:inline}
|
.alternate_upload{background-color:none;display:inline}
|
||||||
.alternate_upload.form{padding:0;margin:0}
|
.alternate_upload.form{padding:0;margin:0}
|
||||||
.alternate_upload input.fileinput{background-color:#f00;position:relative;opacity:0;z-index:2;width:295px;margin-left:163px;cursor:pointer}
|
.alternate_upload input.fileinput{background-color:#f00;position:relative;opacity:0;z-index:2;width:295px;margin-left:163px;cursor:pointer;height:26px}
|
||||||
.list_toolbar{padding:5px;height:25px;line-height:25px}
|
ul#tabs{margin-bottom:4px}
|
||||||
.toolbar_info{float:left}
|
ul#tabs a{padding-top:4px;padding-bottom:4px}
|
||||||
.toolbar_buttons{float:right}
|
ul.breadcrumb a:focus,ul.breadcrumb a:hover{text-decoration:none}
|
||||||
|
ul.breadcrumb i.icon-home{font-size:16px;margin-right:4px}
|
||||||
|
ul.breadcrumb span{color:#5e5e5e}
|
||||||
|
.list_toolbar{padding:4px 0 4px 0}
|
||||||
|
.list_toolbar [class*="span"]{min-height:26px}
|
||||||
.list_header{font-weight:bold}
|
.list_header{font-weight:bold}
|
||||||
.list_container{margin-top:16px;margin-bottom:16px;border:1px solid #ababab;border-radius:4px}
|
.list_container{margin-top:4px;margin-bottom:20px;border:1px solid #ababab;border-radius:4px}
|
||||||
.list_container>div{border-bottom:1px solid #ababab}.list_container>div:hover .list-item{background-color:#f00}
|
.list_container>div{border-bottom:1px solid #ababab}.list_container>div:hover .list-item{background-color:#f00}
|
||||||
.list_container>div:last-child{border:none}
|
.list_container>div:last-child{border:none}
|
||||||
.list_item:hover .list_item{background-color:#ddd}
|
.list_item:hover .list_item{background-color:#ddd}
|
||||||
.item_name{line-height:24px}
|
|
||||||
.list_container>div>span,.list_container>div>div{padding:8px}
|
|
||||||
.list_item a{text-decoration:none}
|
.list_item a{text-decoration:none}
|
||||||
input.nbname_input{height:15px}
|
.list_header>div,.list_item>div{padding-top:4px;padding-bottom:4px;padding-left:7px;padding-right:7px;height:22px;line-height:22px}
|
||||||
|
.item_name{line-height:22px;height:26px}
|
||||||
|
.item_icon{font-size:14px;color:#5e5e5e;margin-right:7px}
|
||||||
|
.item_buttons{line-height:1em}
|
||||||
|
.toolbar_info{height:26px;line-height:26px}
|
||||||
|
input.nbname_input,input.engine_num_input{padding-top:3px;padding-bottom:3px;height:14px;line-height:14px;margin:0}
|
||||||
|
input.engine_num_input{width:60px}
|
||||||
.highlight_text{color:#00f}
|
.highlight_text{color:#00f}
|
||||||
#project_name>.breadcrumb{padding:0;margin-bottom:0;background-color:transparent;font-weight:bold}
|
#project_name>.breadcrumb{padding:0;margin-bottom:0;background-color:transparent;font-weight:bold}
|
||||||
input.engine_num_input{height:20px;margin-bottom:2px;padding-top:0;padding-bottom:0;width:60px}
|
|
||||||
.ansibold{font-weight:bold}
|
.ansibold{font-weight:bold}
|
||||||
.ansiblack{color:#000}
|
.ansiblack{color:#000}
|
||||||
.ansired{color:#8b0000}
|
.ansired{color:#8b0000}
|
||||||
|
25
IPython/html/static/style/style.min.css
vendored
25
IPython/html/static/style/style.min.css
vendored
@ -1305,22 +1305,29 @@ span#login_widget{float:right}
|
|||||||
.center-nav{display:inline-block;margin-bottom:-4px}
|
.center-nav{display:inline-block;margin-bottom:-4px}
|
||||||
.alternate_upload{background-color:none;display:inline}
|
.alternate_upload{background-color:none;display:inline}
|
||||||
.alternate_upload.form{padding:0;margin:0}
|
.alternate_upload.form{padding:0;margin:0}
|
||||||
.alternate_upload input.fileinput{background-color:#f00;position:relative;opacity:0;z-index:2;width:295px;margin-left:163px;cursor:pointer}
|
.alternate_upload input.fileinput{background-color:#f00;position:relative;opacity:0;z-index:2;width:295px;margin-left:163px;cursor:pointer;height:26px}
|
||||||
.list_toolbar{padding:5px;height:25px;line-height:25px}
|
ul#tabs{margin-bottom:4px}
|
||||||
.toolbar_info{float:left}
|
ul#tabs a{padding-top:4px;padding-bottom:4px}
|
||||||
.toolbar_buttons{float:right}
|
ul.breadcrumb a:focus,ul.breadcrumb a:hover{text-decoration:none}
|
||||||
|
ul.breadcrumb i.icon-home{font-size:16px;margin-right:4px}
|
||||||
|
ul.breadcrumb span{color:#5e5e5e}
|
||||||
|
.list_toolbar{padding:4px 0 4px 0}
|
||||||
|
.list_toolbar [class*="span"]{min-height:26px}
|
||||||
.list_header{font-weight:bold}
|
.list_header{font-weight:bold}
|
||||||
.list_container{margin-top:16px;margin-bottom:16px;border:1px solid #ababab;border-radius:4px}
|
.list_container{margin-top:4px;margin-bottom:20px;border:1px solid #ababab;border-radius:4px}
|
||||||
.list_container>div{border-bottom:1px solid #ababab}.list_container>div:hover .list-item{background-color:#f00}
|
.list_container>div{border-bottom:1px solid #ababab}.list_container>div:hover .list-item{background-color:#f00}
|
||||||
.list_container>div:last-child{border:none}
|
.list_container>div:last-child{border:none}
|
||||||
.list_item:hover .list_item{background-color:#ddd}
|
.list_item:hover .list_item{background-color:#ddd}
|
||||||
.item_name{line-height:24px}
|
|
||||||
.list_container>div>span,.list_container>div>div{padding:8px}
|
|
||||||
.list_item a{text-decoration:none}
|
.list_item a{text-decoration:none}
|
||||||
input.nbname_input{height:15px}
|
.list_header>div,.list_item>div{padding-top:4px;padding-bottom:4px;padding-left:7px;padding-right:7px;height:22px;line-height:22px}
|
||||||
|
.item_name{line-height:22px;height:26px}
|
||||||
|
.item_icon{font-size:14px;color:#5e5e5e;margin-right:7px}
|
||||||
|
.item_buttons{line-height:1em}
|
||||||
|
.toolbar_info{height:26px;line-height:26px}
|
||||||
|
input.nbname_input,input.engine_num_input{padding-top:3px;padding-bottom:3px;height:14px;line-height:14px;margin:0}
|
||||||
|
input.engine_num_input{width:60px}
|
||||||
.highlight_text{color:#00f}
|
.highlight_text{color:#00f}
|
||||||
#project_name>.breadcrumb{padding:0;margin-bottom:0;background-color:transparent;font-weight:bold}
|
#project_name>.breadcrumb{padding:0;margin-bottom:0;background-color:transparent;font-weight:bold}
|
||||||
input.engine_num_input{height:20px;margin-bottom:2px;padding-top:0;padding-bottom:0;width:60px}
|
|
||||||
.ansibold{font-weight:bold}
|
.ansibold{font-weight:bold}
|
||||||
.ansiblack{color:#000}
|
.ansiblack{color:#000}
|
||||||
.ansired{color:#8b0000}
|
.ansired{color:#8b0000}
|
||||||
|
@ -100,16 +100,16 @@ var IPython = (function (IPython) {
|
|||||||
|
|
||||||
ClusterItem.prototype.state_stopped = function () {
|
ClusterItem.prototype.state_stopped = function () {
|
||||||
var that = this;
|
var that = this;
|
||||||
var profile_col = $('<span/>').addClass('profile_col span4').text(this.data.profile);
|
var profile_col = $('<div/>').addClass('profile_col span4').text(this.data.profile);
|
||||||
var status_col = $('<span/>').addClass('status_col span3').text('stopped');
|
var status_col = $('<div/>').addClass('status_col span3').text('stopped');
|
||||||
var engines_col = $('<span/>').addClass('engine_col span3');
|
var engines_col = $('<div/>').addClass('engine_col span3');
|
||||||
var input = $('<input/>').attr('type','number')
|
var input = $('<input/>').attr('type','number')
|
||||||
.attr('min',1)
|
.attr('min',1)
|
||||||
.attr('size',3)
|
.attr('size',3)
|
||||||
.addClass('engine_num_input');
|
.addClass('engine_num_input');
|
||||||
engines_col.append(input);
|
engines_col.append(input);
|
||||||
var start_button = $('<button/>').addClass("btn btn-mini").text("Start");
|
var start_button = $('<button/>').addClass("btn btn-mini").text("Start");
|
||||||
var action_col = $('<span/>').addClass('action_col span2').append(
|
var action_col = $('<div/>').addClass('action_col span2').append(
|
||||||
$("<span/>").addClass("item_buttons btn-group").append(
|
$("<span/>").addClass("item_buttons btn-group").append(
|
||||||
start_button
|
start_button
|
||||||
)
|
)
|
||||||
@ -151,11 +151,11 @@ var IPython = (function (IPython) {
|
|||||||
|
|
||||||
ClusterItem.prototype.state_running = function () {
|
ClusterItem.prototype.state_running = function () {
|
||||||
var that = this;
|
var that = this;
|
||||||
var profile_col = $('<span/>').addClass('profile_col span4').text(this.data.profile);
|
var profile_col = $('<div/>').addClass('profile_col span4').text(this.data.profile);
|
||||||
var status_col = $('<span/>').addClass('status_col span3').text('running');
|
var status_col = $('<div/>').addClass('status_col span3').text('running');
|
||||||
var engines_col = $('<span/>').addClass('engines_col span3').text(this.data.n);
|
var engines_col = $('<div/>').addClass('engines_col span3').text(this.data.n);
|
||||||
var stop_button = $('<button/>').addClass("btn btn-mini").text("Stop");
|
var stop_button = $('<button/>').addClass("btn btn-mini").text("Stop");
|
||||||
var action_col = $('<span/>').addClass('action_col span2').append(
|
var action_col = $('<div/>').addClass('action_col span2').append(
|
||||||
$("<span/>").addClass("item_buttons btn-group").append(
|
$("<span/>").addClass("item_buttons btn-group").append(
|
||||||
stop_button
|
stop_button
|
||||||
)
|
)
|
||||||
|
@ -70,11 +70,10 @@ var IPython = (function (IPython) {
|
|||||||
var reader = new FileReader();
|
var reader = new FileReader();
|
||||||
reader.readAsText(f);
|
reader.readAsText(f);
|
||||||
var name_and_ext = utils.splitext(f.name);
|
var name_and_ext = utils.splitext(f.name);
|
||||||
var nbname = name_and_ext[0];
|
|
||||||
var file_ext = name_and_ext[1];
|
var file_ext = name_and_ext[1];
|
||||||
if (file_ext === '.ipynb') {
|
if (file_ext === '.ipynb') {
|
||||||
var item = that.new_notebook_item(0);
|
var item = that.new_notebook_item(0);
|
||||||
that.add_name_input(nbname, item);
|
that.add_name_input(f.name, item);
|
||||||
// Store the notebook item in the reader so we can use it later
|
// Store the notebook item in the reader so we can use it later
|
||||||
// to know which item it belongs to.
|
// to know which item it belongs to.
|
||||||
$(reader).data('item', item);
|
$(reader).data('item', item);
|
||||||
@ -167,26 +166,37 @@ var IPython = (function (IPython) {
|
|||||||
if (param !== undefined && param.msg) {
|
if (param !== undefined && param.msg) {
|
||||||
message = param.msg;
|
message = param.msg;
|
||||||
}
|
}
|
||||||
|
var item = null;
|
||||||
var len = data.length;
|
var len = data.length;
|
||||||
this.clear_list();
|
this.clear_list();
|
||||||
if (len === 0) {
|
if (len === 0) {
|
||||||
$(this.new_notebook_item(0))
|
item = this.new_notebook_item(0);
|
||||||
.append(
|
var span12 = item.children().first();
|
||||||
$('<div style="margin:auto;text-align:center;color:grey"/>')
|
span12.empty();
|
||||||
.text(message)
|
span12.append($('<div style="margin:auto;text-align:center;color:grey"/>').text(message));
|
||||||
);
|
}
|
||||||
|
var path = this.notebookPath();
|
||||||
|
var offset = 0;
|
||||||
|
if (path !== '') {
|
||||||
|
item = this.new_notebook_item(0);
|
||||||
|
this.add_dir(path, '..', item);
|
||||||
|
offset = 1;
|
||||||
}
|
}
|
||||||
for (var i=0; i<len; i++) {
|
for (var i=0; i<len; i++) {
|
||||||
var name = data[i].name;
|
if (data[i].type === 'directory') {
|
||||||
var path = this.notebookPath();
|
var name = data[i].name;
|
||||||
var nbname = utils.splitext(name)[0];
|
item = this.new_notebook_item(i+offset);
|
||||||
var item = this.new_notebook_item(i);
|
this.add_dir(path, name, item);
|
||||||
this.add_link(path, nbname, item);
|
|
||||||
name = utils.url_path_join(path, name);
|
|
||||||
if(this.sessions[name] === undefined){
|
|
||||||
this.add_delete_button(item);
|
|
||||||
} else {
|
} else {
|
||||||
this.add_shutdown_button(item,this.sessions[name]);
|
var name = data[i].name;
|
||||||
|
item = this.new_notebook_item(i+offset);
|
||||||
|
this.add_link(path, name, item);
|
||||||
|
name = utils.url_path_join(path, name);
|
||||||
|
if(this.sessions[name] === undefined){
|
||||||
|
this.add_delete_button(item);
|
||||||
|
} else {
|
||||||
|
this.add_shutdown_button(item,this.sessions[name]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -197,6 +207,8 @@ var IPython = (function (IPython) {
|
|||||||
// item.addClass('list_item ui-widget ui-widget-content ui-helper-clearfix');
|
// item.addClass('list_item ui-widget ui-widget-content ui-helper-clearfix');
|
||||||
// item.css('border-top-style','none');
|
// item.css('border-top-style','none');
|
||||||
item.append($("<div/>").addClass("span12").append(
|
item.append($("<div/>").addClass("span12").append(
|
||||||
|
$('<i/>').addClass('item_icon')
|
||||||
|
).append(
|
||||||
$("<a/>").addClass("item_link").append(
|
$("<a/>").addClass("item_link").append(
|
||||||
$("<span/>").addClass("item_name")
|
$("<span/>").addClass("item_name")
|
||||||
)
|
)
|
||||||
@ -213,17 +225,35 @@ var IPython = (function (IPython) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
NotebookList.prototype.add_dir = function (path, name, item) {
|
||||||
|
item.data('name', name);
|
||||||
|
item.data('path', path);
|
||||||
|
item.find(".item_name").text(name);
|
||||||
|
item.find(".item_icon").addClass('icon-folder-open');
|
||||||
|
item.find("a.item_link")
|
||||||
|
.attr('href',
|
||||||
|
utils.url_join_encode(
|
||||||
|
this.baseProjectUrl(),
|
||||||
|
"tree",
|
||||||
|
path,
|
||||||
|
name
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
NotebookList.prototype.add_link = function (path, nbname, item) {
|
NotebookList.prototype.add_link = function (path, nbname, item) {
|
||||||
item.data('nbname', nbname);
|
item.data('nbname', nbname);
|
||||||
item.data('path', path);
|
item.data('path', path);
|
||||||
item.find(".item_name").text(nbname);
|
item.find(".item_name").text(nbname);
|
||||||
|
item.find(".item_icon").addClass('icon-book');
|
||||||
item.find("a.item_link")
|
item.find("a.item_link")
|
||||||
.attr('href',
|
.attr('href',
|
||||||
utils.url_join_encode(
|
utils.url_join_encode(
|
||||||
this.baseProjectUrl(),
|
this.baseProjectUrl(),
|
||||||
"notebooks",
|
"notebooks",
|
||||||
path,
|
path,
|
||||||
nbname + ".ipynb"
|
nbname
|
||||||
)
|
)
|
||||||
).attr('target','_blank');
|
).attr('target','_blank');
|
||||||
};
|
};
|
||||||
@ -231,10 +261,11 @@ var IPython = (function (IPython) {
|
|||||||
|
|
||||||
NotebookList.prototype.add_name_input = function (nbname, item) {
|
NotebookList.prototype.add_name_input = function (nbname, item) {
|
||||||
item.data('nbname', nbname);
|
item.data('nbname', nbname);
|
||||||
|
item.find(".item_icon").addClass('icon-book');
|
||||||
item.find(".item_name").empty().append(
|
item.find(".item_name").empty().append(
|
||||||
$('<input/>')
|
$('<input/>')
|
||||||
.addClass("nbname_input")
|
.addClass("nbname_input")
|
||||||
.attr('value', nbname)
|
.attr('value', utils.splitext(nbname)[0])
|
||||||
.attr('size', '30')
|
.attr('size', '30')
|
||||||
.attr('type', 'text')
|
.attr('type', 'text')
|
||||||
);
|
);
|
||||||
@ -248,7 +279,7 @@ var IPython = (function (IPython) {
|
|||||||
|
|
||||||
NotebookList.prototype.add_shutdown_button = function (item, session) {
|
NotebookList.prototype.add_shutdown_button = function (item, session) {
|
||||||
var that = this;
|
var that = this;
|
||||||
var shutdown_button = $("<button/>").text("Shutdown").addClass("btn btn-mini").
|
var shutdown_button = $("<button/>").text("Shutdown").addClass("btn btn-mini btn-danger").
|
||||||
click(function (e) {
|
click(function (e) {
|
||||||
var settings = {
|
var settings = {
|
||||||
processData : false,
|
processData : false,
|
||||||
@ -303,7 +334,7 @@ var IPython = (function (IPython) {
|
|||||||
notebooklist.baseProjectUrl(),
|
notebooklist.baseProjectUrl(),
|
||||||
'api/notebooks',
|
'api/notebooks',
|
||||||
notebooklist.notebookPath(),
|
notebooklist.notebookPath(),
|
||||||
nbname + '.ipynb'
|
nbname
|
||||||
);
|
);
|
||||||
$.ajax(url, settings);
|
$.ajax(url, settings);
|
||||||
}
|
}
|
||||||
@ -323,6 +354,9 @@ var IPython = (function (IPython) {
|
|||||||
.addClass('btn btn-primary btn-mini upload_button')
|
.addClass('btn btn-primary btn-mini upload_button')
|
||||||
.click(function (e) {
|
.click(function (e) {
|
||||||
var nbname = item.find('.item_name > input').val();
|
var nbname = item.find('.item_name > input').val();
|
||||||
|
if (nbname.slice(nbname.length-6, nbname.length) != ".ipynb") {
|
||||||
|
nbname = nbname + ".ipynb";
|
||||||
|
}
|
||||||
var path = that.notebookPath();
|
var path = that.notebookPath();
|
||||||
var nbdata = item.data('nbdata');
|
var nbdata = item.data('nbdata');
|
||||||
var content_type = 'application/json';
|
var content_type = 'application/json';
|
||||||
@ -349,7 +383,7 @@ var IPython = (function (IPython) {
|
|||||||
that.baseProjectUrl(),
|
that.baseProjectUrl(),
|
||||||
'api/notebooks',
|
'api/notebooks',
|
||||||
that.notebookPath(),
|
that.notebookPath(),
|
||||||
nbname + '.ipynb'
|
nbname
|
||||||
);
|
);
|
||||||
$.ajax(url, settings);
|
$.ajax(url, settings);
|
||||||
return false;
|
return false;
|
||||||
|
@ -22,4 +22,5 @@
|
|||||||
width: 295px;
|
width: 295px;
|
||||||
margin-left:163px;
|
margin-left:163px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
height: 26px;
|
||||||
}
|
}
|
||||||
|
@ -5,21 +5,43 @@
|
|||||||
* Author: IPython Development Team
|
* Author: IPython Development Team
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#tabs {
|
@dashboard_tb_pad: 4px;
|
||||||
|
@dashboard_lr_pad: 7px;
|
||||||
|
// These are the total heights of the Bootstrap small and mini buttons. These values
|
||||||
|
// are not less variables so we have to track them statically.
|
||||||
|
@btn_small_height: 26px;
|
||||||
|
@btn_mini_height: 22px;
|
||||||
|
@dark_dashboard_color: darken(@border_color, 30%);
|
||||||
|
|
||||||
|
ul#tabs {
|
||||||
|
margin-bottom: @dashboard_tb_pad;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul#tabs a {
|
||||||
|
padding-top: @dashboard_tb_pad;
|
||||||
|
padding-bottom: @dashboard_tb_pad;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.breadcrumb {
|
||||||
|
a:focus, a:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
i.icon-home {
|
||||||
|
font-size: 16px;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: @dark_dashboard_color;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.list_toolbar {
|
.list_toolbar {
|
||||||
padding: 5px;
|
padding: @dashboard_tb_pad 0 @dashboard_tb_pad 0;
|
||||||
height: 25px;
|
|
||||||
line-height: 25px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar_info {
|
.list_toolbar [class*="span"] {
|
||||||
float: left;
|
min-height: @btn_small_height;
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar_buttons {
|
|
||||||
float: right;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.list_header {
|
.list_header {
|
||||||
@ -27,8 +49,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.list_container {
|
.list_container {
|
||||||
margin-top: 16px;
|
margin-top: @dashboard_tb_pad;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 5*@dashboard_tb_pad;
|
||||||
border: 1px solid @border_color;
|
border: 1px solid @border_color;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
@ -48,42 +70,55 @@
|
|||||||
&:hover .list_item {
|
&:hover .list_item {
|
||||||
background-color: #ddd;
|
background-color: #ddd;
|
||||||
};
|
};
|
||||||
|
a {text-decoration: none;}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list_header>div, .list_item>div {
|
||||||
|
padding-top: @dashboard_tb_pad;
|
||||||
|
padding-bottom: @dashboard_tb_pad;
|
||||||
|
padding-left: @dashboard_lr_pad;
|
||||||
|
padding-right: @dashboard_lr_pad;
|
||||||
|
height: @btn_mini_height;
|
||||||
|
line-height: @btn_mini_height;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item_name {
|
.item_name {
|
||||||
line-height: 24px;
|
line-height: @btn_mini_height;
|
||||||
|
height: @btn_small_height;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list_container > div > span, .list_container > div > div {
|
.item_icon {
|
||||||
padding: 8px;
|
font-size: 14px;
|
||||||
|
color: @dark_dashboard_color;
|
||||||
|
margin-right: @dashboard_lr_pad;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.item_buttons {
|
||||||
.list_item a {
|
line-height: 1em;
|
||||||
text-decoration: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile_col {
|
.toolbar_info {
|
||||||
|
height: @btn_small_height;
|
||||||
|
line-height: @btn_small_height;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status_col {
|
input.nbname_input, input.engine_num_input {
|
||||||
|
// These settings give these inputs a height that matches @btn_mini_height = 22
|
||||||
|
padding-top: 3px;
|
||||||
|
padding-bottom: 3px;
|
||||||
|
height: 14px;
|
||||||
|
line-height: 14px;
|
||||||
|
margin: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.engines_col {
|
input.engine_num_input {
|
||||||
}
|
width: 60px;
|
||||||
|
|
||||||
.action_col {
|
|
||||||
}
|
|
||||||
|
|
||||||
input.nbname_input {
|
|
||||||
height: 15px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.highlight_text {
|
.highlight_text {
|
||||||
color: blue;
|
color: blue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#project_name > .breadcrumb {
|
#project_name > .breadcrumb {
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
margin-bottom: 0px;
|
margin-bottom: 0px;
|
||||||
@ -92,10 +127,4 @@ input.nbname_input {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input.engine_num_input {
|
|
||||||
height: 20px;
|
|
||||||
margin-bottom:2px;
|
|
||||||
padding-top:0;
|
|
||||||
padding-bottom:0;
|
|
||||||
width: 60px;
|
|
||||||
}
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{% extends "page.html" %}
|
{% extends "page.html" %}
|
||||||
|
|
||||||
{% block title %}IPython Dashboard{% endblock %}
|
{% block title %}{{page_title}}{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% block stylesheet %}
|
{% block stylesheet %}
|
||||||
@ -22,33 +22,38 @@ data-base-kernel-url="{{base_kernel_url}}"
|
|||||||
|
|
||||||
<div id="ipython-main-app" class="container">
|
<div id="ipython-main-app" class="container">
|
||||||
|
|
||||||
<div id="tabs" class="tabbable">
|
<div id="tab_content" class="tabbable">
|
||||||
<ul class="nav nav-tabs" id="tabs">
|
<ul id="tabs" class="nav nav-tabs">
|
||||||
<li class="active"><a href="#notebooks" data-toggle="tab">Notebooks</a></li>
|
<li class="active"><a href="#notebooks" data-toggle="tab">Notebooks</a></li>
|
||||||
<li><a href="#clusters" data-toggle="tab">Clusters</a></li>
|
<li><a href="#clusters" data-toggle="tab">Clusters</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
<div id="notebooks" class="tab-pane active">
|
<div id="notebooks" class="tab-pane active">
|
||||||
<div id="notebook_toolbar">
|
<div id="notebook_toolbar" class="row-fluid">
|
||||||
<form id='alternate_upload' class='alternate_upload' >
|
<div class="span8">
|
||||||
<span id="drag_info" style="position:absolute" >
|
<form id='alternate_upload' class='alternate_upload' >
|
||||||
To import a notebook, drag the file onto the listing below or <strong>click here</strong>.
|
<span id="drag_info" style="position:absolute" >
|
||||||
</span>
|
To import a notebook, drag the file onto the listing below or <strong>click here</strong>.
|
||||||
<input type="file" name="datafile" class="fileinput" multiple='multiple'>
|
</span>
|
||||||
</form>
|
<input type="file" name="datafile" class="fileinput" multiple='multiple'>
|
||||||
<span id="notebook_buttons">
|
</form>
|
||||||
<button id="refresh_notebook_list" title="Refresh notebook list" class="btn btn-small">Refresh</button>
|
</div>
|
||||||
<button id="new_notebook" title="Create new notebook" class="btn btn-small">New Notebook</button>
|
<div class="span4 clearfix">
|
||||||
</span>
|
<span id="notebook_buttons" class="pull-right">
|
||||||
|
<button id="new_notebook" title="Create new notebook" class="btn btn-small">New Notebook</button>
|
||||||
|
<button id="refresh_notebook_list" title="Refresh notebook list" class="btn btn-small"><i class="icon-refresh"></i></button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="notebook_list">
|
<div id="notebook_list">
|
||||||
<div id="notebook_list_header" class="row-fluid list_header">
|
<div id="notebook_list_header" class="row-fluid list_header">
|
||||||
<div id="project_name">
|
<div id="project_name">
|
||||||
<ul class="breadcrumb">
|
<ul class="breadcrumb">
|
||||||
{% for component in tree_url_path.strip('/').split('/') %}
|
<li><a href="{{breadcrumbs[0][0]}}"><i class="icon-home"></i></a><span>/</span></li>
|
||||||
<li>{{component}} <span>/</span></li>
|
{% for crumb in breadcrumbs[1:] %}
|
||||||
|
<li><a href="{{crumb[0]}}">{{crumb[1]}}</a> <span>/</span></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@ -58,20 +63,23 @@ data-base-kernel-url="{{base_kernel_url}}"
|
|||||||
|
|
||||||
<div id="clusters" class="tab-pane">
|
<div id="clusters" class="tab-pane">
|
||||||
|
|
||||||
<div id="cluster_toolbar">
|
<div id="cluster_toolbar" class="row-fluid">
|
||||||
<span id="cluster_list_info">IPython parallel computing clusters</span>
|
<div class="span8">
|
||||||
|
<span id="cluster_list_info">IPython parallel computing clusters</span>
|
||||||
<span id="cluster_buttons">
|
</div>
|
||||||
<button id="refresh_cluster_list" title="Refresh cluster list" class="btn btn-small">Refresh</button>
|
<div class="span4" class="clearfix">
|
||||||
</span>
|
<span id="cluster_buttons" class="pull-right">
|
||||||
|
<button id="refresh_cluster_list" title="Refresh cluster list" class="btn btn-small"><i class="icon-refresh"></i></button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="cluster_list">
|
<div id="cluster_list">
|
||||||
<div id="cluster_list_header" class="row-fluid list_header">
|
<div id="cluster_list_header" class="row-fluid list_header">
|
||||||
<span class="profile_col span4">profile</span>
|
<div class="profile_col span4">profile</div>
|
||||||
<span class="status_col span3">status</span>
|
<div class="status_col span3">status</div>
|
||||||
<span class="engines_col span3" title="Enter the number of engines to start or empty for default"># of engines</span>
|
<div class="engines_col span3" title="Enter the number of engines to start or empty for default"># of engines</div>
|
||||||
<span class="action_col span2">action</span>
|
<div class="action_col span2">action</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
37
IPython/html/tests/casperjs/test_cases/dashboard_nav.js
Normal file
37
IPython/html/tests/casperjs/test_cases/dashboard_nav.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
|
||||||
|
|
||||||
|
casper.get_list_items = function () {
|
||||||
|
return this.evaluate(function () {
|
||||||
|
return $.makeArray($('.item_link').map(function () {
|
||||||
|
return {
|
||||||
|
link: $(this).attr('href'),
|
||||||
|
label: $(this).find('.item_name').text()
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
casper.test_items = function (baseUrl) {
|
||||||
|
casper.then(function () {
|
||||||
|
var items = casper.get_list_items();
|
||||||
|
casper.each(items, function (self, item) {
|
||||||
|
if (!item.label.match('.ipynb$')) {
|
||||||
|
var followed_url = baseUrl+item.link;
|
||||||
|
if (!followed_url.match('/\.\.$')) {
|
||||||
|
casper.thenOpen(baseUrl+item.link, function () {
|
||||||
|
casper.wait_for_dashboard();
|
||||||
|
this.test.assertEquals(this.getCurrentUrl(), followed_url, 'Testing dashboard link: '+followed_url);
|
||||||
|
casper.test_items(baseUrl);
|
||||||
|
this.back();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
casper.dashboard_test(function () {
|
||||||
|
baseUrl = this.get_notebook_server()
|
||||||
|
casper.test_items(baseUrl);
|
||||||
|
})
|
||||||
|
|
@ -250,6 +250,34 @@ casper.notebook_test = function(test) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
casper.wait_for_dashboard = function () {
|
||||||
|
// Wait for the dashboard list to load.
|
||||||
|
casper.waitForSelector('.list_item');
|
||||||
|
}
|
||||||
|
|
||||||
|
casper.open_dashboard = function () {
|
||||||
|
// Start casper by opening the dashboard page.
|
||||||
|
var baseUrl = this.get_notebook_server();
|
||||||
|
this.start(baseUrl);
|
||||||
|
this.wait_for_dashboard();
|
||||||
|
}
|
||||||
|
|
||||||
|
casper.dashboard_test = function (test) {
|
||||||
|
// Open the dashboard page and run a test.
|
||||||
|
this.open_dashboard();
|
||||||
|
this.then(test);
|
||||||
|
|
||||||
|
this.then(function () {
|
||||||
|
this.page.close();
|
||||||
|
this.page = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Run the browser automation.
|
||||||
|
this.run(function() {
|
||||||
|
this.test.done();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
casper.options.waitTimeout=10000
|
casper.options.waitTimeout=10000
|
||||||
casper.on('waitFor.timeout', function onWaitForTimeout(timeout) {
|
casper.on('waitFor.timeout', function onWaitForTimeout(timeout) {
|
||||||
this.echo("Timeout for " + casper.get_notebook_server());
|
this.echo("Timeout for " + casper.get_notebook_server());
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
"""Base class for notebook tests."""
|
"""Base class for notebook tests."""
|
||||||
|
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import requests
|
import requests
|
||||||
|
@ -42,13 +42,13 @@ class FilesTest(NotebookTestBase):
|
|||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
self.assertEqual(r.text, 'foo')
|
self.assertEqual(r.text, 'foo')
|
||||||
r = requests.get(url_path_join(url, 'files', d, '.foo'))
|
r = requests.get(url_path_join(url, 'files', d, '.foo'))
|
||||||
self.assertEqual(r.status_code, 403)
|
self.assertEqual(r.status_code, 404)
|
||||||
|
|
||||||
for d in hidden:
|
for d in hidden:
|
||||||
path = pjoin(nbdir, d.replace('/', os.sep))
|
path = pjoin(nbdir, d.replace('/', os.sep))
|
||||||
for foo in ('foo', '.foo'):
|
for foo in ('foo', '.foo'):
|
||||||
r = requests.get(url_path_join(url, 'files', d, foo))
|
r = requests.get(url_path_join(url, 'files', d, foo))
|
||||||
self.assertEqual(r.status_code, 403)
|
self.assertEqual(r.status_code, 404)
|
||||||
|
|
||||||
def test_old_files_redirect(self):
|
def test_old_files_redirect(self):
|
||||||
"""pre-2.0 'files/' prefixed links are properly redirected"""
|
"""pre-2.0 'files/' prefixed links are properly redirected"""
|
||||||
|
@ -11,10 +11,13 @@
|
|||||||
# Imports
|
# Imports
|
||||||
#-----------------------------------------------------------------------------
|
#-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
import nose.tools as nt
|
import nose.tools as nt
|
||||||
|
|
||||||
import IPython.testing.tools as tt
|
import IPython.testing.tools as tt
|
||||||
from IPython.html.utils import url_escape, url_unescape
|
from IPython.html.utils import url_escape, url_unescape, is_hidden
|
||||||
|
from IPython.utils.tempdir import TemporaryDirectory
|
||||||
|
|
||||||
#-----------------------------------------------------------------------------
|
#-----------------------------------------------------------------------------
|
||||||
# Test functions
|
# Test functions
|
||||||
@ -59,3 +62,15 @@ def test_url_unescape():
|
|||||||
'/%20%21%40%24%23%25%5E%26%2A%20/%20test%20%25%5E%20notebook%20%40%23%24%20name.ipynb')
|
'/%20%21%40%24%23%25%5E%26%2A%20/%20test%20%25%5E%20notebook%20%40%23%24%20name.ipynb')
|
||||||
nt.assert_equal(path, '/ !@$#%^&* / test %^ notebook @#$ name.ipynb')
|
nt.assert_equal(path, '/ !@$#%^&* / test %^ notebook @#$ name.ipynb')
|
||||||
|
|
||||||
|
def test_is_hidden():
|
||||||
|
with TemporaryDirectory() as root:
|
||||||
|
subdir1 = os.path.join(root, 'subdir')
|
||||||
|
os.makedirs(subdir1)
|
||||||
|
nt.assert_equal(is_hidden(subdir1, root), False)
|
||||||
|
subdir2 = os.path.join(root, '.subdir2')
|
||||||
|
os.makedirs(subdir2)
|
||||||
|
nt.assert_equal(is_hidden(subdir2, root), True)
|
||||||
|
subdir34 = os.path.join(root, 'subdir3', '.subdir4')
|
||||||
|
os.makedirs(subdir34)
|
||||||
|
nt.assert_equal(is_hidden(subdir34, root), True)
|
||||||
|
nt.assert_equal(is_hidden(subdir34), True)
|
||||||
|
@ -19,7 +19,7 @@ import os
|
|||||||
|
|
||||||
from tornado import web
|
from tornado import web
|
||||||
from ..base.handlers import IPythonHandler, notebook_path_regex, path_regex
|
from ..base.handlers import IPythonHandler, notebook_path_regex, path_regex
|
||||||
from ..utils import url_path_join, path2url, url2path, url_escape
|
from ..utils import url_path_join, path2url, url2path, url_escape, is_hidden
|
||||||
|
|
||||||
#-----------------------------------------------------------------------------
|
#-----------------------------------------------------------------------------
|
||||||
# Handlers
|
# Handlers
|
||||||
@ -29,6 +29,27 @@ from ..utils import url_path_join, path2url, url2path, url_escape
|
|||||||
class TreeHandler(IPythonHandler):
|
class TreeHandler(IPythonHandler):
|
||||||
"""Render the tree view, listing notebooks, clusters, etc."""
|
"""Render the tree view, listing notebooks, clusters, etc."""
|
||||||
|
|
||||||
|
def generate_breadcrumbs(self, path):
|
||||||
|
breadcrumbs = [(url_escape(url_path_join(self.base_project_url, 'tree')), '')]
|
||||||
|
comps = path.split('/')
|
||||||
|
ncomps = len(comps)
|
||||||
|
for i in range(ncomps):
|
||||||
|
if comps[i]:
|
||||||
|
link = url_escape(url_path_join(self.base_project_url, 'tree', *comps[0:i+1]))
|
||||||
|
breadcrumbs.append((link, comps[i]))
|
||||||
|
return breadcrumbs
|
||||||
|
|
||||||
|
def generate_page_title(self, path):
|
||||||
|
comps = path.split('/')
|
||||||
|
if len(comps) > 3:
|
||||||
|
for i in range(len(comps)-2):
|
||||||
|
comps.pop(0)
|
||||||
|
page_title = url_escape(url_path_join(*comps))
|
||||||
|
if page_title:
|
||||||
|
return page_title+'/'
|
||||||
|
else:
|
||||||
|
return 'Home'
|
||||||
|
|
||||||
@web.authenticated
|
@web.authenticated
|
||||||
def get(self, path='', name=None):
|
def get(self, path='', name=None):
|
||||||
path = path.strip('/')
|
path = path.strip('/')
|
||||||
@ -41,13 +62,16 @@ class TreeHandler(IPythonHandler):
|
|||||||
self.log.debug("Redirecting %s to %s", self.request.path, url)
|
self.log.debug("Redirecting %s to %s", self.request.path, url)
|
||||||
self.redirect(url)
|
self.redirect(url)
|
||||||
else:
|
else:
|
||||||
if not nbm.path_exists(path=path):
|
if not nbm.path_exists(path=path) or nbm.is_hidden(path):
|
||||||
# no such directory, 404
|
# Directory is hidden or does not exist.
|
||||||
raise web.HTTPError(404)
|
raise web.HTTPError(404)
|
||||||
|
breadcrumbs = self.generate_breadcrumbs(path)
|
||||||
|
page_title = self.generate_page_title(path)
|
||||||
self.write(self.render_template('tree.html',
|
self.write(self.render_template('tree.html',
|
||||||
project=self.project_dir,
|
project=self.project_dir,
|
||||||
tree_url_path=path,
|
page_title=page_title,
|
||||||
notebook_path=path,
|
notebook_path=path,
|
||||||
|
breadcrumbs=breadcrumbs
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
|
@ -12,7 +12,11 @@ Authors:
|
|||||||
# the file COPYING, distributed as part of this software.
|
# the file COPYING, distributed as part of this software.
|
||||||
#-----------------------------------------------------------------------------
|
#-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import stat
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from urllib.parse import quote, unquote
|
from urllib.parse import quote, unquote
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@ -20,6 +24,10 @@ except ImportError:
|
|||||||
|
|
||||||
from IPython.utils import py3compat
|
from IPython.utils import py3compat
|
||||||
|
|
||||||
|
# UF_HIDDEN is a stat flag not defined in the stat module.
|
||||||
|
# It is used by BSD to indicate hidden files.
|
||||||
|
UF_HIDDEN = getattr(stat, 'UF_HIDDEN', 32768)
|
||||||
|
|
||||||
#-----------------------------------------------------------------------------
|
#-----------------------------------------------------------------------------
|
||||||
# Imports
|
# Imports
|
||||||
#-----------------------------------------------------------------------------
|
#-----------------------------------------------------------------------------
|
||||||
@ -72,3 +80,35 @@ def url_unescape(path):
|
|||||||
for p in py3compat.unicode_to_str(path).split('/')
|
for p in py3compat.unicode_to_str(path).split('/')
|
||||||
])
|
])
|
||||||
|
|
||||||
|
def is_hidden(abs_path, abs_root=''):
|
||||||
|
"""Is a file is hidden or contained in a hidden directory.
|
||||||
|
|
||||||
|
This will start with the rightmost path element and work backwards to the
|
||||||
|
given root to see if a path is hidden or in a hidden directory. Hidden is
|
||||||
|
determined by either name starting with '.' or the UF_HIDDEN flag as
|
||||||
|
reported by stat.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
abs_path : unicode
|
||||||
|
The absolute path to check for hidden directories.
|
||||||
|
abs_root : unicode
|
||||||
|
The absolute path of the root directory in which hidden directories
|
||||||
|
should be checked for.
|
||||||
|
"""
|
||||||
|
if not abs_root:
|
||||||
|
abs_root = abs_path.split(os.sep, 1)[0] + os.sep
|
||||||
|
inside_root = abs_path[len(abs_root):]
|
||||||
|
if any(part.startswith('.') for part in inside_root.split(os.sep)):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# check UF_HIDDEN on any location up to root
|
||||||
|
path = abs_path
|
||||||
|
while path and path.startswith(abs_root) and path != abs_root:
|
||||||
|
st = os.stat(path)
|
||||||
|
if getattr(st, 'st_flags', 0) & UF_HIDDEN:
|
||||||
|
return True
|
||||||
|
path = os.path.dirname(path)
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
@ -167,9 +167,12 @@ class JSController(TestController):
|
|||||||
self.section = section
|
self.section = section
|
||||||
|
|
||||||
self.ipydir = TemporaryDirectory()
|
self.ipydir = TemporaryDirectory()
|
||||||
# print(self.ipydir.name)
|
self.nbdir = TemporaryDirectory()
|
||||||
|
print("Running notebook tests in directory: %r" % self.nbdir.name)
|
||||||
|
os.makedirs(os.path.join(self.nbdir.name, os.path.join('subdir1', 'subdir1a')))
|
||||||
|
os.makedirs(os.path.join(self.nbdir.name, os.path.join('subdir2', 'subdir2a')))
|
||||||
self.dirs.append(self.ipydir)
|
self.dirs.append(self.ipydir)
|
||||||
self.env['IPYTHONDIR'] = self.ipydir.name
|
self.dirs.append(self.nbdir)
|
||||||
|
|
||||||
def launch(self):
|
def launch(self):
|
||||||
# start the ipython notebook, so we get the port number
|
# start the ipython notebook, so we get the port number
|
||||||
@ -191,7 +194,7 @@ class JSController(TestController):
|
|||||||
def _init_server(self):
|
def _init_server(self):
|
||||||
"Start the notebook server in a separate process"
|
"Start the notebook server in a separate process"
|
||||||
self.queue = q = Queue()
|
self.queue = q = Queue()
|
||||||
self.server = Process(target=run_webapp, args=(q, self.ipydir.name))
|
self.server = Process(target=run_webapp, args=(q, self.ipydir.name, self.nbdir.name))
|
||||||
self.server.start()
|
self.server.start()
|
||||||
self.server_port = q.get()
|
self.server_port = q.get()
|
||||||
|
|
||||||
@ -202,18 +205,17 @@ class JSController(TestController):
|
|||||||
|
|
||||||
js_test_group_names = {'js'}
|
js_test_group_names = {'js'}
|
||||||
|
|
||||||
def run_webapp(q, nbdir, loglevel=0):
|
def run_webapp(q, ipydir, nbdir, loglevel=0):
|
||||||
"""start the IPython Notebook, and pass port back to the queue"""
|
"""start the IPython Notebook, and pass port back to the queue"""
|
||||||
import os
|
import os
|
||||||
import IPython.html.notebookapp as nbapp
|
import IPython.html.notebookapp as nbapp
|
||||||
import sys
|
import sys
|
||||||
sys.stderr = open(os.devnull, 'w')
|
sys.stderr = open(os.devnull, 'w')
|
||||||
os.environ["IPYTHONDIR"] = nbdir
|
|
||||||
server = nbapp.NotebookApp()
|
server = nbapp.NotebookApp()
|
||||||
args = ['--no-browser']
|
args = ['--no-browser']
|
||||||
args.append('--notebook-dir='+nbdir)
|
args.extend(['--ipython-dir', ipydir])
|
||||||
args.append('--profile-dir='+nbdir)
|
args.extend(['--notebook-dir', nbdir])
|
||||||
args.append('--log-level='+str(loglevel))
|
args.extend(['--log-level', str(loglevel)])
|
||||||
server.initialize(args)
|
server.initialize(args)
|
||||||
# communicate the port number to the parent process
|
# communicate the port number to the parent process
|
||||||
q.put(server.port)
|
q.put(server.port)
|
||||||
|
Loading…
Reference in New Issue
Block a user