Skip to content

Commit

Permalink
instances navigator popup
Browse files Browse the repository at this point in the history
  • Loading branch information
Andrew Schonfeld authored and aschonfeld committed Oct 29, 2019
1 parent 79976f5 commit c3c759f
Show file tree
Hide file tree
Showing 25 changed files with 823 additions and 92 deletions.
16 changes: 7 additions & 9 deletions dtale/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@

from dtale import dtale
from dtale.cli.clickutils import retrieve_meta_info_and_version, setup_logging
from dtale.utils import (build_shutdown_url, build_url, dict_merge,
running_with_flask, running_with_pytest)
from dtale.utils import build_shutdown_url, build_url, dict_merge
from dtale.views import cleanup, startup

if PY3:
Expand Down Expand Up @@ -331,8 +330,8 @@ def find_free_port():
return s.getsockname()[1]


def show(data=None, host='0.0.0.0', port=None, debug=False, subprocess=True, data_loader=None, reaper_on=True,
open_browser=False, **kwargs):
def show(data=None, host='0.0.0.0', port=None, name=None, debug=False, subprocess=True, data_loader=None,
reaper_on=True, open_browser=False, **kwargs):
"""
Entry point for kicking off D-Tale Flask process from python process
Expand All @@ -342,6 +341,8 @@ def show(data=None, host='0.0.0.0', port=None, debug=False, subprocess=True, dat
:type host: str, optional
:param port: port number of D-Tale process, defaults to any open port on server
:type port: str, optional
:param name: optional label to assign a D-Tale process
:type name: str, optional
:param debug: will turn on Flask debug functionality, defaults to False
:type debug: bool, optional
:param subprocess: run D-Tale as a subprocess of your current process, defaults to True
Expand Down Expand Up @@ -369,7 +370,7 @@ def show(data=None, host='0.0.0.0', port=None, debug=False, subprocess=True, dat
setup_logging(logfile, log_level or 'info', verbose)

selected_port = int(port or find_free_port())
data_hook = startup(data=data, data_loader=data_loader, port=selected_port)
data_hook = startup(data=data, data_loader=data_loader, port=selected_port, name=name)

def _show():
app = build_app(reaper_on=reaper_on)
Expand All @@ -380,10 +381,7 @@ def _show():
getLogger("werkzeug").setLevel(LOG_ERROR)
logger.info('D-Tale started at: {}'.format(build_url(selected_port)))
if open_browser:
# when running flask in debug it spins up two instances, we only want this code
# run during startup of the second instance
if not running_with_pytest() and running_with_flask():
webbrowser.get().open(build_url(selected_port))
webbrowser.get().open(build_url(selected_port))
app.run(host=host, port=selected_port, debug=debug)

if subprocess:
Expand Down
58 changes: 58 additions & 0 deletions dtale/swagger/dtale/views/processes.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
summary: Fetch list of running D-Tale processes
tags:
- D-Tale API
responses:
200:
description: JSON object containing array of instance objects and success flag
content:
application/json:
schema:
oneOf:
- properties:
dtypes:
type: array
description: list of process objects
items:
type: object
properties:
rows:
type: integer
description: number of rows in dataframe for process
columns:
type: integer
description: number of columns in dataframe for process
ts:
type: integer
description: millisecond representation of process start
start:
type: string
description: pretty printed representation of process start
name:
type: string
description: optional name assigned to process
port:
type: string
description: integer string of port assigned to process
names:
type: string
description: comma-separated string of column names in dataframe for process
required:
- rows
- columns
- ts
- start
- names
- port
success:
type: boolean
default: true
- properties:
error:
type: string
description: Exception summary
traceback:
type: string
description: Exception traceback
success:
type: boolean
default: false
1 change: 1 addition & 0 deletions dtale/templates/dtale/main.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<input type="hidden" id="settings" value="{{settings}}" />
<input type="hidden" id="version" value="{{version}}" />
<input type="hidden" id="hide_shutdown" value="{{config.HIDE_SHUTDOWN}}" />
<input type="hidden" id="processes" value={{processes}} />
<div class="container-fluid">
<div id="content">
</div>
Expand Down
76 changes: 64 additions & 12 deletions dtale/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,17 @@
from dtale.utils import (build_shutdown_url, build_url, dict_merge,
filter_df_for_grid, find_dtype_formatter,
find_selected_column, get_dtypes, get_int_arg,
get_str_arg, grid_columns, grid_formatter, json_float,
json_int, jsonify, make_list, retrieve_grid_params,
running_with_flask, running_with_pytest,
sort_df_for_grid)
get_str_arg, grid_columns, grid_formatter, json_date,
json_float, json_int, json_timestamp, jsonify,
make_list, retrieve_grid_params, running_with_flask,
running_with_pytest, sort_df_for_grid)

logger = getLogger(__name__)

DATA = {}
DTYPES = {}
SETTINGS = {}
METADATA = {}


class DtaleData(object):
Expand Down Expand Up @@ -82,7 +83,7 @@ def build_dtypes_state(data):
return [dict(name=c, dtype=dtypes[c], index=i) for i, c in enumerate(data.columns)]


def startup(data=None, data_loader=None, port=None):
def startup(data=None, data_loader=None, port=None, name=None):
"""
Loads and stores data globally
- If data has indexes then it will lock save those columns as locked on the front-end
Expand All @@ -93,7 +94,7 @@ def startup(data=None, data_loader=None, port=None):
:param data_loader: function which returns pandas.DataFrame
:param port: integer port for running Flask process
"""
global DATA, DTYPES, SETTINGS
global DATA, DTYPES, SETTINGS, METADATA

if data_loader is not None:
data = data_loader()
Expand All @@ -108,8 +109,9 @@ def startup(data=None, data_loader=None, port=None):
data = data.to_frame(index=False)

logger.debug('pytest: {}, flask: {}'.format(running_with_pytest(), running_with_flask()))
curr_index = [i for i in make_list(data.index.name or data.index.names) if i is not None]
curr_index = [str(i) for i in make_list(data.index.name or data.index.names) if i is not None]
data = data.reset_index().drop('index', axis=1, errors='ignore')
data.columns = [str(c) for c in data.columns]
port_key = str(port)
if port_key in SETTINGS:
curr_settings = SETTINGS[port_key]
Expand All @@ -121,6 +123,7 @@ def startup(data=None, data_loader=None, port=None):
else:
logger.debug('pre-locking index columns ({}) to settings[{}]'.format(curr_index, port))
curr_locked = curr_index
METADATA[port_key] = dict(start=pd.Timestamp('now'), name=name)

# in the case that data has been updated we will drop any sorts or filter for ease of use
SETTINGS[port_key] = dict(locked=curr_locked)
Expand Down Expand Up @@ -156,7 +159,51 @@ def view_main():
"""
curr_settings = SETTINGS.get(request.environ.get('SERVER_PORT', 'curr'), {})
_, version = retrieve_meta_info_and_version('dtale')
return render_template('dtale/main.html', settings=json.dumps(curr_settings), version=str(version))
return render_template(
'dtale/main.html', settings=json.dumps(curr_settings), version=str(version), processes=len(DATA)
)


