supervisely 6.73.444__py3-none-any.whl → 6.73.468__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of supervisely might be problematic. Click here for more details.

Files changed (68) hide show
  1. supervisely/__init__.py +24 -1
  2. supervisely/_utils.py +81 -0
  3. supervisely/annotation/json_geometries_map.py +2 -0
  4. supervisely/api/dataset_api.py +74 -12
  5. supervisely/api/entity_annotation/figure_api.py +8 -5
  6. supervisely/api/image_api.py +4 -0
  7. supervisely/api/video/video_annotation_api.py +4 -2
  8. supervisely/api/video/video_api.py +41 -1
  9. supervisely/app/__init__.py +1 -1
  10. supervisely/app/content.py +14 -6
  11. supervisely/app/fastapi/__init__.py +1 -0
  12. supervisely/app/fastapi/custom_static_files.py +1 -1
  13. supervisely/app/fastapi/multi_user.py +88 -0
  14. supervisely/app/fastapi/subapp.py +88 -42
  15. supervisely/app/fastapi/websocket.py +77 -9
  16. supervisely/app/singleton.py +21 -0
  17. supervisely/app/v1/app_service.py +18 -2
  18. supervisely/app/v1/constants.py +7 -1
  19. supervisely/app/widgets/card/card.py +20 -0
  20. supervisely/app/widgets/deploy_model/deploy_model.py +56 -35
  21. supervisely/app/widgets/dialog/dialog.py +12 -0
  22. supervisely/app/widgets/dialog/template.html +2 -1
  23. supervisely/app/widgets/experiment_selector/experiment_selector.py +8 -0
  24. supervisely/app/widgets/fast_table/fast_table.py +121 -31
  25. supervisely/app/widgets/fast_table/template.html +1 -1
  26. supervisely/app/widgets/radio_tabs/radio_tabs.py +18 -2
  27. supervisely/app/widgets/radio_tabs/template.html +1 -0
  28. supervisely/app/widgets/select_dataset_tree/select_dataset_tree.py +65 -7
  29. supervisely/app/widgets/table/table.py +68 -13
  30. supervisely/app/widgets/tree_select/tree_select.py +2 -0
  31. supervisely/convert/image/csv/csv_converter.py +24 -15
  32. supervisely/convert/video/video_converter.py +2 -2
  33. supervisely/geometry/polyline_3d.py +110 -0
  34. supervisely/io/env.py +76 -1
  35. supervisely/nn/inference/cache.py +37 -17
  36. supervisely/nn/inference/inference.py +667 -114
  37. supervisely/nn/inference/inference_request.py +15 -8
  38. supervisely/nn/inference/predict_app/gui/classes_selector.py +81 -12
  39. supervisely/nn/inference/predict_app/gui/gui.py +676 -488
  40. supervisely/nn/inference/predict_app/gui/input_selector.py +205 -26
  41. supervisely/nn/inference/predict_app/gui/model_selector.py +2 -4
  42. supervisely/nn/inference/predict_app/gui/output_selector.py +46 -6
  43. supervisely/nn/inference/predict_app/gui/settings_selector.py +756 -59
  44. supervisely/nn/inference/predict_app/gui/tags_selector.py +1 -1
  45. supervisely/nn/inference/predict_app/gui/utils.py +236 -119
  46. supervisely/nn/inference/predict_app/predict_app.py +2 -2
  47. supervisely/nn/inference/session.py +43 -35
  48. supervisely/nn/model/model_api.py +9 -0
  49. supervisely/nn/model/prediction_session.py +8 -7
  50. supervisely/nn/prediction_dto.py +7 -0
  51. supervisely/nn/tracker/base_tracker.py +11 -1
  52. supervisely/nn/tracker/botsort/botsort_config.yaml +0 -1
  53. supervisely/nn/tracker/botsort_tracker.py +14 -7
  54. supervisely/nn/tracker/visualize.py +70 -72
  55. supervisely/nn/training/gui/train_val_splits_selector.py +52 -31
  56. supervisely/nn/training/train_app.py +10 -5
  57. supervisely/project/project.py +9 -1
  58. supervisely/video/sampling.py +39 -20
  59. supervisely/video/video.py +41 -12
  60. supervisely/volume/stl_converter.py +2 -0
  61. supervisely/worker_api/agent_rpc.py +24 -1
  62. supervisely/worker_api/rpc_servicer.py +31 -7
  63. {supervisely-6.73.444.dist-info → supervisely-6.73.468.dist-info}/METADATA +14 -11
  64. {supervisely-6.73.444.dist-info → supervisely-6.73.468.dist-info}/RECORD +68 -66
  65. {supervisely-6.73.444.dist-info → supervisely-6.73.468.dist-info}/LICENSE +0 -0
  66. {supervisely-6.73.444.dist-info → supervisely-6.73.468.dist-info}/WHEEL +0 -0
  67. {supervisely-6.73.444.dist-info → supervisely-6.73.468.dist-info}/entry_points.txt +0 -0
  68. {supervisely-6.73.444.dist-info → supervisely-6.73.468.dist-info}/top_level.txt +0 -0
