<h2 dir="auto">[SIP-30] Remove Tabs in SQL Lab</h2>
<h3 dir="auto">Motivation</h3>
<p dir="auto">SQL Lab is devoting screen space to a custom tab UI that is less effective than that offered natively by the browser. Other upcoming features such as more powerful edit history/undo, and better navigation/discovery make tabs less important. The new design directions coming from the superset design group specify a tabless UI. Browser-based tabs would be a better solution for nearly every SQL Lab use case.</p>
<h3 dir="auto">Proposed Change</h3>
<ul dir="auto">
<li>Remove the tab bar in SQL Lab</li>
<li>Include the id of the current <code class="notranslate">queryEditor</code> in the SQL Lab url: <code class="notranslate">myapp.com/superset/sqllab/42</code></li>
<li>Links throughout Superset referencing SQL Lab queries should use urls with a <code class="notranslate">queryEditor</code> id</li>
<li>Implement a user-level flag to switch between the tabful and tabless UI</li>
<li>Stabilize backend-managed SQL Lab state, and make it the default option.</li>
</ul>
<p dir="auto">This change can actually be made without significant changes to state management. SQL Lab state includes some number of <code class="notranslate">queryEditor</code>s ("tabs"). Instead of each <code class="notranslate">queryEditor</code> referencing an in-page tab, the SQL Lab url will contain the id of the <code class="notranslate">queryEditor</code> being used.</p>
<p dir="auto">This will depend on the <a href="https://github.com/apache/incubator-superset/pull/8060" data-hovercard-type="pull_request" data-hovercard-url="/apache/superset/pull/8060/hovercard">backend implementation of state management for SQL Lab</a> which is currently behind a feature flag. Without backend SQL Lab state, your local state would accumulate until there is too much to manage, without a practical way to clean it besides clearing your localStorage or editing it laboriously.</p>
<p dir="auto">One change that will be tricky is offering users a quality migration experience to the tabless UI. More details on that down the page.</p>
<h4 dir="auto">A note on preventing lost work in a tabless UI</h4>
<p dir="auto">Nobody wants to write a query, then close their browser and lose it forever. Mitigating that in this new design will be important. The full UI/UX redesigns call for a robust history feature, but there are some things we can do to protect people's work without the history feature.</p>
<p dir="auto">When you navigate to SQL Lab without specifying an id in the URL, an id will be generated for you just as if you had clicked the "+ tab" button in the current UI. This will allow users to re-open a closed tab from the browser history, even if they didn't save the query. If a user wrote a query but didn't save it, and closes the browser tab, SQLLab should also warn and offer to save the query before the tab closes. If you run or save your query, those will also be ways to recover your work.</p>
<p dir="auto">Just for reference, here's the design we're working towards. This SIP is not about implementing everything on this screen, only replacing the in-page tabs with browser tabs.</p>
<p dir="auto"><a target="_blank" rel="noopener noreferrer nofollow" href="https://user-images.githubusercontent.com/1858430/69596121-60293800-0fb6-11ea-9a8c-e231544eee78.png"><img width="1360" alt="Screen Shot 2019-11-25 at 7 04 12 PM" src="https://user-images.githubusercontent.com/1858430/69596121-60293800-0fb6-11ea-9a8c-e231544eee78.png" style="max-width: 100%;"></a></p>
<h3 dir="auto">New or Changed Public Interfaces</h3>
<ul dir="auto">
<li>"Run query in a new tab" button in the query history will open a new browser tab.</li>
<li>"Share query" should use the new url format, and should save the query if it hasn't been saved.</li>
<li>Links in the saved queries screen and query search should use the new url format.</li>
<li>Remove the ctrl-t keyboard shortcut?</li>
</ul>
<h3 dir="auto">Migration Plan and Compatibility</h3>
<p dir="auto">We should allow users to migrate to the new system at their own pace. Tabs are a heavily-used feature, and should not be ripped out without a proper transition phase to educate and re-orient users.</p>
<p dir="auto">A per-user feature flag will be added indicating whether they are using the tabful or tabless version of SQLLab. SQLLab will appear unchanged as long as a user's flag is off, with the exception of a small banner in the corner notifying the user of the new tabless experience, and allowing them to flip the flag on. Once they flip the flag SQLLab will change to the tabless UI. There should be an explanation of the change, with useful info on how to navigate this brave new world.</p>
<p dir="auto">After some time, we can turn this flag on by default for new users. Eventually when the tabless UI is deemed stable, people are happy with the change, and we've made sure that there isn't a negative impact to existing user needs, we can switch everyone over.</p>
<h3 dir="auto">Rejected Alternatives</h3>
<ul dir="auto">
<li>Keeping in-page tabs - too cumbersome to use, not as accessible, adds unnecessary state complexity</li>
<li>Switching over immediately to a tabless UI without a feature flag - too jarring to users, want to give them the opportunity to change to the new system when it's convenient for them.</li>
<li>Developing the new SQL Lab designs in a separate screen alongside the original SQL Lab - too much code complexity and chance of breaking changes, not enough opportunity to reuse existing components.</li>
<li>Re-organizing SQL Lab frontend state around a tabless UI - too much work for now, and breaks compatibility between the new and old version of the page. It will likely make sense to re-organize state in a future release once all users have migrated to the tabless UI.</li>
</ul>
<p dir="auto">A clear and concise description of what the bug is.</p>
<p dir="auto">I tried running the following code:</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="git clone https://github.com/apache/incubator-superset/
cd incubator-superset
# you can run this command everytime you need to start superset now:
docker-compose up"><pre class="notranslate"><code class="notranslate">git clone https://github.com/apache/incubator-superset/
cd incubator-superset
# you can run this command everytime you need to start superset now:
docker-compose up
</code></pre></div>
<p dir="auto">as mentioned here - <a href="https://superset.incubator.apache.org/installation.html#start-with-docker" rel="nofollow">https://superset.incubator.apache.org/installation.html#start-with-docker</a></p>
<h3 dir="auto">Expected results</h3>
<p dir="auto">Expect Superset UI to allow me to access it and it should work.</p>
<h3 dir="auto">Actual results</h3>
<p dir="auto">The UI didn't recognize the credentials at first. Then I went inside the Docker container of superset named <code class="notranslate">incubator-superset_superset</code> and ran the following command:</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="superset fab create-admin \
              --username admin \
              --firstname Superset \
              --lastname Admin \
              --email admin@superset.com \
              --password admin"><pre class="notranslate"><code class="notranslate">superset fab create-admin \
              --username admin \
              --firstname Superset \
              --lastname Admin \
              --email admin@superset.com \
              --password admin