@dtale.route('/processes')
@swag_from('swagger/dtale/views/test-filter.yml')
def get_processes():
"""
Flask route which returns list of running D-Tale processes within current python process
:return: JSON {
data: [
{
port: 1, name: 'name1', rows: 5, columns: 5, names: 'col1,...,col5', start: '2018-04-30 12:36:44',
ts: 1525106204000
},
...,
{
port: N, name: 'nameN', rows: 5, columns: 5, names: 'col1,...,col5', start: '2018-04-30 12:36:44',
ts: 1525106204000
}
],
success: True/False
}
"""
def _load_process(port):
data = DATA[port]
dtypes = DTYPES[port]
mdata = METADATA[port]
return dict(
port=port,
rows=len(data),
columns=len(dtypes),
names=','.join([c['name'] for c in dtypes]),
start=json_date(mdata['start']),
ts=json_timestamp(mdata['start']),
name=mdata['name']
)

try:
processes = sorted([_load_process(port) for port in DATA], key=lambda p: p['ts'])
return jsonify(dict(data=processes, success=True))
except BaseException as e:
return jsonify(dict(error=str(e), traceback=str(traceback.format_exc())))


@dtale.route('/update-settings')
Expand Down Expand Up @@ -302,7 +349,7 @@ def get_data():
"""
try:
global SETTINGS, DATA, DTYPES
port = request.environ.get('SERVER_PORT', 'curr')
port = get_str_arg(request, 'port', request.environ.get('SERVER_PORT', 'curr'))
data = DATA[port]

# this will check for when someone instantiates D-Tale programatically and directly alters the internal
Expand Down Expand Up @@ -337,19 +384,24 @@ def get_data():

total = len(data)
results = {}
idx_col = str('dtale_index')
for sub_range in ids:
sub_range = list(map(int, sub_range.split('-')))
if len(sub_range) == 1:
sub_df = data.iloc[sub_range[0]:sub_range[0] + 1]
sub_df = f.format_dicts(sub_df.itertuples())
results[sub_range[0]] = dict_merge(dict(dtale_index=sub_range[0]), sub_df[0])
results[sub_range[0]] = dict_merge({idx_col: sub_range[0]}, sub_df[0])
else:
[start, end] = sub_range
sub_df = data.iloc[start:] if end >= len(data) - 1 else data.iloc[start:end + 1]
sub_df = f.format_dicts(sub_df.itertuples())
for i, d in zip(range(start, end + 1), sub_df):
results[i] = dict_merge(dict(dtale_index=i), d)
return_data = dict(results=results, columns=[dict(name='dtale_index', dtype='int64')] + col_types, total=total)
results[i] = dict_merge({idx_col: i}, d)
return_data = dict(
results=results,
columns=[dict(name=idx_col, dtype='int64')] + col_types,
total=total
)
return jsonify(return_data)
except BaseException as e:
return jsonify(dict(error=str(e), traceback=str(traceback.format_exc())))
Expand Down
8 changes: 5 additions & 3 deletions static/__tests__/dtale/DataViewer-about-expired-test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import * as t from "../jest-assertions";
import reduxUtils from "../redux-test-utils";
import { buildInnerHTML, withGlobalJquery } from "../test-utils";

const pjson = require("../../../package.json");

const originalOffsetHeight = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "offsetHeight");
const originalOffsetWidth = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "offsetWidth");

Expand All @@ -20,7 +22,7 @@ describe("DataViewer tests", () => {
const mockBuildLibs = withGlobalJquery(() =>
mockPopsicle.mock(url => {
if (_.includes(url, "pypi.org")) {
return { info: { version: "2.0.0" } };
return { info: { version: "999.0.0" } };
}
const { urlFetcher } = require("../redux-test-utils").default;
return urlFetcher(url);
Expand Down Expand Up @@ -75,15 +77,15 @@ describe("DataViewer tests", () => {
.find("div.modal-body div.row")
.first()
.text(),
"Your Version:1.0.0",
`Your Version:${pjson.version}`,
"renders our version"
);
t.equal(
about
.find("div.modal-body div.row")
.at(1)
.text(),
"PyPi Version:2.0.0",
"PyPi Version:999.0.0",
"renders PyPi version"
);
t.equal(about.find("div.dtale-alert").length, 1, "should render alert");
Expand Down
10 changes: 6 additions & 4 deletions static/__tests__/dtale/DataViewer-about-test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import * as t from "../jest-assertions";
import reduxUtils from "../redux-test-utils";
import { buildInnerHTML, withGlobalJquery } from "../test-utils";

const pjson = require("../../../package.json");

const originalOffsetHeight = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "offsetHeight");
const originalOffsetWidth = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "offsetWidth");

Expand Down Expand Up @@ -65,12 +67,12 @@ describe("DataViewer tests", () => {
.simulate("click");
setTimeout(() => {
result.update();
t.equal(result.find(About).length, 1, "should show describe");
t.equal(result.find(About).length, 1, "should show about");
result
.find(ModalClose)
.first()
.simulate("click");
t.equal(result.find(About).length, 0, "should hide describe");
t.equal(result.find(About).length, 0, "should hide about");
result
.find(DataViewerMenu)
.find("ul li button")
Expand All @@ -85,15 +87,15 @@ describe("DataViewer tests", () => {
.find("div.modal-body div.row")
.first()
.text(),
"Your Version:1.0.0",
`Your Version:${pjson.version}`,
"renders our version"
);
t.equal(
about
.find("div.modal-body div.row")
.at(1)
.text(),
"PyPi Version:1.0.0",
`PyPi Version:${pjson.version}`,
"renders PyPi version"
);
t.equal(about.find("div.dtale-alert").length, 0, "should not render alert");
Expand Down
8 changes: 4 additions & 4 deletions static/__tests__/dtale/DataViewer-coverage-test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ describe("DataViewer tests", () => {
const { DataViewer } = require("../../dtale/DataViewer");
const CoverageChart = require("../../popups/CoverageChart").ReactCoverageChart;
const CoverageChartBody = require("../../popups/CoverageChartBody").default;
const PopupChart = require("../../popups/PopupChart").ReactPopupChart;
const Popup = require("../../popups/Popup").ReactPopup;

const store = reduxUtils.createDtaleStore();
buildInnerHTML("");
Expand All @@ -75,15 +75,15 @@ describe("DataViewer tests", () => {
.at(10)
.simulate("click");
result.update();
t.ok(result.find(PopupChart).instance().props.chartData.visible, "should open coverage");
t.ok(result.find(Popup).instance().props.chartData.visible, "should open coverage");
result
.find(PopupChart)
.find(Popup)
.first()
.find(ModalClose)
.first()
.simulate("click");
result.update();
t.notOk(result.find(PopupChart).instance().props.chartData.visible, "should close coverage");
t.notOk(result.find(Popup).instance().props.chartData.visible, "should close coverage");
result.update();
result
.find(DataViewerMenu)
Expand Down
Loading

0 comments on commit c3c759f

Please sign in to comment.