@@ -2,8 +2,9 @@
2
2
  :title=data.{{{widget.widget_id}}}.title
3
3
  :size=data.{{{widget.widget_id}}}.size
4
4
  :visible.sync="state.{{{widget.widget_id}}}.visible"
5
+ @close="post('/{{{widget.widget_id}}}/close_cb');"
5
6
  >
6
7
  <div>
7
8
  {{{widget._content}}}
8
9
  </div>
9
- </el-dialog>
10
+ </el-dialog>
@@ -721,6 +721,14 @@ class ExperimentSelector(Widget):
721
721
  def enable(self):
722
722
  return self.table.enable()
723
723
 
724
+ @property
725
+ def loading(self):
726
+ return self.table.loading
727
+
728
+ @loading.setter
729
+ def loading(self, value: bool):
730
+ self.table.loading = value
731
+
724
732
  def get_json_data(self):
725
733
  return {}
726
734
 
@@ -221,6 +221,11 @@ class FastTable(Widget):
221
221
  self._validate_input_data(data)
222
222
  self._source_data = self._prepare_input_data(data)
223
223
 
224
+ # Initialize filtered and searched data for proper initialization
225
+ self._filtered_data = self._filter(self._filter_value)
226
+ self._searched_data = self._search(self._search_str)
227
+ self._sorted_data = self._sort_table_data(self._searched_data)
228
+
224
229
  # prepare parsed_source_data, sliced_data, parsed_active_data