</code></pre></div>
<p dir="auto">which I copied from here - <a href="https://github.com/apache/incubator-superset/blob/master/docker/docker-init.sh#L36">https://github.com/apache/incubator-superset/blob/master/docker/docker-init.sh#L36</a>.</p>
<p dir="auto">I got authenticated successfully but got an error page, the content of which is as following:</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="sqlalchemy.exc.ProgrammingError
sqlalchemy.exc.ProgrammingError: (psycopg2.ProgrammingError) relation &quot;user_attribute&quot; does not exist
LINE 2: FROM user_attribute 
             ^

[SQL: SELECT user_attribute.welcome_dashboard_id AS user_attribute_welcome_dashboard_id 
FROM user_attribute 
WHERE user_attribute.user_id = %(user_id_1)s]
[parameters: {'user_id_1': '1'}]
(Background on this error at: http://sqlalche.me/e/f405)

Traceback (most recent call last)
File &quot;/usr/local/lib/python3.6/site-packages/sqlalchemy/engine/base.py&quot;, line 1246, in _execute_context
cursor, statement, parameters, context
File &quot;/usr/local/lib/python3.6/site-packages/sqlalchemy/engine/default.py&quot;, line 581, in do_execute
cursor.execute(statement, parameters)
The above exception was the direct cause of the following exception:
File &quot;/usr/local/lib/python3.6/site-packages/flask/app.py&quot;, line 2463, in __call__
return self.wsgi_app(environ, start_response)
File &quot;/usr/local/lib/python3.6/site-packages/flask/app.py&quot;, line 2449, in wsgi_app
response = self.handle_exception(e)
File &quot;/usr/local/lib/python3.6/site-packages/flask/app.py&quot;, line 1866, in handle_exception
reraise(exc_type, exc_value, tb)
File &quot;/usr/local/lib/python3.6/site-packages/flask/_compat.py&quot;, line 39, in reraise
raise value
File &quot;/usr/local/lib/python3.6/site-packages/flask/app.py&quot;, line 2446, in wsgi_app
response = self.full_dispatch_request()
File &quot;/usr/local/lib/python3.6/site-packages/flask/app.py&quot;, line 1951, in full_dispatch_request
rv = self.handle_user_exception(e)
File &quot;/usr/local/lib/python3.6/site-packages/flask/app.py&quot;, line 1820, in handle_user_exception
reraise(exc_type, exc_value, tb)
File &quot;/usr/local/lib/python3.6/site-packages/flask/_compat.py&quot;, line 39, in reraise
raise value
File &quot;/usr/local/lib/python3.6/site-packages/flask/app.py&quot;, line 1949, in full_dispatch_request
rv = self.dispatch_request()
File &quot;/usr/local/lib/python3.6/site-packages/flask/app.py&quot;, line 1935, in dispatch_request
return self.view_functions[rule.endpoint](**req.view_args)
File &quot;/app/superset/views/core.py&quot;, line 2633, in welcome
.filter_by(user_id=g.user.get_id())
File &quot;/usr/local/lib/python3.6/site-packages/sqlalchemy/orm/query.py&quot;, line 3355, in scalar
ret = self.one()
File &quot;/usr/local/lib/python3.6/site-packages/sqlalchemy/orm/query.py&quot;, line 3325, in one
ret = self.one_or_none()
File &quot;/usr/local/lib/python3.6/site-packages/sqlalchemy/orm/query.py&quot;, line 3294, in one_or_none
ret = list(self)
File &quot;/usr/local/lib/python3.6/site-packages/sqlalchemy/orm/query.py&quot;, line 3367, in __iter__
return self._execute_and_instances(context)
File &quot;/usr/local/lib/python3.6/site-packages/sqlalchemy/orm/query.py&quot;, line 3392, in _execute_and_instances
result = conn.execute(querycontext.statement, self._params)
File &quot;/usr/local/lib/python3.6/site-packages/sqlalchemy/engine/base.py&quot;, line 982, in execute
return meth(self, multiparams, params)
File &quot;/usr/local/lib/python3.6/site-packages/sqlalchemy/sql/elements.py&quot;, line 287, in _execute_on_connection
return connection._execute_clauseelement(self, multiparams, params)
File &quot;/usr/local/lib/python3.6/site-packages/sqlalchemy/engine/base.py&quot;, line 1101, in _execute_clauseelement
distilled_params,
File &quot;/usr/local/lib/python3.6/site-packages/sqlalchemy/engine/base.py&quot;, line 1250, in _execute_context
e, statement, parameters, cursor, context
File &quot;/usr/local/lib/python3.6/site-packages/sqlalchemy/engine/base.py&quot;, line 1476, in _handle_dbapi_exception
util.raise_from_cause(sqlalchemy_exception, exc_info)
File &quot;/usr/local/lib/python3.6/site-packages/sqlalchemy/util/compat.py&quot;, line 398, in raise_from_cause
reraise(type(exception), exception, tb=exc_tb, cause=cause)
File &quot;/usr/local/lib/python3.6/site-packages/sqlalchemy/util/compat.py&quot;, line 152, in reraise
raise value.with_traceback(tb)
File &quot;/usr/local/lib/python3.6/site-packages/sqlalchemy/engine/base.py&quot;, line 1246, in _execute_context
cursor, statement, parameters, context
File &quot;/usr/local/lib/python3.6/site-packages/sqlalchemy/engine/default.py&quot;, line 581, in do_execute
cursor.execute(statement, parameters)
sqlalchemy.exc.ProgrammingError: (psycopg2.ProgrammingError) relation &quot;user_attribute&quot; does not exist
LINE 2: FROM user_attribute
^

[SQL: SELECT user_attribute.welcome_dashboard_id AS user_attribute_welcome_dashboard_id
FROM user_attribute
WHERE user_attribute.user_id = %(user_id_1)s]
[parameters: {'user_id_1': '1'}]
(Background on this error at: http://sqlalche.me/e/f405)
The debugger caught an exception in your WSGI application. You can now look at the traceback which led to the error.
To switch between the interactive traceback and the plaintext one, you can click on the &quot;Traceback&quot; headline. From the text traceback you can also create a paste of it. For code execution mouse-over the frame you want to debug and click on the console icon on the right side.

You can execute arbitrary Python code in the stack frames and there are some extra helpers available for introspection:

dump() shows all variables in the frame
dump(obj) dumps all that's known about the object"><pre class="notranslate"><code class="notranslate">sqlalchemy.exc.ProgrammingError
sqlalchemy.exc.ProgrammingError: (psycopg2.ProgrammingError) relation "user_attribute" does not exist
LINE 2: FROM user_attribute 
             ^

[SQL: SELECT user_attribute.welcome_dashboard_id AS user_attribute_welcome_dashboard_id 
FROM user_attribute 
WHERE user_attribute.user_id = %(user_id_1)s]
[parameters: {'user_id_1': '1'}]
(Background on this error at: http://sqlalche.me/e/f405)

Traceback (most recent call last)
File "/usr/local/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1246, in _execute_context
cursor, statement, parameters, context
File "/usr/local/lib/python3.6/site-packages/sqlalchemy/engine/default.py", line 581, in do_execute
cursor.execute(statement, parameters)
The above exception was the direct cause of the following exception:
File "/usr/local/lib/python3.6/site-packages/flask/app.py", line 2463, in __call__
return self.wsgi_app(environ, start_response)
File "/usr/local/lib/python3.6/site-packages/flask/app.py", line 2449, in wsgi_app
response = self.handle_exception(e)
File "/usr/local/lib/python3.6/site-packages/flask/app.py", line 1866, in handle_exception
reraise(exc_type, exc_value, tb)
File "/usr/local/lib/python3.6/site-packages/flask/_compat.py", line 39, in reraise
raise value
File "/usr/local/lib/python3.6/site-packages/flask/app.py", line 2446, in wsgi_app
response = self.full_dispatch_request()
File "/usr/local/lib/python3.6/site-packages/flask/app.py", line 1951, in full_dispatch_request
rv = self.handle_user_exception(e)
File "/usr/local/lib/python3.6/site-packages/flask/app.py", line 1820, in handle_user_exception
reraise(exc_type, exc_value, tb)
File "/usr/local/lib/python3.6/site-packages/flask/_compat.py", line 39, in reraise
raise value
File "/usr/local/lib/python3.6/site-packages/flask/app.py", line 1949, in full_dispatch_request
rv = self.dispatch_request()
File "/usr/local/lib/python3.6/site-packages/flask/app.py", line 1935, in dispatch_request
return self.view_functions[rule.endpoint](**req.view_args)
File "/app/superset/views/core.py", line 2633, in welcome
.filter_by(user_id=g.user.get_id())
File "/usr/local/lib/python3.6/site-packages/sqlalchemy/orm/query.py", line 3355, in scalar
ret = self.one()
File "/usr/local/lib/python3.6/site-packages/sqlalchemy/orm/query.py", line 3325, in one
ret = self.one_or_none()
File "/usr/local/lib/python3.6/site-packages/sqlalchemy/orm/query.py", line 3294, in one_or_none
ret = list(self)
File "/usr/local/lib/python3.6/site-packages/sqlalchemy/orm/query.py", line 3367, in __iter__
return self._execute_and_instances(context)
File "/usr/local/lib/python3.6/site-packages/sqlalchemy/orm/query.py", line 3392, in _execute_and_instances
result = conn.execute(querycontext.statement, self._params)
File "/usr/local/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 982, in execute
return meth(self, multiparams, params)
File "/usr/local/lib/python3.6/site-packages/sqlalchemy/sql/elements.py", line 287, in _execute_on_connection
return connection._execute_clauseelement(self, multiparams, params)
File "/usr/local/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1101, in _execute_clauseelement
distilled_params,
File "/usr/local/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1250, in _execute_context
e, statement, parameters, cursor, context
File "/usr/local/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1476, in _handle_dbapi_exception
util.raise_from_cause(sqlalchemy_exception, exc_info)
File "/usr/local/lib/python3.6/site-packages/sqlalchemy/util/compat.py", line 398, in raise_from_cause
reraise(type(exception), exception, tb=exc_tb, cause=cause)
File "/usr/local/lib/python3.6/site-packages/sqlalchemy/util/compat.py", line 152, in reraise
raise value.with_traceback(tb)
File "/usr/local/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1246, in _execute_context
cursor, statement, parameters, context
File "/usr/local/lib/python3.6/site-packages/sqlalchemy/engine/default.py", line 581, in do_execute
cursor.execute(statement, parameters)
sqlalchemy.exc.ProgrammingError: (psycopg2.ProgrammingError) relation "user_attribute" does not exist
LINE 2: FROM user_attribute
^

[SQL: SELECT user_attribute.welcome_dashboard_id AS user_attribute_welcome_dashboard_id
FROM user_attribute
WHERE user_attribute.user_id = %(user_id_1)s]
[parameters: {'user_id_1': '1'}]
(Background on this error at: http://sqlalche.me/e/f405)
The debugger caught an exception in your WSGI application. You can now look at the traceback which led to the error.
To switch between the interactive traceback and the plaintext one, you can click on the "Traceback" headline. From the text traceback you can also create a paste of it. For code execution mouse-over the frame you want to debug and click on the console icon on the right side.

You can execute arbitrary Python code in the stack frames and there are some extra helpers available for introspection:

dump() shows all variables in the frame
dump(obj) dumps all that's known about the object
</code></pre></div>
<h4 dir="auto">How to reproduce the bug</h4>
<p dir="auto">Run steps mentioned previously.</p>
<p dir="auto">It is worth noting that when I follow the same steps with the branch <a href="https://github.com/apache/incubator-superset/blob/0.36/docker/docker-init.sh"><code class="notranslate">0.36</code></a> I do not see this problem. So it must be something that got introduced into the <code class="notranslate">master</code> branch but was not released in <code class="notranslate">0.36</code>.</p>
<h3 dir="auto">Environment</h3>
<p dir="auto">(please complete the following information):</p>
<ul dir="auto">
<li>superset version: <code class="notranslate">master</code> branch</li>
<li>python version: based on the docker-compose file</li>
<li>node.js version: based on the docker-compose file</li>
<li>npm version: based on the docker-compose file</li>
</ul>
<h3 dir="auto">Checklist</h3>
<p dir="auto">Make sure these boxes are checked before submitting your issue - thank you!</p>
<ul class="contains-task-list">
<li class="task-list-item"><input type="checkbox" id="" disabled="" class="task-list-item-checkbox" checked=""> I have checked the superset logs for python stacktraces and included it here as text if there are any.</li>
<li class="task-list-item"><input type="checkbox" id="" disabled="" class="task-list-item-checkbox" checked=""> I have reproduced the issue with at least the latest released version of superset.</li>
<li class="task-list-item"><input type="checkbox" id="" disabled="" class="task-list-item-checkbox" checked=""> I have checked the issue tracker for the same issue and I haven't found one similar.</li>
</ul>
<h3 dir="auto">Additional context</h3>
<p dir="auto">Add any other context about the problem here.</p>