Repository: holoviz/panel

Issue:
Streaming new data to tabulator widget with show_index=False raises a ValueError exception
When streaming new data to a tabulator widget, if the show_index value is set to False then a ValueError exception is raised.
It happens with both DataFrames and Dictionaries.

This code below demonstrates it : 

```
import pandas as pd
import panel as pn

df = pd.DataFrame(np.random.randn(10, 5), columns=list('ABCDE'))
tabulator=pn.widgets.Tabulator(df, show_index=False)
def stream_data(event):
    stream_dic = {
        "A":0,
        "B":0,
        "C":0,
        "D":0,
        "E":0
    }
    stream_df = pd.DataFrame(np.random.randn(10, 5), columns=list('ABCDE'))
    tabulator.stream(stream_df)

button = pn.widgets.Button(name="stream")
button.on_click(stream_data)
pn.Row(tabulator, button)
```

If you set show_index to True, the new data is streamed as expected. 
This did not happen in version 0.12.6.

Current environment :
Panel 0.13.0
Pandas 1.3.2
Jupyter Lab


Discussion:
The traceback
``` python-traceback

Traceback (most recent call last):
  File "/home/shh/miniconda3/envs/holoviz/lib/python3.8/site-packages/pyviz_comms/__init__.py", line 338, in _handle_msg
    self._on_msg(msg)
  File "/home/shh/Development/holoviz/panel/panel/viewable.py", line 273, in _on_msg
    patch.apply_to_document(doc, comm.id)
  File "/home/shh/miniconda3/envs/holoviz/lib/python3.8/site-packages/bokeh/protocol/messages/patch_doc.py", line 115, in apply_to_document
    invoke_with_curdoc(doc, lambda: doc.apply_json_patch(self.content, setter))
  File "/home/shh/miniconda3/envs/holoviz/lib/python3.8/site-packages/bokeh/document/callbacks.py", line 408, in invoke_with_curdoc
    return f()
  File "/home/shh/miniconda3/envs/holoviz/lib/python3.8/site-packages/bokeh/protocol/messages/patch_doc.py", line 115, in <lambda>
    invoke_with_curdoc(doc, lambda: doc.apply_json_patch(self.content, setter))
  File "/home/shh/miniconda3/envs/holoviz/lib/python3.8/site-packages/bokeh/document/document.py", line 391, in apply_json_patch
    DocumentPatchedEvent.handle_json(self, event_json, references, setter)
  File "/home/shh/miniconda3/envs/holoviz/lib/python3.8/site-packages/bokeh/document/events.py", line 259, in handle_json
    handler(doc, event_json, references, setter)
  File "/home/shh/miniconda3/envs/holoviz/lib/python3.8/site-packages/bokeh/document/events.py", line 300, in _handle_json
    cb(event_json["msg_data"])
  File "/home/shh/miniconda3/envs/holoviz/lib/python3.8/site-packages/bokeh/document/callbacks.py", line 355, in trigger_json_event
    model._trigger_event(event)
  File "/home/shh/miniconda3/envs/holoviz/lib/python3.8/site-packages/bokeh/util/callback_manager.py", line 119, in _trigger_event
    self.document.callbacks.notify_event(cast(Model, self), event, invoke)
  File "/home/shh/miniconda3/envs/holoviz/lib/python3.8/site-packages/bokeh/document/callbacks.py", line 247, in notify_event
    invoke_with_curdoc(doc, callback_invoker)
  File "/home/shh/miniconda3/envs/holoviz/lib/python3.8/site-packages/bokeh/document/callbacks.py", line 408, in invoke_with_curdoc
    return f()
  File "/home/shh/miniconda3/envs/holoviz/lib/python3.8/site-packages/bokeh/util/callback_manager.py", line 115, in invoke
    cast(EventCallbackWithEvent, callback)(event)
  File "/home/shh/Development/holoviz/panel/panel/reactive.py", line 356, in _comm_event
    self._process_bokeh_event(event)
  File "/home/shh/Development/holoviz/panel/panel/reactive.py", line 306, in _process_bokeh_event
    self._process_event(event)
  File "/home/shh/Development/holoviz/panel/panel/widgets/button.py", line 136, in _process_event
    self.clicks += 1
  File "/home/shh/Development/holoviz/param/param/parameterized.py", line 358, in _f
    instance_param.__set__(obj, val)
  File "/home/shh/Development/holoviz/param/param/parameterized.py", line 360, in _f
    return f(self, obj, val)
  File "/home/shh/Development/holoviz/param/param/__init__.py", line 621, in __set__
    super(Dynamic,self).__set__(obj,val)
  File "/home/shh/Development/holoviz/param/param/parameterized.py", line 360, in _f
    return f(self, obj, val)
  File "/home/shh/Development/holoviz/param/param/parameterized.py", line 1249, in __set__
    obj.param._call_watcher(watcher, event)
  File "/home/shh/Development/holoviz/param/param/parameterized.py", line 2041, in _call_watcher
    self_._execute_watcher(watcher, (event,))
  File "/home/shh/Development/holoviz/param/param/parameterized.py", line 2023, in _execute_watcher
    watcher.fn(*args, **kwargs)
  File "/tmp/ipykernel_11034/3143372746.py", line 16, in stream_data
    tabulator.stream(stream_df)
  File "/home/shh/Development/holoviz/panel/panel/widgets/tables.py", line 1253, in stream
    super().stream(stream_value, rollover, reset_index)
  File "/home/shh/Development/holoviz/panel/panel/widgets/tables.py", line 571, in stream
    self._stream(stream_value, rollover)
  File "/home/shh/Development/holoviz/panel/panel/util.py", line 379, in wrapped
    fn(self, *args, **kwargs)
  File "/home/shh/Development/holoviz/panel/panel/widgets/tables.py", line 1242, in _stream
    super()._stream(stream, rollover)
  File "/home/shh/Development/holoviz/panel/panel/util.py", line 379, in wrapped
    fn(self, *args, **kwargs)
  File "/home/shh/Development/holoviz/panel/panel/reactive.py", line 708, in _stream
    m.source.stream(stream, rollover)
  File "/home/shh/miniconda3/envs/holoviz/lib/python3.8/site-packages/bokeh/models/sources.py", line 450, in stream
    self._stream(new_data, rollover)
  File "/home/shh/miniconda3/envs/holoviz/lib/python3.8/site-packages/bokeh/models/sources.py", line 532, in _stream
    raise ValueError("Must stream updates to all existing columns (extra: %s)" % ", ".join(sorted(extra)))
ValueError: Must stream updates to all existing columns (extra: index)

```
Circling back on that issue, I've found out that simply adding "index" in the "hidden_columns" parameter somewhat fixes this issue. It's a bit counter intuitive because you're explicitly showing the index only to hide it but it works.