225
230
  (
226
231
  self._parsed_source_data,
@@ -265,7 +270,7 @@ class FastTable(Widget):
265
270
  self._sliced_data = self._slice_table_data(self._sorted_data, actual_page=self._active_page)
266
271
  self._parsed_active_data = self._unpack_pandas_table_data(self._sliced_data)
267
272
  StateJson().send_changes()
268
- DataJson()[self.widget_id]["data"] = self._parsed_active_data["data"]
273
+ DataJson()[self.widget_id]["data"] = list(self._parsed_active_data["data"])
269
274
  DataJson()[self.widget_id]["total"] = self._rows_total
270
275
  DataJson().send_changes()
271
276
  StateJson()["reactToChanges"] = True
@@ -295,7 +300,7 @@ class FastTable(Widget):
295
300
  :rtype: Dict[str, Any]
296
301
  """
297
302
  return {
298
- "data": self._parsed_active_data["data"],
303
+ "data": list(self._parsed_active_data["data"]),
299
304
  "columns": self._parsed_source_data["columns"],
300
305
  "projectMeta": self._project_meta,
301
306
  "columnsOptions": self._columns_options,
@@ -307,7 +312,7 @@ class FastTable(Widget):
307
312
  "isRadio": self._is_radio,
308
313
  "isRowSelectable": self._is_selectable,
309
314
  "maxSelectedRows": self._max_selected_rows,
310
- "searchPosition": self._search_position
315
+ "searchPosition": self._search_position,
311
316
  },
312
317
  "pageSize": self._page_size,
313
318
  "showHeader": self._show_header,
@@ -490,7 +495,7 @@ class FastTable(Widget):
490
495
  self._sort_column_idx = None
491
496
  self._sort_order = sort.get("order", None)
492
497
  self._page_size = init_options.pop("pageSize", 10)
493
- DataJson()[self.widget_id]["data"] = self._parsed_active_data["data"]
498
+ DataJson()[self.widget_id]["data"] = list(self._parsed_active_data["data"])
494
499
  DataJson()[self.widget_id]["columns"] = self._parsed_active_data["columns"]
495
500
  DataJson()[self.widget_id]["columnsOptions"] = self._columns_options
496
501
  DataJson()[self.widget_id]["options"] = init_options
@@ -519,7 +524,7 @@ class FastTable(Widget):
519
524
  self._parsed_active_data = self._unpack_pandas_table_data(self._sliced_data)
520
525
  self._parsed_source_data = self._unpack_pandas_table_data(self._source_data)
521
526
  self._rows_total = len(self._parsed_source_data["data"])
522
- DataJson()[self.widget_id]["data"] = self._parsed_active_data["data"]
527
+ DataJson()[self.widget_id]["data"] = list(self._parsed_active_data["data"])
523
528
  DataJson()[self.widget_id]["columns"] = self._parsed_active_data["columns"]
524
529
  DataJson()[self.widget_id]["total"] = len(self._source_data)
525
530
  DataJson().send_changes()
@@ -578,10 +583,17 @@ class FastTable(Widget):
578
583
  :rtype: pd.DataFrame
579
584
  """
580
585
  if active_page is True:
581
- temp_parsed_data = [d["items"] for d in self._parsed_active_data["data"]]
586
+ # Return sliced data directly from source to preserve None/NaN values
587
+ packed_data = self._sliced_data.copy()
588
+ # Reset column names to first level only
589
+ if isinstance(packed_data.columns, pd.MultiIndex):
590
+ packed_data.columns = packed_data.columns.get_level_values("first")
582
591
  else:
583
- temp_parsed_data = [d["items"] for d in self._parsed_source_data["data"]]
584
- packed_data = pd.DataFrame(data=temp_parsed_data, columns=self._columns_first_idx)
592
+ # Return source data directly to preserve None/NaN values
593
+ packed_data = self._source_data.copy()
594
+ # Reset column names to first level only
595
+ if isinstance(packed_data.columns, pd.MultiIndex):
596
+ packed_data.columns = packed_data.columns.get_level_values("first")
585
597
  return packed_data
586
598
 
587
599
  def clear_selection(self) -> None:
@@ -621,8 +633,12 @@ class FastTable(Widget):
621
633
  rows = []
622
634
  for row in selected_rows:
623
635
  row_index = row["idx"]
624
- row_data = row.get("row", row.get("items", None))
625
- if row_index is None or row_data is None:
636
+ if row_index is None:
637
+ continue
638
+ # Get original data from source_data to preserve None/NaN values
639
+ try:
640
+ row_data = self._source_data.loc[row_index].values.tolist()
641
+ except (KeyError, IndexError):
626
642
  continue
627
643
  rows.append(self.ClickedRow(row_data, row_index))
628
644
  return rows
@@ -633,8 +649,12 @@ class FastTable(Widget):
633
649
  if clicked_row is None:
634
650
  return None
635
651
  row_index = clicked_row["idx"]
636
- row = clicked_row["row"]
637
- if row_index is None or row is None:
652
+ if row_index is None:
653
+ return None
654
+ # Get original data from source_data to preserve None/NaN values
655
+ try:
656
+ row = self._source_data.loc[row_index].values.tolist()
657
+ except (KeyError, IndexError):
638
658
  return None
639
659
  return self.ClickedRow(row, row_index)
640
660
 
@@ -644,15 +664,19 @@ class FastTable(Widget):
644
664
  :return: Selected cell
645
665
  :rtype: ClickedCell
646
666
  """
647
- cell_data = StateJson()[self.widget_id]["clickedCell"]
667
+ cell_data = StateJson()[self.widget_id]["selectedCell"]
648
668
  if cell_data is None:
649
669
  return None
650
670
  row_index = cell_data["idx"]
651
- row = cell_data["row"]
652
671
  column_index = cell_data["column"]
672
+ if column_index is None or row_index is None:
673
+ return None
653
674
  column_name = self._columns_first_idx[column_index]
654
- column_value = row[column_index]
655
- if column_index is None or row is None:
675
+ # Get original data from source_data to preserve None/NaN values
676
+ try:
677
+ row = self._source_data.loc[row_index].values.tolist()
678
+ column_value = row[column_index]
679
+ except (KeyError, IndexError):
656
680
  return None
657
681
  return self.ClickedCell(row, column_index, row_index, column_name, column_value)
658
682
 
@@ -715,7 +739,25 @@ class FastTable(Widget):
715
739
  self._parsed_active_data,
716
740
  ) = self._prepare_working_data()
717
741
  self._rows_total = len(self._parsed_source_data["data"])
718
- DataJson()[self.widget_id]["data"] = self._parsed_active_data["data"]
742
+ DataJson()[self.widget_id]["data"] = list(self._parsed_active_data["data"])
743
+ DataJson()[self.widget_id]["total"] = self._rows_total
744
+ DataJson().send_changes()
745
+ self._maybe_update_selected_row()
746
+
747
+ def add_rows(self, rows: List):
748
+ for row in rows:
749
+ self._validate_table_sizes(row)
750
+ self._validate_row_values_types(row)
751
+ self._source_data = pd.concat(
752
+ [self._source_data, pd.DataFrame(rows, columns=self._source_data.columns)]
753
+ ).reset_index(drop=True)
754
+ (
755
+ self._parsed_source_data,
756
+ self._sliced_data,
757
+ self._parsed_active_data,
758
+ ) = self._prepare_working_data()
759
+ self._rows_total = len(self._parsed_source_data["data"])
760
+ DataJson()[self.widget_id]["data"] = list(self._parsed_active_data["data"])
719
761
  DataJson()[self.widget_id]["total"] = self._rows_total
720
762
  DataJson().send_changes()
721
763
  self._maybe_update_selected_row()
@@ -743,7 +785,7 @@ class FastTable(Widget):
743
785
  self._parsed_active_data,
744
786
  ) = self._prepare_working_data()
745
787
  self._rows_total = len(self._parsed_source_data["data"])
746
- DataJson()[self.widget_id]["data"] = self._parsed_active_data["data"]
788
+ DataJson()[self.widget_id]["data"] = list(self._parsed_active_data["data"])
747
789
  DataJson()[self.widget_id]["total"] = self._rows_total
748
790
  self._maybe_update_selected_row()
749
791
  return popped_row
@@ -755,7 +797,7 @@ class FastTable(Widget):
755
797
  self._sliced_data = pd.DataFrame(columns=self._columns_first_idx)
756
798
  self._parsed_active_data = {"data": [], "columns": []}
757
799
  self._rows_total = 0
758
- DataJson()[self.widget_id]["data"] = []
800
+ DataJson()[self.widget_id]["data"] = {}
759
801
  DataJson()[self.widget_id]["total"] = 0
760
802
  DataJson().send_changes()
761
803
  self._maybe_update_selected_row()
@@ -856,7 +898,11 @@ class FastTable(Widget):
856
898
  self._refresh()
857
899
 
858
900
  def _default_search_function(self, data: pd.DataFrame, search_value: str) -> pd.DataFrame:
859
- data = data[data.applymap(lambda x: search_value in str(x)).any(axis=1)]
901
+ # Use map() for pandas >= 2.1.0, fallback to applymap() for older versions
902
+ if hasattr(pd.DataFrame, "map"):
903
+ data = data[data.map(lambda x: search_value in str(x)).any(axis=1)]
904
+ else:
905
+ data = data[data.applymap(lambda x: search_value in str(x)).any(axis=1)]
860
906
  return data
861
907
 
862
908
  def _search(self, search_value: str) -> pd.DataFrame:
@@ -867,8 +913,14 @@ class FastTable(Widget):
867
913
  :return: Filtered data
868
914
  :rtype: pd.DataFrame
869
915
  """
870
- filtered_data = self._filtered_data.copy()
916
+ # Use filtered_data if available, otherwise use source_data directly
917
+ if self._filtered_data is not None:
918
+ filtered_data = self._filtered_data.copy()
919
+ else:
920
+ filtered_data = self._source_data.copy()
921
+
871
922
  if search_value == "":
923
+ self._search_str = search_value
872
924
  return filtered_data
873
925
  if self._search_str != search_value:
874
926
  self._active_page = 1
@@ -894,7 +946,24 @@ class FastTable(Widget):
894
946
  else:
895
947
  ascending = False
896
948
  try:
897
- data = data.sort_values(by=data.columns[column_idx], ascending=ascending)
949
+ column = data.columns[column_idx]
950
+ # Try to convert to numeric for proper sorting
951
+ numeric_column = pd.to_numeric(data[column], errors="coerce")
952
+
953
+ # Check if column contains numeric data (has at least one non-NaN numeric value)
954
+ if numeric_column.notna().sum() > 0:
955
+ # Create temporary column for sorting
956
+ data_copy = data.copy()
957
+ data_copy["_sort_key"] = numeric_column
958
+ # Sort by numeric values with NaN at the end
959
+ data_copy = data_copy.sort_values(
960
+ by="_sort_key", ascending=ascending, na_position="last"
961
+ )
962
+ # Remove temporary column and return original data in sorted order
963
+ data = data.loc[data_copy.index]
964
+ else:
965
+ # Sort as strings with NaN values at the end
966
+ data = data.sort_values(by=column, ascending=ascending, na_position="last")
898
967
  except IndexError as e:
899
968
  e.args = (
900
969
  f"Sorting by column idx = {column_idx} is not possible, your table has only {len(data.columns)} columns with idx from 0 to {len(data.columns) - 1}",
@@ -925,7 +994,7 @@ class FastTable(Widget):
925
994
  self._sorted_data = self._sort_table_data(self._searched_data)
926
995
  self._sliced_data = self._slice_table_data(self._sorted_data, actual_page=self._active_page)
927
996
  self._parsed_active_data = self._unpack_pandas_table_data(self._sliced_data)
928
- DataJson()[self.widget_id]["data"] = self._parsed_active_data["data"]
997
+ DataJson()[self.widget_id]["data"] = list(self._parsed_active_data["data"])
929
998
  DataJson()[self.widget_id]["total"] = self._rows_total
930
999
  self._maybe_update_selected_row()
931
1000
  StateJson().send_changes()
@@ -1030,12 +1099,21 @@ class FastTable(Widget):
1030
1099
  def _get_pandas_unpacked_data(self, data: pd.DataFrame) -> dict:
1031
1100
  if not isinstance(data, pd.DataFrame):
1032
1101
  raise TypeError("Cannot parse input data, please use Pandas Dataframe as input data")
1033
- data = data.replace({np.nan: None})
1034
- # data = data.astype(object).replace(np.nan, "-") # TODO: replace None later
1102
+
1103
+ # Create a copy for frontend display to avoid modifying source data
1104
+ display_data = data.copy()
1105
+ # Replace NaN and None with empty string only for display
1106
+ display_data = display_data.replace({np.nan: "", None: ""})
1107
+
1108
+ # Handle MultiIndex columns - extract only the first level
1109
+ if isinstance(display_data.columns, pd.MultiIndex):
1110
+ columns = display_data.columns.get_level_values("first").tolist()
1111
+ else:
1112
+ columns = display_data.columns.to_list()
1035
1113
 
1036
1114
  unpacked_data = {
1037
- "columns": data.columns.to_list(),
1038
- "data": data.values.tolist(),
1115
+ "columns": columns,
1116
+ "data": display_data.values.tolist(),
1039
1117
  }
1040
1118
  return unpacked_data
1041
1119
 
@@ -1206,7 +1284,7 @@ class FastTable(Widget):
1206
1284
 
1207
1285
  self._sliced_data = self._slice_table_data(self._sorted_data, actual_page=self._active_page)
1208
1286
  self._parsed_active_data = self._unpack_pandas_table_data(self._sliced_data)
1209
- DataJson()[self.widget_id]["data"] = self._parsed_active_data["data"]
1287
+ DataJson()[self.widget_id]["data"] = list(self._parsed_active_data["data"])
1210
1288
  DataJson()[self.widget_id]["total"] = self._rows_total
1211
1289
  DataJson().send_changes()
1212
1290
  StateJson().send_changes()
@@ -1275,6 +1353,7 @@ class FastTable(Widget):
1275
1353
 
1276
1354
  def select_row_by_value(self, column, value: Any):
1277
1355
  """Selects a row by value in a specific column.
1356
+ The first column with the given name is used in case of duplicate column names.
1278
1357
 
1279
1358
  :param column: Column name to filter by
1280
1359
  :type column: str
@@ -1288,7 +1367,12 @@ class FastTable(Widget):
1288
1367
  if column not in self._columns_first_idx:
1289
1368
  raise ValueError(f"Column '{column}' does not exist in the table.")
1290
1369
 
1291
- idx = self._source_data[self._source_data[column] == value].index.tolist()
1370
+ # Find the first column index with this name (in case of duplicates)
1371
+ column_idx = self._columns_first_idx.index(column)
1372
+ column_tuple = self._source_data.columns[column_idx]
1373
+
1374
+ # Use column tuple to access the specific column
1375
+ idx = self._source_data[self._source_data[column_tuple] == value].index.tolist()
1292
1376
  if not idx:
1293
1377
  raise ValueError(f"No rows found with {column} = {value}.")
1294
1378
  if len(idx) > 1:
@@ -1299,6 +1383,7 @@ class FastTable(Widget):
1299
1383
 
1300
1384
  def select_rows_by_value(self, column, values: List):
1301
1385
  """Selects rows by value in a specific column.
1386
+ The first column with the given name is used in case of duplicate column names.
1302
1387
 
1303
1388
  :param column: Column name to filter by
1304
1389
  :type column: str
@@ -1312,7 +1397,12 @@ class FastTable(Widget):
1312
1397
  if column not in self._columns_first_idx:
1313
1398
  raise ValueError(f"Column '{column}' does not exist in the table.")
1314
1399
 
1315
- idxs = self._source_data[self._source_data[column].isin(values)].index.tolist()
1400
+ # Find the first column index with this name (in case of duplicates)
1401
+ column_idx = self._columns_first_idx.index(column)
1402
+ column_tuple = self._source_data.columns[column_idx]
1403
+
1404
+ # Use column tuple to access the specific column
1405
+ idxs = self._source_data[self._source_data[column_tuple].isin(values)].index.tolist()
1316
1406
  self.select_rows(idxs)
1317
1407
 
1318
1408
  def _read_custom_columns(self, columns: List[Union[str, tuple]]) -> None:
@@ -1335,4 +1425,4 @@ class FastTable(Widget):
1335
1425
  else:
1336
1426
  raise TypeError(f"Column name must be a string or a tuple, got {type(col)}")
1337
1427
 
1338
- self._validate_sort_attrs()
1428
+ self._validate_sort_attrs()
@@ -11,7 +11,7 @@
11
11
  :project-meta="data.{{{widget.widget_id}}}.projectMeta"
12
12
  :sort.sync="state.{{{widget.widget_id}}}.sort"
13
13
  :search.sync="state.{{{widget.widget_id}}}.search"
14
- :data="data.{{{widget.widget_id}}}.data"
14
+ :data="Object.values(data.{{{widget.widget_id}}}.data || [])"
15
15
  :show-header="data.{{{widget.widget_id}}}.showHeader"
16
16
  :selected-rows="state.{{{widget.widget_id}}}.selectedRows"
17
17
  :selected-radio-idx="state.{{{widget.widget_id}}}.selectedRows && state.{{{widget.widget_id}}}.selectedRows.length > 0 ? state.{{{widget.widget_id}}}.selectedRows[0].idx : null"
@@ -1,5 +1,9 @@
1
- from typing import List, Optional, Dict
1
+ import traceback
2
+ from typing import Dict, List, Optional
3
+
4
+ from supervisely._utils import logger
2
5
  from supervisely.app import StateJson
6
+ from supervisely.app.content import DataJson
3
7
  from supervisely.app.widgets import Widget
4
8
 
5
9
 
@@ -65,7 +69,7 @@ class RadioTabs(Widget):
65
69
  return _value_changed
66
70
 
67
71
  def get_json_data(self) -> Dict:
68
- return {}
72
+ return {"tabsOptions": {item.name: {"disabled": False} for item in self._items}}
69
73
 
70
74
  def get_json_state(self) -> Dict:
71
75
  return {"value": self._value}
@@ -77,3 +81,15 @@ class RadioTabs(Widget):
77
81
 
78
82
  def get_active_tab(self) -> str:
79
83
  return StateJson()[self.widget_id]["value"]
84
+
85
+ def disable_tab(self, tab_name: str):
86
+ if tab_name not in [item.name for item in self._items]:
87
+ raise ValueError(f"Tab with name '{tab_name}' does not exist.")
88
+ DataJson()[self.widget_id]["tabsOptions"][tab_name]["disabled"] = True
89
+ DataJson().send_changes()
90
+
91
+ def enable_tab(self, tab_name: str):
92
+ if tab_name not in [item.name for item in self._items]:
93
+ raise ValueError(f"Tab with name '{tab_name}' does not exist.")
94
+ DataJson()[self.widget_id]["tabsOptions"][tab_name]["disabled"] = False
95
+ DataJson().send_changes()
@@ -20,6 +20,7 @@
20
20
  {% for tab_pane in widget._items %}
21
21
  <el-tab-pane
22
22
  name="{{{tab_pane.name}}}"
23
+ :disabled="data.{{{widget.widget_id}}}.tabsOptions['{{{tab_pane.name}}}'].disabled"
23
24
  >
24
25
  <el-radio
25
26
  slot="label"
@@ -5,6 +5,7 @@ from supervisely.api.api import Api
5
5
  from supervisely.app.widgets import Widget
6
6
  from supervisely.app.widgets.checkbox.checkbox import Checkbox
7
7
  from supervisely.app.widgets.container.container import Container
8
+ from supervisely.app.widgets.field.field import Field
8
9
  from supervisely.app.widgets.select.select import Select
9
10
  from supervisely.app.widgets.tree_select.tree_select import TreeSelect
10
11
  from supervisely.project.project_type import ProjectType
@@ -97,6 +98,7 @@ class SelectDatasetTree(Widget):
97
98
  widget_id: Union[str, None] = None,
98
99
  show_select_all_datasets_checkbox: bool = True,
99
100
  width: int = 193,
101
+ show_selectors_labels: bool = False,
100
102
  ):
101
103
  self._api = Api.from_env()
102
104
 
@@ -114,11 +116,29 @@ class SelectDatasetTree(Widget):
114
116
  # Using environment variables to set the default values if they are not provided.
115
117
  self._project_id = project_id or env.project_id(raise_not_found=False)
116
118
  self._dataset_id = default_id or env.dataset_id(raise_not_found=False)
119
+ if self._project_id:
120
+ project_info = self._api.project.get_info_by_id(self._project_id)
121
+ if allowed_project_types is not None:
122
+ allowed_values = []
123
+ if not isinstance(allowed_project_types, list):
124
+ allowed_project_types = [allowed_project_types]
125
+
126
+ for pt in allowed_project_types:
127
+ if isinstance(pt, (ProjectType, str)):
128
+ allowed_values.append(str(pt))
129
+
130
+ if project_info.type not in allowed_values:
131
+ self._project_id = None
117
132
 
118
133
  self._multiselect = multiselect
119
134
  self._compact = compact
120
135
  self._append_to_body = append_to_body
121
136
 
137
+ # User-defined callbacks
138
+ self._team_changed_callbacks = []
139
+ self._workspace_changed_callbacks = []
140
+ self._project_changed_callbacks = []
141
+
122
142
  # Extract values from Enum to match the .type property of the ProjectInfo object.
123
143
  self._project_types = None
124
144
  if allowed_project_types is not None:
@@ -160,6 +180,7 @@ class SelectDatasetTree(Widget):
160
180
  if show_select_all_datasets_checkbox:
161
181
  self._create_select_all_datasets_checkbox(select_all_datasets)
162
182
 
183
+ self._show_selectors_labels = show_selectors_labels
163
184
  # Group the selectors and the dataset selector into a container.
164
185
  self._content = Container(self._widgets)
165
186
  super().__init__(widget_id=widget_id, file_path=__file__)
@@ -308,8 +329,30 @@ class SelectDatasetTree(Widget):
308
329
  """
309
330
  if not self._multiselect:
310
331
  raise ValueError("This method can only be called when multiselect is enabled.")
332
+ self._select_all_datasets_checkbox.uncheck()
311
333
  self._select_dataset.set_selected_by_id(dataset_ids)
312
334
 
335
+ def team_changed(self, func: Callable) -> Callable:
336
+ """Decorator to set the callback function for the team changed event."""
337
+ if self._compact:
338
+ raise ValueError("callback 'team_changed' is not available in compact mode.")
339
+ self._team_changed_callbacks.append(func)
340
+ return func
341
+
342
+ def workspace_changed(self, func: Callable) -> Callable:
343
+ """Decorator to set the callback function for the workspace changed event."""
344
+ if self._compact:
345
+ raise ValueError("callback 'workspace_changed' is not available in compact mode.")
346
+ self._workspace_changed_callbacks.append(func)
347
+ return func
348
+
349
+ def project_changed(self, func: Callable) -> Callable:
350
+ """Decorator to set the callback function for the project changed event."""
351
+ if self._compact:
352
+ raise ValueError("callback 'project_changed' is not available in compact mode.")
353
+ self._project_changed_callbacks.append(func)
354
+ return func
355
+
313
356
  def value_changed(self, func: Callable) -> Callable:
314
357
  """Decorator to set the callback function for the value changed event.
315
358
 
@@ -353,13 +396,13 @@ class SelectDatasetTree(Widget):
353
396
 
354
397
  if checked:
355
398
  self._select_dataset.select_all()
356
- self._select_dataset.hide()
399
+ self._select_dataset_field.hide()
357
400
  else:
358
401
  self._select_dataset.clear_selected()
359
- self._select_dataset.show()
402
+ self._select_dataset_field.show()
360
403
 
361
404
  if select_all_datasets:
362
- self._select_dataset.hide()
405
+ self._select_dataset_field.hide()
363
406
  select_all_datasets_checkbox.check()
364
407
 
365
408
  self._widgets.append(select_all_datasets_checkbox)
@@ -390,9 +433,10 @@ class SelectDatasetTree(Widget):
390
433
  self._select_dataset.set_selected_by_id(self._dataset_id)
391
434
  if select_all_datasets:
392
435
  self._select_dataset.select_all()
436
+ self._select_dataset_field = Field(self._select_dataset, title="Dataset")
393
437
 
394
438
  # Adding the dataset selector to the list of widgets to be added to the container.
395
- self._widgets.append(self._select_dataset)
439
+ self._widgets.append(self._select_dataset_field)
396
440
 
397
441
  def _create_selectors(self, team_is_selectable: bool, workspace_is_selectable: bool):
398
442
  """Create the team, workspace, and project selectors.
@@ -412,6 +456,9 @@ class SelectDatasetTree(Widget):
412
456
  self._select_workspace.set(items=self._get_select_items(team_id=team_id))
413
457
  self._team_id = team_id
414
458
 
459
+ for callback in self._team_changed_callbacks:
460
+ callback(team_id)
461
+
415
462
  def workspace_selector_handler(workspace_id: int):
416
463
  """Handler function for the event when the workspace selector value changes.
417
464
 
@@ -421,6 +468,9 @@ class SelectDatasetTree(Widget):
421
468
  self._select_project.set(items=self._get_select_items(workspace_id=workspace_id))
422
469
  self._workspace_id = workspace_id
423
470
 
471
+ for callback in self._workspace_changed_callbacks:
472
+ callback(workspace_id)
473
+
424
474
  def project_selector_handler(project_id: int):
425
475
  """Handler function for the event when the project selector value changes.
426
476
 
@@ -435,7 +485,10 @@ class SelectDatasetTree(Widget):
435
485
  and self._select_all_datasets_checkbox.is_checked()
436
486
  ):
437
487
  self._select_dataset.select_all()
438
- self._select_dataset.hide()
488
+ self._select_dataset_field.hide()
489
+
490
+ for callback in self._project_changed_callbacks:
491
+ callback(project_id)
439
492
 
440
493
  self._select_team = Select(
441
494
  items=self._get_select_items(),
@@ -446,6 +499,7 @@ class SelectDatasetTree(Widget):
446
499
  self._select_team.set_value(self._team_id)
447
500
  if not team_is_selectable:
448
501
  self._select_team.disable()
502
+ self._select_team_field = Field(self._select_team, title="Team")
449
503
 
450
504
  self._select_workspace = Select(
451
505
  items=self._get_select_items(team_id=self._team_id),
@@ -456,6 +510,7 @@ class SelectDatasetTree(Widget):
456
510
  self._select_workspace.set_value(self._workspace_id)
457
511
  if not workspace_is_selectable:
458
512
  self._select_workspace.disable()
513
+ self._select_workspace_field = Field(self._select_workspace, title="Workspace")
459
514
 
460
515
  self._select_project = Select(
461
516
  items=self._get_select_items(workspace_id=self._workspace_id),
@@ -464,14 +519,17 @@ class SelectDatasetTree(Widget):
464
519
  width_px=self._width,
465
520
  )
466
521
  self._select_project.set_value(self._project_id)
522
+ self._select_project_field = Field(self._select_project, title="Project")
467
523
 
468
- # Register the event handlers.
524
+ # Register the event handlers._select_project
469
525
  self._select_team.value_changed(team_selector_handler)
470
526
  self._select_workspace.value_changed(workspace_selector_handler)
471
527
  self._select_project.value_changed(project_selector_handler)
472
528
 
473
529
  # Adding widgets to the list, so they can be added to the container.
474
- self._widgets.extend([self._select_team, self._select_workspace, self._select_project])
530
+ self._widgets.extend(
531
+ [self._select_team_field, self._select_workspace_field, self._select_project_field]
532
+ )
475
533
 
476
534
  def _get_select_items(self, **kwargs) -> List[Select.Item]:
477
535
  """Get the list of items for the team, workspace, and project selectors.