`tabulator=pn.widgets.Tabulator(df, show_index=True, hidden_columns=["index"])`

Resolving patch:
diff --git a/panel/io/admin.py b/panel/io/admin.py
--- a/panel/io/admin.py
+++ b/panel/io/admin.py
@@ -15,13 +15,15 @@
 from bokeh.plotting import ColumnDataSource, figure
 
 from ..config import config
+from ..depends import bind
 from ..layout import (
     Accordion, Column, FlexBox, Row, Tabs,
 )
-from ..models import terminal  # noqa
 from ..pane import HTML, Bokeh
 from ..template import FastListTemplate
-from ..widgets import MultiSelect, Terminal, TextInput
+from ..widgets import (
+    Button, MultiSelect, Tabulator, TextInput,
+)
 from ..widgets.indicators import Trend
 from .logging import (
     LOG_SESSION_CREATED, LOG_SESSION_DESTROYED, LOG_SESSION_LAUNCHING,
@@ -49,11 +51,7 @@ def filter(self, record):
         if session_id in log_sessions:
             return False
         elif session_id not in session_filter.options:
-            session_filter.options = session_filter.options + [session_id]
-        if session_filter.value and session_id not in session_filter.value:
-            return False
-        if name_filter.value and name_filter.value not in record.name:
-            return False
+            session_filter.options = [session_id] + session_filter.options
         return True
 
 
@@ -75,6 +73,48 @@ def emit(self, record):
         self._data.param.trigger('data')
 
 
+class _LogTabulator(Tabulator):
+
+    _update_defaults = {
+        "theme": "midnight",
+        "layout": "fit_data_stretch",
+        "show_index": False,
+        "sorters": [{'field': 'datetime', 'dir': 'dsc'}],
+        "disabled": True,
+        "pagination": "local",
+        "page_size": 18,
+    }
+
+    def __init__(self, **params):
+        params["value"] = self._create_frame()
+        params = {**self._update_defaults, **params}
+        super().__init__(**params)
+
+    @staticmethod
+    def _create_frame(data=None):
+        columns=["datetime", "level", "app", "session", "message"]
+        if data is None:
+            return pd.DataFrame(columns=columns)
+        else:
+            return pd.Series(data, index=columns)
+
+    def write(self, log):
+        # Example of a log message:
+        # '2022-07-13 14:38:04,803 INFO: panel.io.server - Session 140255299576448 launching\n'
+        try:
+            s = log.strip().split(" ")
+            datetime = f"{s[0]} {s[1]}"
+            level = s[2][:-1]
+            app = s[3]
+            session = int(s[6])
+            message = " ".join(s[7:])
+            df = self._create_frame([datetime, level, app, session, message])
+
+            self.stream(df, follow=False)
+        except Exception:
+            pass
+
+
 # Set up logging
 data = Data()
 log_data_handler = LogDataHandler(data)
@@ -88,11 +128,41 @@ def emit(self, record):
 log_data_handler.addFilter(log_filter)
 formatter = logging.Formatter('%(asctime)s %(levelname)s: %(name)s - %(message)s')
 log_handler.setFormatter(formatter)
-log_terminal = Terminal(sizing_mode='stretch_both', min_height=400)
+log_terminal = _LogTabulator(sizing_mode='stretch_both', min_height=400)
 log_handler.setStream(log_terminal)
 
 session_filter = MultiSelect(name='Filter by session', options=[])
-name_filter = TextInput(name='Filter by component')
+message_filter = TextInput(name='Filter by message')
+level_filter = MultiSelect(name="Filter by level", options=["DEBUG", "INFO", "WARNING", "ERROR"])
+app_filter = TextInput(name='Filter by app')
+
+def _textinput_filter(df, pattern, column):
+    if not pattern or df.empty:
+        return df
+    return df[df[column].str.contains(pattern)].copy()
+
+log_terminal.add_filter(level_filter, 'level')
+log_terminal.add_filter(bind(_textinput_filter, pattern=app_filter, column='app'))
+log_terminal.add_filter(session_filter, 'session')
+log_terminal.add_filter(bind(_textinput_filter, pattern=message_filter, column='message'))
+
+
+def _clear_log_filters(*events):
+    level_filter.value = []
+    app_filter.value = ""
+    session_filter.value = []
+    message_filter.value = ""
+
+
+reset_filter = Button(name="Clear filters")
+reset_filter.on_click(_clear_log_filters)
+
+
+download_filename, download_button = log_terminal.download_menu(
+    text_kwargs={'name': 'Enter filename for logfile', 'value': 'log.csv'},
+    button_kwargs={'name': 'Download logfile'}
+)
+
 
 EVENT_TYPES = {
     'initializing': 'MediumSeaGreen',
@@ -347,11 +417,21 @@ def get_overview(doc=None):
 
 
 def log_component():
+    # Without this tabulator is empty after reload of website
+    log_terminal.param.trigger("value")
+
     return Column(
         Accordion(
-            ('Filters', Row(
+            ('Filters & Download', Row(
+                level_filter,
+                app_filter,
                 session_filter,
-                name_filter,
+                message_filter,
+                Column(
+                    download_filename,
+                    download_button,
+                    reset_filter,
+                ),
                 sizing_mode='stretch_width'
             )),
             active=[],
diff --git a/panel/widgets/tables.py b/panel/widgets/tables.py
--- a/panel/widgets/tables.py
+++ b/panel/widgets/tables.py
@@ -476,8 +476,11 @@ def _process_column(self, values):
         return values
 
     def _get_data(self) -> Tuple[DataFrameType, DataDict]:
+        return self._process_df_and_convert_to_cds(self.value)
+
+    def _process_df_and_convert_to_cds(self, df: DataFrameType) -> Tuple[DataFrameType, DataDict]:
         import pandas as pd
-        df = self._filter_dataframe(self.value)
+        df = self._filter_dataframe(df)
         if df is None:
             return [], {}
         if isinstance(self.value.index, pd.MultiIndex):
@@ -595,10 +598,10 @@ def stream(self, stream_value, rollover=None, reset_index=True):
                 self.param.trigger('value')
             finally:
                 self._updating = False
-            stream_value = self._filter_dataframe(stream_value)
+            stream_value, stream_data = self._process_df_and_convert_to_cds(stream_value)
             try:
                 self._updating = True
-                self._stream(stream_value, rollover)
+                self._stream(stream_data, rollover)
             finally:
                 self._updating = False
         elif isinstance(stream_value, pd.Series):
@@ -606,10 +609,10 @@ def stream(self, stream_value, rollover=None, reset_index=True):
             if rollover is not None and len(self.value) > rollover:
                 with param.discard_events(self):
                     self.value = self.value.iloc[-rollover:]
-            stream_value = self._filter_dataframe(self.value.iloc[-1:])
+            stream_value, stream_data = self._process_df_and_convert_to_cds(self.value.iloc[-1:])
             try:
                 self._updating = True
-                self._stream(stream_value, rollover)
+                self._stream(stream_data, rollover)
             finally:
                 self._updating = False
         elif isinstance(stream_value, dict):
@@ -1278,7 +1281,7 @@ def _stream(self, stream, rollover=None, follow=True):
         if self.pagination == 'remote':
             length = self._length
             nrows = self.page_size
-            max_page = length//nrows + bool(length%nrows)
+            max_page = max(length//nrows + bool(length%nrows), 1)
             if self.page != max_page:
                 return
         super()._stream(stream, rollover)
@@ -1291,8 +1294,10 @@ def stream(self, stream_value, rollover=None, reset_index=True, follow=True):
         if follow and self.pagination:
             length = self._length
             nrows = self.page_size
-            self.page = length//nrows + bool(length%nrows)
+            self.page = max(length//nrows + bool(length%nrows), 1)
         super().stream(stream_value, rollover, reset_index)
+        if follow and self.pagination:
+            self._update_max_page()
 
     @updating
     def _patch(self, patch):
@@ -1342,7 +1347,7 @@ def _update_selectable(self):
     def _update_max_page(self):
         length = self._length
         nrows = self.page_size
-        max_page = length//nrows + bool(length%nrows)
+        max_page = max(length//nrows + bool(length%nrows), 1)
         self.param.page.bounds = (1, max_page)
         for ref, (model, _) in self._models.items():
             self._apply_update([], {'max_page': max_page}, model, ref)
@@ -1423,7 +1428,7 @@ def _get_properties(self, source: ColumnDataSource) -> Dict[str, Any]:
         props.update(self._process_param_change(process))
         if self.pagination:
             length = 0 if self._processed is None else len(self._processed)
-            props['max_page'] = length//self.page_size + bool(length%self.page_size)
+            props['max_page'] = max(length//self.page_size + bool(length%self.page_size), 1)
         if self.frozen_rows and 'height' not in props and 'sizing_mode' not in props:
             length = self.page_size+2 if self.pagination else self._length
             props['height'] = min([length * self.row_height + 30, 2000])


Test patch: