From ddd8a7aa4008b542ea2803099b64577d91f74c1c Mon Sep 17 00:00:00 2001 From: Jiaen Ren Date: Tue, 21 Jul 2026 22:00:10 -0700 Subject: [PATCH 01/12] Add bounded workflow label metrics dimensions Add one prefixed attribute per curated policy key to osmo_tasks_count, clamped to the allow-list plus / sentinels so series stay bounded in every enforcement mode. Fix the gauge's counting defect by selecting COUNT(*) instead of emitting presence rows. Pin the OSS Workflow Resources dashboard as deployment-neutral (no PPP binding) with a structural test; PPP panels ship via internal provisioning. Co-Authored-By: Claude Fable 5 --- docs/deployment_guide/dashboards/BUILD | 23 ++ .../dashboards/test_dashboards.py | 80 +++++ src/service/core/workflow/helpers.py | 6 +- src/service/core/workflow/tests/BUILD | 10 + .../core/workflow/tests/test_helpers.py | 13 + .../workflow/tests/test_workflow_metrics.py | 309 ++++++++++++++++++ src/service/core/workflow/workflow_metrics.py | 72 +++- 7 files changed, 506 insertions(+), 7 deletions(-) create mode 100644 docs/deployment_guide/dashboards/BUILD create mode 100644 docs/deployment_guide/dashboards/test_dashboards.py create mode 100644 src/service/core/workflow/tests/test_workflow_metrics.py diff --git a/docs/deployment_guide/dashboards/BUILD b/docs/deployment_guide/dashboards/BUILD new file mode 100644 index 0000000000..faad6d9fdd --- /dev/null +++ b/docs/deployment_guide/dashboards/BUILD @@ -0,0 +1,23 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +load("//bzl:py.bzl", "osmo_py_test") + +osmo_py_test( + name = "test_dashboards", + srcs = ["test_dashboards.py"], + data = ["workflow_resources_usage.json"], +) diff --git a/docs/deployment_guide/dashboards/test_dashboards.py b/docs/deployment_guide/dashboards/test_dashboards.py new file mode 100644 index 0000000000..ce33ea9b3d --- /dev/null +++ b/docs/deployment_guide/dashboards/test_dashboards.py @@ -0,0 +1,80 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # pylint: disable=line-too-long +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Structural checks for the Grafana dashboard JSON definitions.""" + +import json +import pathlib +import unittest +from typing import Any + + +DASHBOARD_DIRECTORY = pathlib.Path(__file__).parent + + +def _load_dashboard(filename: str) -> dict[str, Any]: + with (DASHBOARD_DIRECTORY / filename).open(encoding='utf-8') as dashboard_file: + return json.load(dashboard_file) + + +def _variables(dashboard: dict[str, Any]) -> dict[str, dict[str, Any]]: + return { + variable['name']: variable + for variable in dashboard.get('templating', {}).get('list', []) + } + + +def _query(variable: dict[str, Any]) -> str: + query = variable.get('query', '') + return query.get('query', '') if isinstance(query, dict) else query + + +class WorkflowResourcesDashboardTest(unittest.TestCase): + """The OSS workflow dashboard remains deployment-neutral.""" + + dashboard: dict[str, Any] + + @classmethod + def setUpClass(cls) -> None: + cls.dashboard = _load_dashboard('workflow_resources_usage.json') + + def test_preserves_workflow_resource_panels(self): + panel_titles = {panel['title'] for panel in self.dashboard['panels']} + self.assertTrue({ + 'CPU Usage', + 'Memory Usage', + 'Disk Usage', + 'GPU Utilization', + 'GPU Memory Usage', + 'GPU Node Conditions', + 'GPU Usage', + 'GPU Throttle', + }.issubset(panel_titles)) + + def test_workflow_selector_is_not_bound_to_ppp(self): + variables = _variables(self.dashboard) + self.assertNotIn('PPP', variables) + uuid_query = _query(variables['uuid']) + self.assertIn('kube_pod_info', uuid_query) + self.assertNotIn('label_PPP', uuid_query) + + def test_panel_ids_are_unique(self): + panel_ids = [panel['id'] for panel in self.dashboard['panels']] + self.assertEqual(len(panel_ids), len(set(panel_ids))) + + +if __name__ == '__main__': + unittest.main() diff --git a/src/service/core/workflow/helpers.py b/src/service/core/workflow/helpers.py index db922415cd..e0bbd275b8 100644 --- a/src/service/core/workflow/helpers.py +++ b/src/service/core/workflow/helpers.py @@ -542,7 +542,9 @@ def get_recent_tasks(database: connectors.PostgresConnector, w.pool AS pool, w.submitted_by AS user, w.workflow_uuid AS workflow_uuid, - t.status AS status + t.status AS status, + w.labels AS labels, + COUNT(*) AS count FROM tasks t JOIN @@ -551,7 +553,7 @@ def get_recent_tasks(database: connectors.PostgresConnector, (t.end_time is NULL AND w.status IN ('WAITING', 'PENDING', 'RUNNING')) OR t.end_time > %s - GROUP BY w.pool, w.submitted_by, t.status, w.workflow_uuid + GROUP BY w.pool, w.submitted_by, w.workflow_uuid, t.status, w.labels """ return database.execute_fetch_command(query, (cutoff_time,), True) diff --git a/src/service/core/workflow/tests/BUILD b/src/service/core/workflow/tests/BUILD index 0521907322..95878c10c5 100644 --- a/src/service/core/workflow/tests/BUILD +++ b/src/service/core/workflow/tests/BUILD @@ -104,3 +104,13 @@ osmo_py_test( ], tags = ["requires-network"], ) + +py_test( + name = "test_workflow_metrics", + srcs = ["test_workflow_metrics.py"], + deps = [ + "//src/service/core/workflow", + "//src/utils/connectors", + "//src/utils/metrics", + ], +) diff --git a/src/service/core/workflow/tests/test_helpers.py b/src/service/core/workflow/tests/test_helpers.py index 6c4a132b39..5411f1ac2e 100644 --- a/src/service/core/workflow/tests/test_helpers.py +++ b/src/service/core/workflow/tests/test_helpers.py @@ -595,6 +595,19 @@ def test_get_router_cookie_joins_multiple_cookies_with_comma(self): class TestGetRecentTasks(unittest.TestCase): + def test_get_recent_tasks_selects_labels_and_count(self): + database = mock.Mock() + database.execute_fetch_command.return_value = [] + + helpers.get_recent_tasks(database, minutes_ago=5) + + query = database.execute_fetch_command.call_args.args[0] + self.assertIn('w.labels AS labels', query) + self.assertIn('COUNT(*) AS count', query) + self.assertIn( + 'GROUP BY w.pool, w.submitted_by, w.workflow_uuid, t.status, w.labels', + query) + def test_get_recent_tasks_passes_cutoff_time_to_database(self): database = mock.Mock() database.execute_fetch_command.return_value = [] diff --git a/src/service/core/workflow/tests/test_workflow_metrics.py b/src/service/core/workflow/tests/test_workflow_metrics.py new file mode 100644 index 0000000000..807642d2eb --- /dev/null +++ b/src/service/core/workflow/tests/test_workflow_metrics.py @@ -0,0 +1,309 @@ +""" +SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +SPDX-License-Identifier: Apache-2.0 +""" + +import os +import types +import unittest +from unittest import mock + +from src.service.core.workflow import workflow_metrics +from src.utils import connectors + + +def _workflow_config( + policy_allow_lists: dict[str, list[str]] | None = None, +) -> types.SimpleNamespace: + policy_allow_lists = policy_allow_lists if policy_allow_lists is not None else {} + policy = [ + connectors.LabelPolicy(key=key, allow_list=allow_list) + for key, allow_list in policy_allow_lists.items() + ] + return types.SimpleNamespace( + labels_config=types.SimpleNamespace(policy=policy) + ) + + +class GetTaskMetricsTest(unittest.TestCase): + """Task counts are projected onto configured attribution dimensions.""" + + def setUp(self): + self._disable_metrics = os.environ.pop('OSMO_DISABLE_TASK_METRICS', None) + workflow_metrics._metric_cache.clear() # pylint: disable=protected-access + workflow_metrics._last_refresh_time = 0 # pylint: disable=protected-access + + def tearDown(self): + if self._disable_metrics is None: + os.environ.pop('OSMO_DISABLE_TASK_METRICS', None) + else: + os.environ['OSMO_DISABLE_TASK_METRICS'] = self._disable_metrics + workflow_metrics._metric_cache.clear() # pylint: disable=protected-access + workflow_metrics._last_refresh_time = 0 # pylint: disable=protected-access + + def test_sums_database_counts_and_collapses_non_policy_labels(self): + rows = [ + { + 'pool': 'pool-a', + 'user': 'alice', + 'workflow_uuid': 'workflow-1', + 'status': 'RUNNING', + 'labels': { + 'PPP': 'project-a', + 'cost-center': 'center-1', + 'experiment': 'first', + }, + 'count': 2, + }, + { + 'pool': 'pool-a', + 'user': 'alice', + 'workflow_uuid': 'workflow-1', + 'status': 'RUNNING', + 'labels': ( + '{"PPP":"project-a","cost-center":"center-1",' + '"experiment":"second"}' + ), + 'count': 3, + }, + { + 'pool': 'pool-a', + 'user': 'alice', + 'workflow_uuid': 'workflow-1', + 'status': 'RUNNING', + 'labels': '{"PPP":"project-a"}', + 'count': 4, + }, + { + 'pool': 'pool-a', + 'user': 'alice', + 'workflow_uuid': 'workflow-1', + 'status': 'RUNNING', + 'labels': None, + 'count': 1, + }, + { + 'pool': 'pool-a', + 'user': 'alice', + 'workflow_uuid': 'workflow-1', + 'status': 'RUNNING', + 'labels': 'not-json', + 'count': 2, + }, + { + 'pool': 'pool-a', + 'user': 'alice', + 'workflow_uuid': 'workflow-1', + 'status': 'RUNNING', + 'labels': {'PPP': 'unattributed'}, + 'count': 6, + }, + ] + database = mock.Mock() + database.get_workflow_configs.return_value = _workflow_config({ + 'PPP': ['project-a'], + 'cost-center': ['center-1'], + }) + + with mock.patch.object( + connectors.PostgresConnector, 'get_instance', return_value=database + ), mock.patch.object( + workflow_metrics.helpers, 'get_recent_tasks', return_value=rows + ), mock.patch.object( + workflow_metrics.time, 'time', return_value=100 + ): + observations = list(workflow_metrics.get_task_metrics()) + + counts = {} + for observation in observations: + attributes = observation.attributes + if attributes is None: + self.fail('Task metric observation is missing attributes.') + counts[( + attributes['workflow_label_PPP'], + attributes['workflow_label_cost_dash_center'], + )] = observation.value + self.assertEqual(attributes['pool'], 'pool-a') + self.assertEqual(attributes['user'], 'alice') + self.assertEqual(attributes['workflow_uuid'], 'workflow-1') + self.assertEqual(attributes['status'], 'RUNNING') + self.assertEqual(counts, { + ('project-a', 'center-1'): 5, + ('project-a', ''): 4, + ('', ''): 3, + ('', ''): 6, + }) + + def test_empty_allow_list_clamps_all_present_values_to_other(self): + label_policy = connectors.LabelPolicy(key='PPP') + + self.assertEqual( + workflow_metrics._workflow_label_metric_value( + {'PPP': 'arbitrary'}, label_policy, + ), + '', + ) + self.assertEqual( + workflow_metrics._workflow_label_metric_value({}, label_policy), + '', + ) + + def test_policy_label_attributes_cannot_overwrite_or_sanitize_to_same_name(self): + database = mock.Mock() + database.get_workflow_configs.return_value = _workflow_config({ + 'pool': ['label-pool'], + 'a.b': ['dot'], + 'a/b': ['slash'], + 'a_dot_b': ['literal-token'], + }) + rows = [{ + 'pool': 'pool-a', + 'user': 'alice', + 'workflow_uuid': 'workflow-1', + 'status': 'RUNNING', + 'labels': { + 'pool': 'label-pool', + 'a.b': 'dot', + 'a/b': 'slash', + 'a_dot_b': 'literal-token', + }, + 'count': 1, + }] + + with mock.patch.object( + connectors.PostgresConnector, 'get_instance', return_value=database + ), mock.patch.object( + workflow_metrics.helpers, 'get_recent_tasks', return_value=rows + ), mock.patch.object( + workflow_metrics.time, 'time', return_value=100 + ): + observations = list(workflow_metrics.get_task_metrics()) + + attributes = observations[0].attributes + if attributes is None: + self.fail('Task metric observation is missing attributes.') + self.assertEqual(attributes['pool'], 'pool-a') + self.assertEqual(attributes['workflow_label_pool'], 'label-pool') + self.assertEqual(attributes['workflow_label_a_dot_b'], 'dot') + self.assertEqual(attributes['workflow_label_a_slash_b'], 'slash') + self.assertEqual( + attributes['workflow_label_a__dot__b'], 'literal-token' + ) + + def test_no_label_policies_keeps_base_dimensions_and_database_count(self): + database = mock.Mock() + database.get_workflow_configs.return_value = _workflow_config() + rows = [{ + 'pool': None, + 'user': 'alice', + 'workflow_uuid': 'workflow-1', + 'status': 'COMPLETED', + 'labels': {'experiment': 'ignored'}, + 'count': 7, + }] + + with mock.patch.object( + connectors.PostgresConnector, 'get_instance', return_value=database + ), mock.patch.object( + workflow_metrics.helpers, 'get_recent_tasks', return_value=rows + ), mock.patch.object( + workflow_metrics.time, 'time', return_value=100 + ): + observations = list(workflow_metrics.get_task_metrics()) + + self.assertEqual(len(observations), 1) + self.assertEqual(observations[0].value, 7) + attributes = observations[0].attributes + if attributes is None: + self.fail('Task metric observation is missing attributes.') + self.assertEqual(dict(attributes), { + 'pool': 'unknown', + 'user': 'alice', + 'workflow_uuid': 'workflow-1', + 'status': 'COMPLETED', + }) + + def test_cache_is_reused_until_30_second_ttl_expires(self): + database = mock.Mock() + database.get_workflow_configs.return_value = _workflow_config({'PPP': ['project-a']}) + first_rows = [{ + 'pool': 'pool-a', + 'user': 'alice', + 'workflow_uuid': 'workflow-1', + 'status': 'RUNNING', + 'labels': {'PPP': 'project-a'}, + 'count': 2, + }] + refreshed_rows = [{ + **first_rows[0], + 'count': 9, + }] + + with mock.patch.object( + connectors.PostgresConnector, 'get_instance', return_value=database + ), mock.patch.object( + workflow_metrics.helpers, + 'get_recent_tasks', + side_effect=[first_rows, refreshed_rows], + ) as get_recent_tasks, mock.patch.object( + workflow_metrics.time, 'time', side_effect=[100, 129, 131] + ): + first = list(workflow_metrics.get_task_metrics()) + cached = list(workflow_metrics.get_task_metrics()) + refreshed = list(workflow_metrics.get_task_metrics()) + + self.assertEqual(first[0].value, 2) + self.assertEqual(cached[0].value, 2) + self.assertEqual(refreshed[0].value, 9) + self.assertEqual(get_recent_tasks.call_count, 2) + self.assertEqual(database.get_workflow_configs.call_count, 2) + + def test_disabled_metrics_do_not_query_database(self): + os.environ['OSMO_DISABLE_TASK_METRICS'] = 'true' + + with mock.patch.object( + connectors.PostgresConnector, 'get_instance' + ) as get_database: + observations = list(workflow_metrics.get_task_metrics()) + + self.assertEqual(observations, []) + get_database.assert_not_called() + + +class RegisterTaskMetricsTest(unittest.TestCase): + def test_description_mentions_curated_workflow_labels(self): + metric_creator = mock.Mock() + + with mock.patch.object( + workflow_metrics.metrics.MetricCreator, + 'get_meter_instance', + return_value=metric_creator, + ): + workflow_metrics.register_task_metrics() + + metric_creator.send_observable_gauge.assert_called_once_with( + name='osmo_tasks_count', + callbacks=workflow_metrics.get_task_metrics, + description=( + 'Count of OSMO tasks by status, pool, workflow, ' + 'and prefixed curated workflow labels' + ), + unit='count', + ) + + +if __name__ == '__main__': + unittest.main() diff --git a/src/service/core/workflow/workflow_metrics.py b/src/service/core/workflow/workflow_metrics.py index 2c8985f2fc..31fdad5887 100644 --- a/src/service/core/workflow/workflow_metrics.py +++ b/src/service/core/workflow/workflow_metrics.py @@ -1,5 +1,5 @@ """ -SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # pylint: disable=line-too-long Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,10 +16,12 @@ SPDX-License-Identifier: Apache-2.0 """ +from collections.abc import Mapping +import json import logging import os import time -from typing import Dict, Iterable, List, Tuple +from typing import Any, Dict, Iterable, List, Tuple # Import with type: ignore to avoid import errors in linting import opentelemetry.metrics as otelmetrics # type: ignore @@ -34,6 +36,15 @@ _metric_cache: List[otelmetrics.Observation] = [] _last_refresh_time: float = 0 _CACHE_TTL_SECONDS: int = 30 # Refresh cache every 30 seconds +_MISSING_WORKFLOW_LABEL_VALUE = '' +_OTHER_WORKFLOW_LABEL_VALUE = '' +_WORKFLOW_LABEL_ATTRIBUTE_PREFIX = 'workflow_label_' +_WORKFLOW_LABEL_ATTRIBUTE_ESCAPES = { + '_': '__', + '-': '_dash_', + '.': '_dot_', + '/': '_slash_', +} def _is_task_metrics_disabled() -> bool: @@ -43,6 +54,43 @@ def _is_task_metrics_disabled() -> bool: ) +def _parse_workflow_labels(raw_labels: Any) -> Dict[str, str]: + """Return string workflow labels from a decoded JSONB value or JSON text.""" + if isinstance(raw_labels, str): + try: + raw_labels = json.loads(raw_labels) + except json.JSONDecodeError: + return {} + if not isinstance(raw_labels, Mapping): + return {} + return { + key: value + for key, value in raw_labels.items() + if isinstance(key, str) and isinstance(value, str) + } + + +def _workflow_label_attribute_name(label_key: str) -> str: + """Build a readable, collision-free Prometheus-safe attribute name.""" + encoded_key = ''.join( + _WORKFLOW_LABEL_ATTRIBUTE_ESCAPES.get(character, character) + for character in label_key + ) + return f'{_WORKFLOW_LABEL_ATTRIBUTE_PREFIX}{encoded_key}' + + +def _workflow_label_metric_value( + workflow_labels: Dict[str, str], + label_policy: connectors.LabelPolicy) -> str: + """Clamp a policy label to its bounded metric vocabulary.""" + value = workflow_labels.get(label_policy.key) + if value is None: + return _MISSING_WORKFLOW_LABEL_VALUE + if not label_policy.allow_list or value not in label_policy.allow_list: + return _OTHER_WORKFLOW_LABEL_VALUE + return value + + def get_task_metrics( *args, # pylint: disable=unused-argument minutes_ago: int = 5 @@ -94,8 +142,11 @@ def get_task_metrics( prev_age ) + label_policies: List[connectors.LabelPolicy] = [] try: database = connectors.PostgresConnector.get_instance() + workflow_config = database.get_workflow_configs() + label_policies = workflow_config.labels_config.policy rows = helpers.get_recent_tasks(database, minutes_ago) except osmo_errors.OSMODatabaseError as err: logging.debug( @@ -104,17 +155,25 @@ def get_task_metrics( ) rows = [] - # Count tasks by unique label combinations + # Rows arrive pre-aggregated by (pool, user, workflow_uuid, status, labels); + # workflow_uuid is a metric dimension, so keys are unique per row today. The + # dict guards against duplicate series if the SQL grouping ever loosens. task_counts: Dict[Tuple[Tuple[str, str], ...], int] = {} for row in rows: + workflow_labels = _parse_workflow_labels(row['labels']) labels = { 'pool': row['pool'] or 'unknown', 'user': row['user'], 'workflow_uuid': row['workflow_uuid'], 'status': row['status'] } + labels.update({ + _workflow_label_attribute_name(label_policy.key): + _workflow_label_metric_value(workflow_labels, label_policy) + for label_policy in label_policies + }) key = tuple(sorted(labels.items())) - task_counts[key] = task_counts.get(key, 0) + 1 + task_counts[key] = task_counts.get(key, 0) + int(row['count']) # Generate observations _metric_cache.clear() @@ -140,7 +199,10 @@ def register_task_metrics(): metric_creator.send_observable_gauge( name='osmo_tasks_count', callbacks=get_task_metrics, - description='Count of OSMO tasks by status, pool, workflow', + description=( + 'Count of OSMO tasks by status, pool, workflow, ' + 'and prefixed curated workflow labels' + ), unit='count' ) except (ValueError, AttributeError, TypeError) as err: From d1843625f9169799bb62a08caa1d3655d5d89153 Mon Sep 17 00:00:00 2001 From: Jiaen Ren Date: Tue, 21 Jul 2026 22:30:08 -0700 Subject: [PATCH 02/12] Apply metrics review simplifications Drop the unreachable JSON-text parsing branch (psycopg2 decodes JSONB to dicts), hoist loop-invariant attribute names, consolidate the triplicated patch scaffolding in the metrics tests, loosen change-detector assertions, and replace the dashboard title pinning with a deployment-neutrality scan. Re-home the osmo_tasks_count label-dimension reference docs from the CLI PR. Co-Authored-By: Claude Fable 5 --- .../dashboards/test_dashboards.py | 20 ++---- .../configs_definitions/workflow.rst | 12 +++- .../core/workflow/tests/test_helpers.py | 5 +- .../workflow/tests/test_workflow_metrics.py | 65 +++++++++---------- src/service/core/workflow/workflow_metrics.py | 22 +++---- 5 files changed, 56 insertions(+), 68 deletions(-) diff --git a/docs/deployment_guide/dashboards/test_dashboards.py b/docs/deployment_guide/dashboards/test_dashboards.py index ce33ea9b3d..dbc188580b 100644 --- a/docs/deployment_guide/dashboards/test_dashboards.py +++ b/docs/deployment_guide/dashboards/test_dashboards.py @@ -51,25 +51,15 @@ class WorkflowResourcesDashboardTest(unittest.TestCase): def setUpClass(cls) -> None: cls.dashboard = _load_dashboard('workflow_resources_usage.json') - def test_preserves_workflow_resource_panels(self): - panel_titles = {panel['title'] for panel in self.dashboard['panels']} - self.assertTrue({ - 'CPU Usage', - 'Memory Usage', - 'Disk Usage', - 'GPU Utilization', - 'GPU Memory Usage', - 'GPU Node Conditions', - 'GPU Usage', - 'GPU Throttle', - }.issubset(panel_titles)) - - def test_workflow_selector_is_not_bound_to_ppp(self): + def test_has_workflow_resource_panels(self): + self.assertGreaterEqual(len(self.dashboard['panels']), 8) + + def test_dashboard_is_deployment_neutral(self): variables = _variables(self.dashboard) self.assertNotIn('PPP', variables) uuid_query = _query(variables['uuid']) self.assertIn('kube_pod_info', uuid_query) - self.assertNotIn('label_PPP', uuid_query) + self.assertNotIn('label_PPP', json.dumps(self.dashboard)) def test_panel_ids_are_unique(self): panel_ids = [panel['id'] for panel in self.dashboard['panels']] diff --git a/docs/deployment_guide/references/configs_definitions/workflow.rst b/docs/deployment_guide/references/configs_definitions/workflow.rst index 22d2d5c11b..d014aae887 100644 --- a/docs/deployment_guide/references/configs_definitions/workflow.rst +++ b/docs/deployment_guide/references/configs_definitions/workflow.rst @@ -310,7 +310,17 @@ Existing and in-flight workflows are not modified, although their detail-page warnings always reflect the current warn policy. In ConfigMap mode, an invalid edit is rejected and the previous valid snapshot remains active. -Admission emits +Only configured policy keys become workflow-label dimensions on +``osmo_tasks_count``. Attribute names start with ``workflow_label_``. Letters +and numbers are unchanged; ``_``, ``-``, ``.``, and ``/`` are encoded as +``__``, ``_dash_``, ``_dot_``, and ``_slash_`` respectively. For example, +``PPP`` is exported as ``workflow_label_PPP``. Values in the configured +allow-list are exported verbatim; a present value outside that list is clamped +to ````, and a missing key is reported as ````. An empty +allow-list therefore exports every present value as ````. This keeps +the number of series bounded to the allow-list plus two sentinels per key. + +Admission also emits ``osmo_label_validation_total{key, outcome}``, where ``outcome`` is ``ok``, ``missing``, ``invalid``, or ``rejected``. The counter covers rejected submissions that do not create a workflow row. Keep the policy list small to diff --git a/src/service/core/workflow/tests/test_helpers.py b/src/service/core/workflow/tests/test_helpers.py index 5411f1ac2e..f5974c893d 100644 --- a/src/service/core/workflow/tests/test_helpers.py +++ b/src/service/core/workflow/tests/test_helpers.py @@ -604,9 +604,8 @@ def test_get_recent_tasks_selects_labels_and_count(self): query = database.execute_fetch_command.call_args.args[0] self.assertIn('w.labels AS labels', query) self.assertIn('COUNT(*) AS count', query) - self.assertIn( - 'GROUP BY w.pool, w.submitted_by, w.workflow_uuid, t.status, w.labels', - query) + group_by_clause = query[query.index('GROUP BY'):] + self.assertIn('w.labels', group_by_clause) def test_get_recent_tasks_passes_cutoff_time_to_database(self): database = mock.Mock() diff --git a/src/service/core/workflow/tests/test_workflow_metrics.py b/src/service/core/workflow/tests/test_workflow_metrics.py index 807642d2eb..ec42b7c9a8 100644 --- a/src/service/core/workflow/tests/test_workflow_metrics.py +++ b/src/service/core/workflow/tests/test_workflow_metrics.py @@ -54,6 +54,14 @@ def tearDown(self): workflow_metrics._metric_cache.clear() # pylint: disable=protected-access workflow_metrics._last_refresh_time = 0 # pylint: disable=protected-access + def _observe(self, database): + with mock.patch.object( + connectors.PostgresConnector, 'get_instance', return_value=database + ), mock.patch.object( + workflow_metrics.time, 'time', return_value=100 + ): + return list(workflow_metrics.get_task_metrics()) + def test_sums_database_counts_and_collapses_non_policy_labels(self): rows = [ { @@ -73,10 +81,11 @@ def test_sums_database_counts_and_collapses_non_policy_labels(self): 'user': 'alice', 'workflow_uuid': 'workflow-1', 'status': 'RUNNING', - 'labels': ( - '{"PPP":"project-a","cost-center":"center-1",' - '"experiment":"second"}' - ), + 'labels': { + 'PPP': 'project-a', + 'cost-center': 'center-1', + 'experiment': 'second', + }, 'count': 3, }, { @@ -84,7 +93,7 @@ def test_sums_database_counts_and_collapses_non_policy_labels(self): 'user': 'alice', 'workflow_uuid': 'workflow-1', 'status': 'RUNNING', - 'labels': '{"PPP":"project-a"}', + 'labels': {'PPP': 'project-a'}, 'count': 4, }, { @@ -100,7 +109,7 @@ def test_sums_database_counts_and_collapses_non_policy_labels(self): 'user': 'alice', 'workflow_uuid': 'workflow-1', 'status': 'RUNNING', - 'labels': 'not-json', + 'labels': ['unexpected-type'], 'count': 2, }, { @@ -119,13 +128,9 @@ def test_sums_database_counts_and_collapses_non_policy_labels(self): }) with mock.patch.object( - connectors.PostgresConnector, 'get_instance', return_value=database - ), mock.patch.object( - workflow_metrics.helpers, 'get_recent_tasks', return_value=rows - ), mock.patch.object( - workflow_metrics.time, 'time', return_value=100 - ): - observations = list(workflow_metrics.get_task_metrics()) + workflow_metrics.helpers, 'get_recent_tasks', + return_value=rows): + observations = self._observe(database) counts = {} for observation in observations: @@ -184,13 +189,9 @@ def test_policy_label_attributes_cannot_overwrite_or_sanitize_to_same_name(self) }] with mock.patch.object( - connectors.PostgresConnector, 'get_instance', return_value=database - ), mock.patch.object( - workflow_metrics.helpers, 'get_recent_tasks', return_value=rows - ), mock.patch.object( - workflow_metrics.time, 'time', return_value=100 - ): - observations = list(workflow_metrics.get_task_metrics()) + workflow_metrics.helpers, 'get_recent_tasks', + return_value=rows): + observations = self._observe(database) attributes = observations[0].attributes if attributes is None: @@ -216,13 +217,9 @@ def test_no_label_policies_keeps_base_dimensions_and_database_count(self): }] with mock.patch.object( - connectors.PostgresConnector, 'get_instance', return_value=database - ), mock.patch.object( - workflow_metrics.helpers, 'get_recent_tasks', return_value=rows - ), mock.patch.object( - workflow_metrics.time, 'time', return_value=100 - ): - observations = list(workflow_metrics.get_task_metrics()) + workflow_metrics.helpers, 'get_recent_tasks', + return_value=rows): + observations = self._observe(database) self.assertEqual(len(observations), 1) self.assertEqual(observations[0].value, 7) @@ -294,15 +291,11 @@ def test_description_mentions_curated_workflow_labels(self): ): workflow_metrics.register_task_metrics() - metric_creator.send_observable_gauge.assert_called_once_with( - name='osmo_tasks_count', - callbacks=workflow_metrics.get_task_metrics, - description=( - 'Count of OSMO tasks by status, pool, workflow, ' - 'and prefixed curated workflow labels' - ), - unit='count', - ) + metric_creator.send_observable_gauge.assert_called_once() + kwargs = metric_creator.send_observable_gauge.call_args.kwargs + self.assertEqual(kwargs['name'], 'osmo_tasks_count') + self.assertEqual(kwargs['callbacks'], workflow_metrics.get_task_metrics) + self.assertIn('workflow labels', kwargs['description']) if __name__ == '__main__': diff --git a/src/service/core/workflow/workflow_metrics.py b/src/service/core/workflow/workflow_metrics.py index 31fdad5887..9fa89c1f81 100644 --- a/src/service/core/workflow/workflow_metrics.py +++ b/src/service/core/workflow/workflow_metrics.py @@ -17,7 +17,6 @@ """ from collections.abc import Mapping -import json import logging import os import time @@ -55,18 +54,13 @@ def _is_task_metrics_disabled() -> bool: def _parse_workflow_labels(raw_labels: Any) -> Dict[str, str]: - """Return string workflow labels from a decoded JSONB value or JSON text.""" - if isinstance(raw_labels, str): - try: - raw_labels = json.loads(raw_labels) - except json.JSONDecodeError: - return {} + """Return string workflow labels from a decoded JSONB row value.""" if not isinstance(raw_labels, Mapping): return {} return { key: value for key, value in raw_labels.items() - if isinstance(key, str) and isinstance(value, str) + if isinstance(value, str) } @@ -158,6 +152,10 @@ def get_task_metrics( # Rows arrive pre-aggregated by (pool, user, workflow_uuid, status, labels); # workflow_uuid is a metric dimension, so keys are unique per row today. The # dict guards against duplicate series if the SQL grouping ever loosens. + policy_attributes = [ + (_workflow_label_attribute_name(label_policy.key), label_policy) + for label_policy in label_policies + ] task_counts: Dict[Tuple[Tuple[str, str], ...], int] = {} for row in rows: workflow_labels = _parse_workflow_labels(row['labels']) @@ -167,11 +165,9 @@ def get_task_metrics( 'workflow_uuid': row['workflow_uuid'], 'status': row['status'] } - labels.update({ - _workflow_label_attribute_name(label_policy.key): - _workflow_label_metric_value(workflow_labels, label_policy) - for label_policy in label_policies - }) + for attribute_name, label_policy in policy_attributes: + labels[attribute_name] = _workflow_label_metric_value( + workflow_labels, label_policy) key = tuple(sorted(labels.items())) task_counts[key] = task_counts.get(key, 0) + int(row['count']) From ddc97beb140fc9fdd4e69a734e907a7c4f8dfa6b Mon Sep 17 00:00:00 2001 From: Jiaen Ren Date: Wed, 22 Jul 2026 14:57:09 -0700 Subject: [PATCH 03/12] Polish metrics comments and dashboard test docs Correct the aggregation comment (clamping can collapse distinct rows today), explain the sentinel angle-bracket choice at both the code and docs sites, refresh the stale get_recent_tasks docstring, and note the Grafana schema quirk behind the query helper. Co-Authored-By: Claude Fable 5 --- .../deployment_guide/dashboards/test_dashboards.py | 7 +++++-- .../references/configs_definitions/workflow.rst | 8 +++++--- src/service/core/workflow/helpers.py | 3 ++- .../core/workflow/tests/test_workflow_metrics.py | 12 +++--------- src/service/core/workflow/workflow_metrics.py | 14 ++++++++++---- 5 files changed, 25 insertions(+), 19 deletions(-) diff --git a/docs/deployment_guide/dashboards/test_dashboards.py b/docs/deployment_guide/dashboards/test_dashboards.py index dbc188580b..dae971a985 100644 --- a/docs/deployment_guide/dashboards/test_dashboards.py +++ b/docs/deployment_guide/dashboards/test_dashboards.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # pylint: disable=line-too-long +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -38,12 +38,14 @@ def _variables(dashboard: dict[str, Any]) -> dict[str, dict[str, Any]]: def _query(variable: dict[str, Any]) -> str: + # Grafana stores template-variable queries as either a raw string or a + # {query: ...} object depending on the dashboard schema version. query = variable.get('query', '') return query.get('query', '') if isinstance(query, dict) else query class WorkflowResourcesDashboardTest(unittest.TestCase): - """The OSS workflow dashboard remains deployment-neutral.""" + """Structural and deployment-neutrality checks for the OSS dashboard.""" dashboard: dict[str, Any] @@ -52,6 +54,7 @@ def setUpClass(cls) -> None: cls.dashboard = _load_dashboard('workflow_resources_usage.json') def test_has_workflow_resource_panels(self): + # Floor, not an exact count: panels may be added freely. self.assertGreaterEqual(len(self.dashboard['panels']), 8) def test_dashboard_is_deployment_neutral(self): diff --git a/docs/deployment_guide/references/configs_definitions/workflow.rst b/docs/deployment_guide/references/configs_definitions/workflow.rst index d014aae887..0240a5f898 100644 --- a/docs/deployment_guide/references/configs_definitions/workflow.rst +++ b/docs/deployment_guide/references/configs_definitions/workflow.rst @@ -316,9 +316,11 @@ and numbers are unchanged; ``_``, ``-``, ``.``, and ``/`` are encoded as ``__``, ``_dash_``, ``_dot_``, and ``_slash_`` respectively. For example, ``PPP`` is exported as ``workflow_label_PPP``. Values in the configured allow-list are exported verbatim; a present value outside that list is clamped -to ````, and a missing key is reported as ````. An empty -allow-list therefore exports every present value as ````. This keeps -the number of series bounded to the allow-list plus two sentinels per key. +to ````, and a missing key is reported as ````. Angle +brackets are not valid in label values, so the sentinels never collide with +real values. An empty allow-list exports every present value as ````. +This keeps the number of series bounded to the allow-list plus two sentinels +per key. Admission also emits ``osmo_label_validation_total{key, outcome}``, where ``outcome`` is ``ok``, diff --git a/src/service/core/workflow/helpers.py b/src/service/core/workflow/helpers.py index e0bbd275b8..a7245df118 100644 --- a/src/service/core/workflow/helpers.py +++ b/src/service/core/workflow/helpers.py @@ -531,7 +531,8 @@ def get_recent_tasks(database: connectors.PostgresConnector, minutes_ago: How many minutes back to look for completed tasks Returns: - List of task records with task and workflow information + Aggregated task-count rows grouped by pool, user, workflow, task + status, and the workflow's labels """ now = datetime.datetime.now(datetime.timezone.utc) cutoff_time = now - datetime.timedelta(minutes=minutes_ago) diff --git a/src/service/core/workflow/tests/test_workflow_metrics.py b/src/service/core/workflow/tests/test_workflow_metrics.py index ec42b7c9a8..f640974889 100644 --- a/src/service/core/workflow/tests/test_workflow_metrics.py +++ b/src/service/core/workflow/tests/test_workflow_metrics.py @@ -155,16 +155,10 @@ def test_sums_database_counts_and_collapses_non_policy_labels(self): def test_empty_allow_list_clamps_all_present_values_to_other(self): label_policy = connectors.LabelPolicy(key='PPP') + metric_value = workflow_metrics._workflow_label_metric_value # pylint: disable=protected-access self.assertEqual( - workflow_metrics._workflow_label_metric_value( - {'PPP': 'arbitrary'}, label_policy, - ), - '', - ) - self.assertEqual( - workflow_metrics._workflow_label_metric_value({}, label_policy), - '', - ) + metric_value({'PPP': 'arbitrary'}, label_policy), '') + self.assertEqual(metric_value({}, label_policy), '') def test_policy_label_attributes_cannot_overwrite_or_sanitize_to_same_name(self): database = mock.Mock() diff --git a/src/service/core/workflow/workflow_metrics.py b/src/service/core/workflow/workflow_metrics.py index 9fa89c1f81..2dc75eb955 100644 --- a/src/service/core/workflow/workflow_metrics.py +++ b/src/service/core/workflow/workflow_metrics.py @@ -35,6 +35,8 @@ _metric_cache: List[otelmetrics.Observation] = [] _last_refresh_time: float = 0 _CACHE_TTL_SECONDS: int = 30 # Refresh cache every 30 seconds +# Angle brackets are not valid label characters, so these sentinels can +# never collide with real label values. _MISSING_WORKFLOW_LABEL_VALUE = '' _OTHER_WORKFLOW_LABEL_VALUE = '' _WORKFLOW_LABEL_ATTRIBUTE_PREFIX = 'workflow_label_' @@ -76,7 +78,11 @@ def _workflow_label_attribute_name(label_key: str) -> str: def _workflow_label_metric_value( workflow_labels: Dict[str, str], label_policy: connectors.LabelPolicy) -> str: - """Clamp a policy label to its bounded metric vocabulary.""" + """Clamp a policy label to its bounded metric vocabulary. + + With an empty allow-list every present value clamps to the other + sentinel, so series stay bounded in every enforcement mode. + """ value = workflow_labels.get(label_policy.key) if value is None: return _MISSING_WORKFLOW_LABEL_VALUE @@ -149,13 +155,13 @@ def get_task_metrics( ) rows = [] - # Rows arrive pre-aggregated by (pool, user, workflow_uuid, status, labels); - # workflow_uuid is a metric dimension, so keys are unique per row today. The - # dict guards against duplicate series if the SQL grouping ever loosens. policy_attributes = [ (_workflow_label_attribute_name(label_policy.key), label_policy) for label_policy in label_policies ] + # SQL groups by the raw labels JSONB; clamping to the bounded policy + # vocabulary can collapse distinct rows onto the same attribute set, so + # counts are summed per projected key. task_counts: Dict[Tuple[Tuple[str, str], ...], int] = {} for row in rows: workflow_labels = _parse_workflow_labels(row['labels']) From f3bcad813c1d0d37f2d3b41003257a1958d5a94b Mon Sep 17 00:00:00 2001 From: Jiaen Ren Date: Mon, 27 Jul 2026 16:56:59 -0700 Subject: [PATCH 04/12] Use a generic label key in metrics tests and docs Replace the NVIDIA-internal 'PPP' example key with the generic 'project' in the metrics test, dashboard-neutrality test, and the metrics docs, to match merged #1220. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../dashboards/test_dashboards.py | 4 ++-- .../configs_definitions/workflow.rst | 2 +- .../workflow/tests/test_workflow_metrics.py | 20 +++++++++---------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/deployment_guide/dashboards/test_dashboards.py b/docs/deployment_guide/dashboards/test_dashboards.py index dae971a985..285da04b54 100644 --- a/docs/deployment_guide/dashboards/test_dashboards.py +++ b/docs/deployment_guide/dashboards/test_dashboards.py @@ -59,10 +59,10 @@ def test_has_workflow_resource_panels(self): def test_dashboard_is_deployment_neutral(self): variables = _variables(self.dashboard) - self.assertNotIn('PPP', variables) + self.assertNotIn('project', variables) uuid_query = _query(variables['uuid']) self.assertIn('kube_pod_info', uuid_query) - self.assertNotIn('label_PPP', json.dumps(self.dashboard)) + self.assertNotIn('label_project', json.dumps(self.dashboard)) def test_panel_ids_are_unique(self): panel_ids = [panel['id'] for panel in self.dashboard['panels']] diff --git a/docs/deployment_guide/references/configs_definitions/workflow.rst b/docs/deployment_guide/references/configs_definitions/workflow.rst index 0240a5f898..28ad9a910a 100644 --- a/docs/deployment_guide/references/configs_definitions/workflow.rst +++ b/docs/deployment_guide/references/configs_definitions/workflow.rst @@ -314,7 +314,7 @@ Only configured policy keys become workflow-label dimensions on ``osmo_tasks_count``. Attribute names start with ``workflow_label_``. Letters and numbers are unchanged; ``_``, ``-``, ``.``, and ``/`` are encoded as ``__``, ``_dash_``, ``_dot_``, and ``_slash_`` respectively. For example, -``PPP`` is exported as ``workflow_label_PPP``. Values in the configured +``project`` is exported as ``workflow_label_project``. Values in the configured allow-list are exported verbatim; a present value outside that list is clamped to ````, and a missing key is reported as ````. Angle brackets are not valid in label values, so the sentinels never collide with diff --git a/src/service/core/workflow/tests/test_workflow_metrics.py b/src/service/core/workflow/tests/test_workflow_metrics.py index f640974889..b80793f624 100644 --- a/src/service/core/workflow/tests/test_workflow_metrics.py +++ b/src/service/core/workflow/tests/test_workflow_metrics.py @@ -70,7 +70,7 @@ def test_sums_database_counts_and_collapses_non_policy_labels(self): 'workflow_uuid': 'workflow-1', 'status': 'RUNNING', 'labels': { - 'PPP': 'project-a', + 'project': 'project-a', 'cost-center': 'center-1', 'experiment': 'first', }, @@ -82,7 +82,7 @@ def test_sums_database_counts_and_collapses_non_policy_labels(self): 'workflow_uuid': 'workflow-1', 'status': 'RUNNING', 'labels': { - 'PPP': 'project-a', + 'project': 'project-a', 'cost-center': 'center-1', 'experiment': 'second', }, @@ -93,7 +93,7 @@ def test_sums_database_counts_and_collapses_non_policy_labels(self): 'user': 'alice', 'workflow_uuid': 'workflow-1', 'status': 'RUNNING', - 'labels': {'PPP': 'project-a'}, + 'labels': {'project': 'project-a'}, 'count': 4, }, { @@ -117,13 +117,13 @@ def test_sums_database_counts_and_collapses_non_policy_labels(self): 'user': 'alice', 'workflow_uuid': 'workflow-1', 'status': 'RUNNING', - 'labels': {'PPP': 'unattributed'}, + 'labels': {'project': 'unattributed'}, 'count': 6, }, ] database = mock.Mock() database.get_workflow_configs.return_value = _workflow_config({ - 'PPP': ['project-a'], + 'project': ['project-a'], 'cost-center': ['center-1'], }) @@ -138,7 +138,7 @@ def test_sums_database_counts_and_collapses_non_policy_labels(self): if attributes is None: self.fail('Task metric observation is missing attributes.') counts[( - attributes['workflow_label_PPP'], + attributes['workflow_label_project'], attributes['workflow_label_cost_dash_center'], )] = observation.value self.assertEqual(attributes['pool'], 'pool-a') @@ -153,11 +153,11 @@ def test_sums_database_counts_and_collapses_non_policy_labels(self): }) def test_empty_allow_list_clamps_all_present_values_to_other(self): - label_policy = connectors.LabelPolicy(key='PPP') + label_policy = connectors.LabelPolicy(key='project') metric_value = workflow_metrics._workflow_label_metric_value # pylint: disable=protected-access self.assertEqual( - metric_value({'PPP': 'arbitrary'}, label_policy), '') + metric_value({'project': 'arbitrary'}, label_policy), '') self.assertEqual(metric_value({}, label_policy), '') def test_policy_label_attributes_cannot_overwrite_or_sanitize_to_same_name(self): @@ -229,13 +229,13 @@ def test_no_label_policies_keeps_base_dimensions_and_database_count(self): def test_cache_is_reused_until_30_second_ttl_expires(self): database = mock.Mock() - database.get_workflow_configs.return_value = _workflow_config({'PPP': ['project-a']}) + database.get_workflow_configs.return_value = _workflow_config({'project': ['project-a']}) first_rows = [{ 'pool': 'pool-a', 'user': 'alice', 'workflow_uuid': 'workflow-1', 'status': 'RUNNING', - 'labels': {'PPP': 'project-a'}, + 'labels': {'project': 'project-a'}, 'count': 2, }] refreshed_rows = [{ From e0fd724949802dba03e0e8fb165bf3f3bafb8e73 Mon Sep 17 00:00:00 2001 From: Jiaen Ren Date: Tue, 21 Jul 2026 22:04:38 -0700 Subject: [PATCH 05/12] Add workflow label UI support Workflow list gains a Labels column and label/no-label filter chips wired to the list API; the detail page shows the stored labels and the recomputed warn-mode policy warnings; the resubmit panel carries labels forward and lets users edit them; the new-workflow submit page reads labels from the spec editor with an optional override editor. Adds a shared workflow-labels formatting module, adapter shims for the new response fields, and unit plus Playwright coverage. Co-Authored-By: Claude Fable 5 --- .../e2e/journeys/submit-workflow-form.spec.ts | 73 +++++++++++ .../journeys/workflow-detail-overview.spec.ts | 77 +++++++++--- src/ui/e2e/journeys/workflow-filters.spec.ts | 29 +++++ .../journeys/workflow-resubmit-panel.spec.ts | 77 +++++++++--- .../submit-workflow-config-panel.tsx | 12 ++ .../submit-workflow-content.tsx | 1 + .../use-submit-workflow-form.ts | 22 +++- .../workflow/workflow-label-editor.tsx | 117 ++++++++++++++++++ .../panel/ui/workflow/workflow-details.tsx | 51 ++++++++ .../resubmit/resubmit-panel-content.tsx | 22 +++- .../components/resubmit/use-resubmit-form.ts | 36 +++++- .../resubmit/use-resubmit-mutation.ts | 4 +- .../components/table/workflow-column-defs.tsx | 21 ++++ .../list/components/workflows-toolbar.tsx | 4 +- .../workflows/list/lib/actions.test.ts | 59 +++++++++ .../features/workflows/list/lib/actions.ts | 34 ++--- .../list/lib/workflow-columns.test.ts | 32 +++++ .../workflows/list/lib/workflow-columns.ts | 13 +- .../list/lib/workflow-search-fields.test.ts | 25 ++++ .../list/lib/workflow-search-fields.ts | 16 +++ src/ui/src/lib/api/adapter/types.ts | 2 + .../lib/api/adapter/workflows-shim.test.ts | 86 +++++++++++++ src/ui/src/lib/api/adapter/workflows-shim.ts | 12 +- src/ui/src/lib/api/server/workflows.ts | 10 ++ src/ui/src/lib/workflow-labels.test.ts | 62 ++++++++++ src/ui/src/lib/workflow-labels.ts | 62 ++++++++++ 26 files changed, 895 insertions(+), 64 deletions(-) create mode 100644 src/ui/src/components/workflow/workflow-label-editor.tsx create mode 100644 src/ui/src/features/workflows/list/lib/actions.test.ts create mode 100644 src/ui/src/features/workflows/list/lib/workflow-columns.test.ts create mode 100644 src/ui/src/lib/api/adapter/workflows-shim.test.ts create mode 100644 src/ui/src/lib/workflow-labels.test.ts create mode 100644 src/ui/src/lib/workflow-labels.ts diff --git a/src/ui/e2e/journeys/submit-workflow-form.spec.ts b/src/ui/e2e/journeys/submit-workflow-form.spec.ts index 590bb0571b..d5d9ff333a 100644 --- a/src/ui/e2e/journeys/submit-workflow-form.spec.ts +++ b/src/ui/e2e/journeys/submit-workflow-form.spec.ts @@ -233,6 +233,79 @@ test.describe("Submit Workflow Form Validation", () => { // ASSERT — High is now checked await expect(overlay.getByRole("radio", { name: "High priority" })).toBeChecked(); }); + + test("submits YAML labels in the body without a separate label query override", async ({ page }) => { + test.setTimeout(30_000); + let submittedLabels: string[] | null = null; + let submittedBody: string | null = null; + await page.route("**/api/pool/test-pool/workflow*", (route) => { + const url = new URL(route.request().url()); + if (!url.searchParams.has("validation_only") && !url.searchParams.has("dry_run")) { + submittedLabels = url.searchParams.getAll("label"); + submittedBody = route.request().postData(); + return route.fulfill({ + status: 200, + contentType: CT_JSON, + body: JSON.stringify({ + name: "yaml-labels", + logs: "/api/workflow/yaml-labels/logs", + warnings: [], + }), + }); + } + return route.fulfill({ status: 404, contentType: CT_JSON, body: '{"detail":"Not mocked"}' }); + }); + + const overlay = await openFormView(page); + await waitForPoolSelected(overlay, "test-pool"); + const editor = overlay.getByRole("textbox", { name: "YAML workflow specification editor" }); + await editor.click(); + await page.keyboard.insertText("workflow:\n labels:\n PPP: robotics\n tasks:\n - name: hello"); + + await expect(overlay.getByText("Workflow Labels", { exact: true })).toHaveCount(0); + await expect(overlay.getByRole("button", { name: "Add workflow label" })).toHaveCount(0); + await overlay.getByRole("button", { name: "Submit workflow", exact: true }).click(); + + await expect.poll(() => submittedLabels).toEqual([]); + await expect.poll(() => submittedBody).toContain("labels:"); + await expect.poll(() => submittedBody).toContain("PPP: robotics"); + await expect(page.getByText("Workflow submitted as yaml-labels")).toBeVisible(); + }); + + test("shows workflow policy warnings returned by validation", async ({ page }) => { + const warning = "Workflow is missing label 'PPP'; add it now to avoid rejected submissions once it is required."; + let submittedLabels: string[] | null = null; + await page.route("**/api/pool/test-pool/workflow*", (route) => { + const url = new URL(route.request().url()); + if (url.searchParams.get("validation_only") === "true") { + submittedLabels = url.searchParams.getAll("label"); + return route.fulfill({ + status: 200, + contentType: CT_JSON, + body: JSON.stringify({ + name: "hello-osmo", + logs: "Workflow spec is valid.", + warnings: [warning], + }), + }); + } + return route.fulfill({ status: 404, contentType: CT_JSON, body: '{"detail":"Not mocked"}' }); + }); + + const overlay = await openFormView(page); + await waitForPoolSelected(overlay, "test-pool"); + const editor = overlay.getByRole("textbox", { name: "YAML workflow specification editor" }); + await editor.click(); + await page.keyboard.insertText("workflow:\n tasks:\n - name: hello"); + + await overlay.getByRole("button", { name: "More workflow options" }).click(); + const validateItem = page.getByRole("menuitem", { name: /validate/i }); + await expect(validateItem).toBeVisible(); + await validateItem.click(); + + await expect.poll(() => submittedLabels).toEqual([]); + await expect(overlay.getByText(warning)).toBeVisible(); + }); }); test.describe("Submit Workflow Localpath Warnings", () => { diff --git a/src/ui/e2e/journeys/workflow-detail-overview.spec.ts b/src/ui/e2e/journeys/workflow-detail-overview.spec.ts index 7cc429d36d..faa89ab1dd 100644 --- a/src/ui/e2e/journeys/workflow-detail-overview.spec.ts +++ b/src/ui/e2e/journeys/workflow-detail-overview.spec.ts @@ -49,6 +49,8 @@ function createWorkflowDetailResponse( outputs?: string; priority?: string; tags?: string[]; + labels?: Record; + warnings?: string[]; groups?: Array<{ name: string; status?: string; @@ -75,9 +77,7 @@ function createWorkflowDetailResponse( WorkflowStatus.FAILED, WorkflowStatus.FAILED_CANCELED, ]; - const isTerminal = terminalWorkflowStatuses.includes( - (overrides.status as WorkflowStatus) ?? WorkflowStatus.RUNNING, - ); + const isTerminal = terminalWorkflowStatuses.includes((overrides.status as WorkflowStatus) ?? WorkflowStatus.RUNNING); return { name, @@ -94,6 +94,8 @@ function createWorkflowDetailResponse( dashboard_url: overrides.dashboard_url !== undefined ? overrides.dashboard_url : null, grafana_url: overrides.grafana_url !== undefined ? overrides.grafana_url : null, tags: overrides.tags ?? [], + labels: overrides.labels ?? {}, + warnings: overrides.warnings ?? [], submit_time: twoHoursAgo.toISOString(), start_time: twoHoursAgo.toISOString(), end_time: isTerminal ? oneHourAgo.toISOString() : null, @@ -289,9 +291,7 @@ test.describe("Workflow Detail Overview — Failed Workflow", () => { await page.waitForLoadState("networkidle"); // ASSERT — Cancel Workflow button is disabled - await expect( - page.getByRole("button", { name: /cancel workflow/i }).first(), - ).toBeDisabled(); + await expect(page.getByRole("button", { name: /cancel workflow/i }).first()).toBeDisabled(); }); }); @@ -331,11 +331,7 @@ test.describe("Workflow Detail Overview — Details Section", () => { test("shows UUID with copy button", async ({ page }) => { const wfName = "uuid-wf"; - await setupWorkflowDetail( - page, - wfName, - createWorkflowDetailResponse(wfName), - ); + await setupWorkflowDetail(page, wfName, createWorkflowDetailResponse(wfName)); // ACT await page.goto(`/workflows/${wfName}`); @@ -346,14 +342,61 @@ test.describe("Workflow Detail Overview — Details Section", () => { await expect(page.getByText(`uuid-${wfName}`).first()).toBeVisible(); }); - test("user name links to workflows filtered by user", async ({ page }) => { - const wfName = "user-link-wf"; + test("shows immutable workflow labels separately from tags", async ({ page }) => { + test.setTimeout(30_000); + const wfName = "labels-wf"; + await setupWorkflowDetail( + page, + wfName, + createWorkflowDetailResponse(wfName, { + labels: { team: "robotics", experiment: "run42" }, + tags: ["mutable-tag"], + }), + ); + + await page.goto(`/workflows/${wfName}`); + await page.waitForLoadState("networkidle"); + + await expect(page.getByText("Labels", { exact: true })).toBeVisible(); + const teamLabelLink = page.getByRole("link", { name: "team=robotics", exact: true }); + const experimentLabelLink = page.getByRole("link", { name: "experiment=run42", exact: true }); + await expect(teamLabelLink).toHaveAttribute("href", "/workflows?f=label:team%3Drobotics&all=true"); + await expect(experimentLabelLink).toHaveAttribute("href", "/workflows?f=label:experiment%3Drun42&all=true"); + + const teamLabelUrl = new URL((await teamLabelLink.getAttribute("href"))!, "https://osmo.invalid"); + expect(teamLabelUrl.searchParams.get("f")).toBe("label:team=robotics"); + expect(teamLabelUrl.searchParams.get("all")).toBe("true"); + await expect(page.getByText("Tags", { exact: true })).toBeVisible(); + }); + + test("shows current workflow policy warnings on completed workflows", async ({ page }) => { + // The backend recomputes warnings from the current policy for every + // status, including COMPLETED, so users see violations on finished runs. + const wfName = "warnings-wf"; + const warning = + "Workflow is missing label 'PPP'; add it now to avoid rejected submissions once it is required."; await setupWorkflowDetail( page, wfName, - createWorkflowDetailResponse(wfName), + createWorkflowDetailResponse(wfName, { + status: WorkflowStatus.COMPLETED, + warnings: [warning], + }), ); + await page.goto(`/workflows/${wfName}`); + await page.waitForLoadState("networkidle"); + + const warningRegion = page.getByRole("region", { name: "Workflow policy warnings" }); + await expect(warningRegion).toBeVisible(); + await expect(warningRegion.getByText("Workflow label policy")).toBeVisible(); + await expect(warningRegion.getByText(warning)).toBeVisible(); + }); + + test("user name links to workflows filtered by user", async ({ page }) => { + const wfName = "user-link-wf"; + await setupWorkflowDetail(page, wfName, createWorkflowDetailResponse(wfName)); + // ACT await page.goto(`/workflows/${wfName}`); await page.waitForLoadState("networkidle"); @@ -366,11 +409,7 @@ test.describe("Workflow Detail Overview — Details Section", () => { test("pool name links to workflows filtered by pool", async ({ page }) => { const wfName = "pool-link-wf"; - await setupWorkflowDetail( - page, - wfName, - createWorkflowDetailResponse(wfName, { pool: "production-pool" }), - ); + await setupWorkflowDetail(page, wfName, createWorkflowDetailResponse(wfName, { pool: "production-pool" })); // ACT await page.goto(`/workflows/${wfName}`); diff --git a/src/ui/e2e/journeys/workflow-filters.spec.ts b/src/ui/e2e/journeys/workflow-filters.spec.ts index fa8a85bc5d..b98ecd0022 100644 --- a/src/ui/e2e/journeys/workflow-filters.spec.ts +++ b/src/ui/e2e/journeys/workflow-filters.spec.ts @@ -246,4 +246,33 @@ test.describe("Workflow URL Filter State", () => { await expect(page.locator("body")).not.toBeEmpty(); await expect(page).toHaveURL(/f=pool(%3A|:)production/); }); + + test("forwards wildcard and inline-alternative workflow label selectors unchanged", async ({ page }) => { + const response = createWorkflowsResponse([ + { name: "label-wf", status: WorkflowStatus.RUNNING, user: "test-user", labels: { PPP: "robotics_team" } }, + ]); + const observedLabelSelectors: string[][] = []; + await page.route("**/api/workflow?*", (route) => { + observedLabelSelectors.push(new URL(route.request().url()).searchParams.getAll("label")); + return route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify(response), + }); + }); + + await page.goto("/workflows?all=true"); + await page.waitForLoadState("networkidle"); + + const selectors = ["PPP=(team_*|osmo_*)", "PPP=team_(a|b)"]; + const filterInput = page.getByRole("combobox", { name: /search and filter/i }); + for (const selector of selectors) { + await filterInput.fill(`label:${selector}`); + await filterInput.press("Enter"); + } + + await expect + .poll(() => observedLabelSelectors.some((observed) => selectors.every((selector) => observed.includes(selector)))) + .toBe(true); + }); }); diff --git a/src/ui/e2e/journeys/workflow-resubmit-panel.spec.ts b/src/ui/e2e/journeys/workflow-resubmit-panel.spec.ts index e95fac88ee..6a9b8a9bbd 100644 --- a/src/ui/e2e/journeys/workflow-resubmit-panel.spec.ts +++ b/src/ui/e2e/journeys/workflow-resubmit-panel.spec.ts @@ -110,6 +110,7 @@ function createCompletedWorkflow(name: string) { app_name: null, app_version: null, plugins: { rsync: false }, + labels: { PPP: "robotics" }, }; } @@ -133,10 +134,7 @@ test.describe("Workflow Resubmit Panel", () => { ); // Setup pools for pool picker - await setupPools( - page, - createPoolResponse([{ name: "test-pool", status: PoolStatus.ONLINE }]), - ); + await setupPools(page, createPoolResponse([{ name: "test-pool", status: PoolStatus.ONLINE }])); }); test("resubmit button opens panel with workflow name in header", async ({ page }) => { @@ -145,7 +143,10 @@ test.describe("Workflow Resubmit Panel", () => { await page.waitForLoadState("networkidle"); // Click the Resubmit Workflow button - await page.getByRole("button", { name: /resubmit workflow/i }).first().click(); + await page + .getByRole("button", { name: /resubmit workflow/i }) + .first() + .click(); // ASSERT — panel opens with correct aria-label and header content const panel = page.locator(`[aria-label="Resubmit workflow: ${wfName}"]`); @@ -159,7 +160,10 @@ test.describe("Workflow Resubmit Panel", () => { await page.goto(`/workflows/${wfName}`); await page.waitForLoadState("networkidle"); - await page.getByRole("button", { name: /resubmit workflow/i }).first().click(); + await page + .getByRole("button", { name: /resubmit workflow/i }) + .first() + .click(); const panel = page.locator(`[aria-label="Resubmit workflow: ${wfName}"]`); await expect(panel).toBeVisible(); @@ -173,7 +177,10 @@ test.describe("Workflow Resubmit Panel", () => { await page.goto(`/workflows/${wfName}`); await page.waitForLoadState("networkidle"); - await page.getByRole("button", { name: /resubmit workflow/i }).first().click(); + await page + .getByRole("button", { name: /resubmit workflow/i }) + .first() + .click(); const panel = page.locator(`[aria-label="Resubmit workflow: ${wfName}"]`); await expect(panel).toBeVisible(); @@ -187,7 +194,10 @@ test.describe("Workflow Resubmit Panel", () => { await page.goto(`/workflows/${wfName}`); await page.waitForLoadState("networkidle"); - await page.getByRole("button", { name: /resubmit workflow/i }).first().click(); + await page + .getByRole("button", { name: /resubmit workflow/i }) + .first() + .click(); const panel = page.locator(`[aria-label="Resubmit workflow: ${wfName}"]`); await expect(panel).toBeVisible(); @@ -204,13 +214,16 @@ test.describe("Workflow Resubmit Panel", () => { await page.goto(`/workflows/${wfName}`); await page.waitForLoadState("networkidle"); - await page.getByRole("button", { name: /resubmit workflow/i }).first().click(); + await page + .getByRole("button", { name: /resubmit workflow/i }) + .first() + .click(); const panel = page.locator(`[aria-label="Resubmit workflow: ${wfName}"]`); await expect(panel).toBeVisible(); // ASSERT — Workflow Specification section visible - await expect(panel.getByText("Workflow Specification")).toBeVisible(); + await expect(panel.getByText("Workflow Specification", { exact: true })).toBeVisible(); }); test("submit button has correct aria-label with workflow name", async ({ page }) => { @@ -218,15 +231,41 @@ test.describe("Workflow Resubmit Panel", () => { await page.goto(`/workflows/${wfName}`); await page.waitForLoadState("networkidle"); - await page.getByRole("button", { name: /resubmit workflow/i }).first().click(); + await page + .getByRole("button", { name: /resubmit workflow/i }) + .first() + .click(); const panel = page.locator(`[aria-label="Resubmit workflow: ${wfName}"]`); await expect(panel).toBeVisible(); // ASSERT — Submit button exists with proper aria-label - await expect( - panel.getByRole("button", { name: `Submit workflow ${wfName}` }), - ).toBeVisible(); + await expect(panel.getByRole("button", { name: `Submit workflow ${wfName}` })).toBeVisible(); + }); + + test("resubmit preserves existing label keys while allowing value overrides", async ({ page }) => { + test.setTimeout(30_000); + await page.goto(`/workflows/${wfName}`); + await page.waitForLoadState("networkidle"); + await page + .getByRole("button", { name: /resubmit workflow/i }) + .first() + .click(); + + const panel = page.locator(`[aria-label="Resubmit workflow: ${wfName}"]`); + await expect(panel.getByRole("textbox", { name: "Workflow label key 1" })).toHaveValue("PPP"); + await expect(panel.getByRole("textbox", { name: "Workflow label key 1" })).toBeDisabled(); + await expect(panel.getByRole("button", { name: "Remove workflow label 1" })).toBeDisabled(); + await panel.getByRole("textbox", { name: "Workflow label value 1" }).fill("simulation"); + + await panel.getByRole("button", { name: "Add workflow label" }).click(); + await panel.getByRole("textbox", { name: "Workflow label key 2" }).fill("team"); + await panel.getByRole("textbox", { name: "Workflow label value 2" }).fill("robotics"); + + await expect(panel.getByRole("textbox", { name: "Workflow label value 1" })).toHaveValue("simulation"); + await expect(panel.getByRole("textbox", { name: "Workflow label key 2" })).toHaveValue("team"); + await expect(panel.getByRole("textbox", { name: "Workflow label value 2" })).toHaveValue("robotics"); + await expect(panel.getByRole("button", { name: `Submit workflow ${wfName}` })).toBeEnabled(); }); test("submit shows 'Submitting...' while pending", async ({ page }) => { @@ -239,7 +278,10 @@ test.describe("Workflow Resubmit Panel", () => { await page.goto(`/workflows/${wfName}`); await page.waitForLoadState("networkidle"); - await page.getByRole("button", { name: /resubmit workflow/i }).first().click(); + await page + .getByRole("button", { name: /resubmit workflow/i }) + .first() + .click(); const panel = page.locator(`[aria-label="Resubmit workflow: ${wfName}"]`); await expect(panel).toBeVisible(); @@ -259,7 +301,10 @@ test.describe("Workflow Resubmit Panel", () => { await page.goto(`/workflows/${wfName}`); await page.waitForLoadState("networkidle"); - await page.getByRole("button", { name: /resubmit workflow/i }).first().click(); + await page + .getByRole("button", { name: /resubmit workflow/i }) + .first() + .click(); const panel = page.locator(`[aria-label="Resubmit workflow: ${wfName}"]`); await expect(panel).toBeVisible(); diff --git a/src/ui/src/components/submit-workflow/submit-workflow-config-panel.tsx b/src/ui/src/components/submit-workflow/submit-workflow-config-panel.tsx index b477c79566..2033ad8567 100644 --- a/src/ui/src/components/submit-workflow/submit-workflow-config-panel.tsx +++ b/src/ui/src/components/submit-workflow/submit-workflow-config-panel.tsx @@ -85,6 +85,7 @@ export interface SubmitWorkflowConfigPanelProps { isValidatePending: boolean; validationOk: boolean | null; validationError: string | null; + validationWarnings: string[]; canValidate: boolean; onValidate: () => void; } @@ -107,6 +108,7 @@ export const SubmitWorkflowConfigPanel = memo(function SubmitWorkflowConfigPanel isValidatePending, validationOk, validationError, + validationWarnings, canValidate, onValidate, }: SubmitWorkflowConfigPanelProps) { @@ -194,6 +196,16 @@ export const SubmitWorkflowConfigPanel = memo(function SubmitWorkflowConfigPanel Workflow spec is valid )} + {validationWarnings.length > 0 && ( +
+ {validationWarnings.map((warning) => ( +

{warning}

+ ))} +
+ )} {validationError && (
diff --git a/src/ui/src/components/submit-workflow/use-submit-workflow-form.ts b/src/ui/src/components/submit-workflow/use-submit-workflow-form.ts index 28cb17dcf6..f61854afb3 100644 --- a/src/ui/src/components/submit-workflow/use-submit-workflow-form.ts +++ b/src/ui/src/components/submit-workflow/use-submit-workflow-form.ts @@ -60,6 +60,7 @@ interface ValidationState { spec: string; ok: boolean; error: string | null; + warnings: string[]; } export interface UseSubmitWorkflowFormReturn { @@ -86,6 +87,7 @@ export interface UseSubmitWorkflowFormReturn { isValidatePending: boolean; validationOk: boolean | null; validationError: string | null; + validationWarnings: string[]; canValidate: boolean; handleValidate: () => void; // Lifecycle @@ -115,6 +117,10 @@ export function useSubmitWorkflowForm(initialSpec = ""): UseSubmitWorkflowFormRe const isValidationFresh = validationState !== null && validationState.spec === spec; const validationOk = isValidationFresh ? (validationState.ok ? true : null) : null; const validationError = isValidationFresh ? validationState.error : null; + const validationWarnings = useMemo( + () => (isValidationFresh ? validationState.warnings : []), + [isValidationFresh, validationState], + ); // ── Mutation hooks ──────────────────────────────────────────────────────── @@ -122,6 +128,9 @@ export function useSubmitWorkflowForm(initialSpec = ""): UseSubmitWorkflowFormRe mutation: { onSuccess: (response) => { const newName = response.name; + for (const warning of response.warnings ?? []) { + toast.warning(warning); + } toast.success(`Workflow submitted as ${newName}`, { action: { label: "View Workflow", @@ -203,13 +212,18 @@ export function useSubmitWorkflowForm(initialSpec = ""): UseSubmitWorkflowFormRe params: { priority, validation_only: true }, }, { - onSuccess: () => { - setValidationState({ spec: specAtCall, ok: true, error: null }); + onSuccess: (response) => { + setValidationState({ + spec: specAtCall, + ok: true, + error: null, + warnings: response.warnings ?? [], + }); announcer.announce("Workflow spec is valid", "polite"); }, onError: (err) => { const msg = extractErrorMessage(err); - setValidationState({ spec: specAtCall, ok: false, error: msg }); + setValidationState({ spec: specAtCall, ok: false, error: msg, warnings: [] }); announcer.announce(`Validation failed: ${msg}`, "assertive"); }, }, @@ -244,6 +258,7 @@ export function useSubmitWorkflowForm(initialSpec = ""): UseSubmitWorkflowFormRe isValidatePending, validationOk, validationError, + validationWarnings, canValidate, handleValidate, handleClose, @@ -267,6 +282,7 @@ export function useSubmitWorkflowForm(initialSpec = ""): UseSubmitWorkflowFormRe isValidatePending, validationOk, validationError, + validationWarnings, canValidate, handleValidate, handleClose, diff --git a/src/ui/src/components/workflow/workflow-label-editor.tsx b/src/ui/src/components/workflow/workflow-label-editor.tsx new file mode 100644 index 0000000000..aa6b28a67f --- /dev/null +++ b/src/ui/src/components/workflow/workflow-label-editor.tsx @@ -0,0 +1,117 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +"use client"; + +import { Plus, X } from "lucide-react"; +import { Button } from "@/components/shadcn/button"; +import { Input } from "@/components/shadcn/input"; +import { MAX_WORKFLOW_LABELS, type WorkflowLabelDraft } from "@/lib/workflow-labels"; + +export interface WorkflowLabelEditorProps { + labels: WorkflowLabelDraft[]; + onChange: (labels: WorkflowLabelDraft[]) => void; + disabled?: boolean; + error?: string | null; + lockedLabelCount?: number; +} + +export function WorkflowLabelEditor({ + labels, + onChange, + disabled = false, + error, + lockedLabelCount = 0, +}: WorkflowLabelEditorProps) { + const updateLabel = (index: number, field: keyof WorkflowLabelDraft, value: string) => { + onChange(labels.map((label, labelIndex) => (labelIndex === index ? { ...label, [field]: value } : label))); + }; + + const removeLabel = (index: number) => { + onChange(labels.filter((_, labelIndex) => labelIndex !== index)); + }; + + return ( +
+

+ Per-run overrides take precedence over labels in the workflow YAML. + {lockedLabelCount > 0 && + " Existing keys cannot be removed here; edit the workflow specification to remove one."} +

+ {labels.map((label, index) => { + const keyIsLocked = index < lockedLabelCount; + return ( +
+ updateLabel(index, "key", event.target.value)} + /> + updateLabel(index, "value", event.target.value)} + /> + +
+ ); + })} + + {error && ( +

+ {error} +

+ )} +
+ ); +} diff --git a/src/ui/src/features/workflows/detail/components/panel/ui/workflow/workflow-details.tsx b/src/ui/src/features/workflows/detail/components/panel/ui/workflow/workflow-details.tsx index e3c0f1d849..9ffdafa5e0 100644 --- a/src/ui/src/features/workflows/detail/components/panel/ui/workflow/workflow-details.tsx +++ b/src/ui/src/features/workflows/detail/components/panel/ui/workflow/workflow-details.tsx @@ -34,6 +34,7 @@ import { Loader2, RotateCw, FileCode, + TriangleAlert, } from "lucide-react"; import { cn } from "@/lib/utils"; import { Card, CardContent } from "@/components/shadcn/card"; @@ -45,6 +46,7 @@ import { PanelTabs, type PanelTab } from "@/components/panel/panel-tabs"; import { SeparatedParts } from "@/components/panel/separated-parts"; import { TabPanel } from "@/components/panel/tab-panel"; import type { WorkflowQueryResponse } from "@/lib/api/adapter/types"; +import { sortedWorkflowLabelEntries } from "@/lib/workflow-labels"; import type { GroupWithLayout, TaskQueryResponse } from "@/features/workflows/detail/lib/workflow-types"; import { formatDuration } from "@/features/workflows/detail/lib/workflow-types"; import { getStatusIcon } from "@/features/workflows/detail/lib/status"; @@ -186,6 +188,34 @@ const StatusDisplay = memo(function StatusDisplay({ ); }); +/** Current warn-mode violations recomputed by the API from stored labels and active policy. */ +const WorkflowWarnings = memo(function WorkflowWarnings({ warnings }: { warnings: string[] | undefined }) { + if (!warnings || warnings.length === 0) return null; + + return ( +
+

Warnings

+
+
+
+ ); +}); + /** Details section */ const Details = memo(function Details({ workflow }: { workflow: WorkflowQueryResponse }) { return ( @@ -247,6 +277,25 @@ const Details = memo(function Details({ workflow }: { workflow: WorkflowQueryRes
)} + {sortedWorkflowLabelEntries(workflow.labels).length > 0 && ( +
+
+ + Labels +
+
+ {sortedWorkflowLabelEntries(workflow.labels).map(([key, value]) => ( + + {key}={value} + + ))} +
+
+ )} @@ -304,6 +353,8 @@ const OverviewTab = memo(function OverviewTab({ workflow, canCancel, onCancel, o return (
+ + {/* Timeline section */}

Timeline

diff --git a/src/ui/src/features/workflows/detail/components/resubmit/resubmit-panel-content.tsx b/src/ui/src/features/workflows/detail/components/resubmit/resubmit-panel-content.tsx index 5d519e732f..0f61456732 100644 --- a/src/ui/src/features/workflows/detail/components/resubmit/resubmit-panel-content.tsx +++ b/src/ui/src/features/workflows/detail/components/resubmit/resubmit-panel-content.tsx @@ -35,6 +35,7 @@ import { PriorityPicker, PRIORITY_LABELS } from "@/components/workflow/priority- import { useSpecData } from "@/features/workflows/detail/hooks/use-spec-data"; import { SpecSection } from "@/features/workflows/detail/components/resubmit/spec-section"; import { useResubmitForm } from "@/features/workflows/detail/components/resubmit/use-resubmit-form"; +import { WorkflowLabelEditor } from "@/components/workflow/workflow-label-editor"; export interface ResubmitPanelContentProps { workflow: WorkflowQueryResponse; @@ -62,6 +63,7 @@ export const ResubmitPanelContent = memo(function ResubmitPanelContent({ const focusPanel = usePanelFocus(); const [poolOpen, setPoolOpen] = useState(true); const [priorityOpen, setPriorityOpen] = useState(true); + const [labelsOpen, setLabelsOpen] = useState(true); // Return focus to panel after priority selection so ESC works const handlePriorityChange = useCallback( @@ -114,13 +116,31 @@ export const ResubmitPanelContent = memo(function ResubmitPanelContent({ open={priorityOpen} onOpenChange={setPriorityOpen} selectedValue={PRIORITY_LABELS[form.priority]} - isLast > + + 0 ? `${form.labels.length} label${form.labels.length === 1 ? "" : "s"}` : undefined + } + isLast + > + +
{/* Error message */} diff --git a/src/ui/src/features/workflows/detail/components/resubmit/use-resubmit-form.ts b/src/ui/src/features/workflows/detail/components/resubmit/use-resubmit-form.ts index df75b1e938..3f6b1adc97 100644 --- a/src/ui/src/features/workflows/detail/components/resubmit/use-resubmit-form.ts +++ b/src/ui/src/features/workflows/detail/components/resubmit/use-resubmit-form.ts @@ -30,6 +30,12 @@ import type { WorkflowQueryResponse } from "@/lib/api/adapter/types"; import { WorkflowPriority } from "@/lib/api/generated"; import { usePoolSelection } from "@/components/workflow/use-pool-selection"; import { useResubmitMutation } from "@/features/workflows/detail/components/resubmit/use-resubmit-mutation"; +import { + getChangedWorkflowLabelAssignments, + sortedWorkflowLabelEntries, + validateWorkflowLabelDrafts, + type WorkflowLabelDraft, +} from "@/lib/workflow-labels"; export interface UseResubmitFormOptions { workflow: WorkflowQueryResponse; @@ -41,6 +47,11 @@ export interface UseResubmitFormReturn { setPool: (pool: string) => void; priority: WorkflowPriority; setPriority: (priority: WorkflowPriority) => void; + labels: WorkflowLabelDraft[]; + setLabels: (labels: WorkflowLabelDraft[]) => void; + labelError: string | null; + /** Count of leading drafts seeded from the workflow's own labels; their keys are locked. */ + lockedLabelCount: number; /** * Custom spec (if edited AND changed, otherwise undefined = use original via workflow_id). * - undefined: User hasn't edited OR content is identical to original @@ -68,9 +79,18 @@ export function useResubmitForm({ workflow, onSuccess }: UseResubmitFormOptions) const { pool, setPool } = usePoolSelection(workflow.pool ?? ""); const [priority, setPriority] = useState(() => deriveInitialPriority(workflow)); const [spec, setSpec] = useState(undefined); + const [labels, setLabels] = useState(() => + sortedWorkflowLabelEntries(workflow.labels).map(([key, value]) => ({ key, value })), + ); + const lockedLabelCount = Object.keys(workflow.labels ?? {}).length; + const labelError = useMemo(() => validateWorkflowLabelDrafts(labels), [labels]); + const labelAssignments = useMemo( + () => getChangedWorkflowLabelAssignments(labels, workflow.labels ?? {}), + [labels, workflow.labels], + ); const { execute, isPending, error } = useResubmitMutation({ - onSuccess: (newWorkflowName) => { + onSuccess: (newWorkflowName, warnings) => { const message = newWorkflowName ? `Workflow resubmitted as ${newWorkflowName}` : "Workflow resubmitted successfully"; @@ -83,12 +103,15 @@ export function useResubmitForm({ workflow, onSuccess }: UseResubmitFormOptions) } : undefined, }); + for (const warning of warnings) { + toast.warning(warning); + } onSuccess?.(); }, }); - const canSubmit = pool.length > 0 && !isPending; + const canSubmit = pool.length > 0 && !isPending && !labelError; const handleSubmit = useCallback(() => { if (!canSubmit) return; @@ -98,8 +121,9 @@ export function useResubmitForm({ workflow, onSuccess }: UseResubmitFormOptions) poolName: pool, priority, spec, + labels: labelAssignments, }); - }, [canSubmit, execute, workflow.name, pool, priority, spec]); + }, [canSubmit, execute, workflow.name, pool, priority, spec, labelAssignments]); return useMemo( () => ({ @@ -107,6 +131,10 @@ export function useResubmitForm({ workflow, onSuccess }: UseResubmitFormOptions) setPool, priority, setPriority, + labels, + setLabels, + labelError, + lockedLabelCount, spec, setSpec, canSubmit, @@ -114,6 +142,6 @@ export function useResubmitForm({ workflow, onSuccess }: UseResubmitFormOptions) isPending, error, }), - [pool, setPool, priority, spec, canSubmit, handleSubmit, isPending, error], + [pool, setPool, priority, labels, labelError, lockedLabelCount, spec, canSubmit, handleSubmit, isPending, error], ); } diff --git a/src/ui/src/features/workflows/detail/components/resubmit/use-resubmit-mutation.ts b/src/ui/src/features/workflows/detail/components/resubmit/use-resubmit-mutation.ts index ef97bf793d..e9c35e7764 100644 --- a/src/ui/src/features/workflows/detail/components/resubmit/use-resubmit-mutation.ts +++ b/src/ui/src/features/workflows/detail/components/resubmit/use-resubmit-mutation.ts @@ -22,7 +22,7 @@ import { resubmitWorkflow, type ResubmitParams } from "@/features/workflows/list export interface UseResubmitMutationOptions { /** Called on successful resubmission with the new workflow name */ - onSuccess?: (newWorkflowName: string | undefined) => void; + onSuccess?: (newWorkflowName: string | undefined, warnings: string[]) => void; } export interface UseResubmitMutationReturn { @@ -48,7 +48,7 @@ export function useResubmitMutation(options: UseResubmitMutationOptions = {}): U if (actionResult.success) { announcer.announce("Workflow submitted successfully", "polite"); - onSuccess?.(actionResult.newWorkflowName); + onSuccess?.(actionResult.newWorkflowName, actionResult.warnings ?? []); } else { const errorMsg = actionResult.error ?? "Unknown error"; setError(errorMsg); diff --git a/src/ui/src/features/workflows/list/components/table/workflow-column-defs.tsx b/src/ui/src/features/workflows/list/components/table/workflow-column-defs.tsx index 7c0f35f9d6..9cf6c6e17d 100644 --- a/src/ui/src/features/workflows/list/components/table/workflow-column-defs.tsx +++ b/src/ui/src/features/workflows/list/components/table/workflow-column-defs.tsx @@ -32,6 +32,7 @@ import { PRIORITY_DISPLAY } from "@/lib/workflows/priority-display"; import { WORKFLOW_STATUS_ICONS } from "@/lib/workflows/workflow-status-icons"; import { formatDuration } from "@/lib/format-date"; import { WorkflowStatus, WorkflowPriority } from "@/lib/api/generated"; +import { formatWorkflowLabels } from "@/lib/workflow-labels"; export interface WorkflowSelectionOptions { selectedWorkflowNames: ReadonlySet; @@ -243,5 +244,25 @@ export function createWorkflowColumns(selection?: WorkflowSelectionOptions): Col {row.original.app_name || "—"} ), }, + { + id: "labels", + accessorKey: "labels", + header: COLUMN_LABELS.labels, + minSize: getMinSize("labels"), + enableSorting: false, + cell: ({ row }) => { + const labels = row.original.labels; + const hasLabels = Object.keys(labels ?? {}).length > 0; + const formatted = formatWorkflowLabels(labels); + return ( + + {formatted} + + ); + }, + }, ]; } diff --git a/src/ui/src/features/workflows/list/components/workflows-toolbar.tsx b/src/ui/src/features/workflows/list/components/workflows-toolbar.tsx index 803806abc3..f8ed4fed99 100644 --- a/src/ui/src/features/workflows/list/components/workflows-toolbar.tsx +++ b/src/ui/src/features/workflows/list/components/workflows-toolbar.tsx @@ -72,6 +72,8 @@ export const WorkflowsToolbar = memo(function WorkflowsToolbar({ WORKFLOW_FIELD.priority, WORKFLOW_FIELD.app, WORKFLOW_FIELD.tag, + WORKFLOW_FIELD.label, + WORKFLOW_FIELD.no_label, ], [userField, poolField], ); @@ -142,7 +144,7 @@ export const WorkflowsToolbar = memo(function WorkflowsToolbar({ searchChips={searchChips} onSearchChipsChange={onSearchChipsChange} defaultField="name" - placeholder="Search workflows... (try 'name:', 'status:', 'submitted:', 'pool:')" + placeholder="Search workflows... (try 'name:', 'status:', 'label:', 'no-label:')" searchPresets={searchPresets} resultsCount={resultsCount} autoRefreshProps={autoRefreshProps} diff --git a/src/ui/src/features/workflows/list/lib/actions.test.ts b/src/ui/src/features/workflows/list/lib/actions.test.ts new file mode 100644 index 0000000000..b0866ffd07 --- /dev/null +++ b/src/ui/src/features/workflows/list/lib/actions.test.ts @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const { customFetch } = vi.hoisted(() => ({ customFetch: vi.fn() })); + +vi.mock("next/cache", () => ({ + revalidatePath: vi.fn(), + updateTag: vi.fn(), + refresh: vi.fn(), +})); +vi.mock("@/lib/api/fetcher", () => ({ customFetch })); + +import { resubmitWorkflow } from "@/features/workflows/list/lib/actions"; + +const WARN_MISSING_PPP_MESSAGE = + "Workflow is missing label 'PPP'; add it now to avoid rejected submissions once it is required."; + +describe("resubmit workflow labels", () => { + beforeEach(() => { + customFetch.mockReset(); + }); + + it("returns the raw submit name and warnings and sends repeated labels", async () => { + customFetch.mockResolvedValue({ + name: "workflow-copy-2", + warnings: [WARN_MISSING_PPP_MESSAGE], + }); + + const result = await resubmitWorkflow({ + workflowId: "workflow-1", + poolName: "pool-a", + priority: "NORMAL", + labels: ["PPP=robotics", "run=42"], + }); + + const endpoint = new URL(customFetch.mock.calls[0][0], "https://osmo.invalid"); + expect(endpoint.searchParams.getAll("label")).toEqual(["PPP=robotics", "run=42"]); + expect(result).toMatchObject({ + success: true, + newWorkflowName: "workflow-copy-2", + warnings: [WARN_MISSING_PPP_MESSAGE], + }); + }); +}); diff --git a/src/ui/src/features/workflows/list/lib/actions.ts b/src/ui/src/features/workflows/list/lib/actions.ts index 2b3e43cdb4..13ee1257e2 100644 --- a/src/ui/src/features/workflows/list/lib/actions.ts +++ b/src/ui/src/features/workflows/list/lib/actions.ts @@ -41,6 +41,8 @@ import { revalidatePath, updateTag, refresh } from "next/cache"; import { customFetch } from "@/lib/api/fetcher"; +import type { SubmitResponse, SubmitWorkflowApiPoolPoolNameWorkflowPostParams } from "@/lib/api/adapter/types"; +import { getSubmitWorkflowApiPoolPoolNameWorkflowPostUrl } from "@/lib/api/generated"; import type { ActionResult } from "@/lib/server-actions"; // ============================================================================= @@ -263,6 +265,8 @@ export async function cancelTaskGroup(workflowName: string, groupName: string): export interface ResubmitResult extends ActionResult { /** New workflow name returned by the backend on success */ newWorkflowName?: string; + /** Admission warnings returned for accepted submissions */ + warnings?: string[]; } export interface ResubmitParams { @@ -280,6 +284,8 @@ export interface ResubmitParams { * Backend constraint: EITHER template_spec OR workflow_id, never both. */ spec?: string; + /** Per-run key=value label overrides */ + labels?: string[]; } // ============================================================================= @@ -300,18 +306,18 @@ export interface ResubmitParams { * @returns Result with the new workflow name on success, or error message */ export async function resubmitWorkflow(params: ResubmitParams): Promise { - const { workflowId, poolName, priority, spec } = params; - - const queryParams = new URLSearchParams(); - queryParams.set("priority", priority); - - // Backend constraint: EITHER template_spec OR workflow_id, never both - if (!spec) { - // No custom spec: send workflow_id to reuse original spec - queryParams.set("workflow_id", workflowId); - } + const { workflowId, poolName, priority, spec, labels = [] } = params; + + // Backend constraint: EITHER template_spec OR workflow_id, never both. + // The generated URL builder explodes repeated `label` params and skips + // undefined values. + const queryParams: SubmitWorkflowApiPoolPoolNameWorkflowPostParams = { + priority: priority as SubmitWorkflowApiPoolPoolNameWorkflowPostParams["priority"], + label: labels.length > 0 ? labels : undefined, + workflow_id: spec ? undefined : workflowId, + }; - const endpoint = `/api/pool/${encodeURIComponent(poolName)}/workflow?${queryParams.toString()}`; + const endpoint = getSubmitWorkflowApiPoolPoolNameWorkflowPostUrl(encodeURIComponent(poolName), queryParams); try { const init: RequestInit = { method: "POST" }; @@ -321,16 +327,16 @@ export async function resubmitWorkflow(params: ResubmitParams): Promise(endpoint, init); + const response = await customFetch(endpoint, init); - const newName = response?.data?.name; + const newName = response.name; // No cache revalidation needed - creating a new workflow doesn't affect: // - Current workflow page (unchanged) // - New workflow page (will fetch fresh when user navigates to it) // - Workflows list (will fetch fresh when user navigates to it) - return { success: true, newWorkflowName: newName }; + return { success: true, newWorkflowName: newName, warnings: response.warnings ?? [] }; } catch (error) { return { success: false, diff --git a/src/ui/src/features/workflows/list/lib/workflow-columns.test.ts b/src/ui/src/features/workflows/list/lib/workflow-columns.test.ts new file mode 100644 index 0000000000..2112b0d0c0 --- /dev/null +++ b/src/ui/src/features/workflows/list/lib/workflow-columns.test.ts @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it } from "vitest"; +import { + COLUMN_LABELS, + DEFAULT_COLUMN_ORDER, + DEFAULT_VISIBLE_COLUMNS, + isWorkflowColumnId, +} from "@/features/workflows/list/lib/workflow-columns"; + +describe("workflow labels column", () => { + it("is visible by default and has a stable column identity", () => { + expect(isWorkflowColumnId("labels")).toBe(true); + expect(COLUMN_LABELS.labels).toBe("Labels"); + expect(DEFAULT_COLUMN_ORDER).toContain("labels"); + expect(DEFAULT_VISIBLE_COLUMNS).toContain("labels"); + }); +}); diff --git a/src/ui/src/features/workflows/list/lib/workflow-columns.ts b/src/ui/src/features/workflows/list/lib/workflow-columns.ts index 88fc8a6502..85d21205f1 100644 --- a/src/ui/src/features/workflows/list/lib/workflow-columns.ts +++ b/src/ui/src/features/workflows/list/lib/workflow-columns.ts @@ -33,7 +33,8 @@ export type WorkflowColumnId = | "queued_time" | "pool" | "priority" - | "app_name"; + | "app_name" + | "labels"; // ============================================================================= // Column Configuration (via factory) @@ -53,6 +54,7 @@ const workflowColumnConfig = createColumnConfig({ "pool", "priority", "app_name", + "labels", ] as const, labels: { _select: "", @@ -67,9 +69,10 @@ const workflowColumnConfig = createColumnConfig({ pool: "Pool", priority: "Priority", app_name: "App", + labels: "Labels", }, mandatory: ["_select", "name"], - defaultVisible: ["_select", "name", "status", "user", "submit_time", "duration", "pool", "priority"], + defaultVisible: ["_select", "name", "status", "user", "submit_time", "duration", "pool", "priority", "labels"], defaultOrder: [ "_select", "name", @@ -83,6 +86,7 @@ const workflowColumnConfig = createColumnConfig({ "pool", "priority", "app_name", + "labels", ], sizeConfig: [ { @@ -145,6 +149,11 @@ const workflowColumnConfig = createColumnConfig({ minWidthRem: COLUMN_MIN_WIDTHS_REM.TEXT_SHORT, preferredWidthRem: COLUMN_PREFERRED_WIDTHS_REM.TEXT_SHORT, }, + { + id: "labels", + minWidthRem: COLUMN_MIN_WIDTHS_REM.TEXT_TRUNCATE, + preferredWidthRem: COLUMN_PREFERRED_WIDTHS_REM.TEXT_TRUNCATE * 1.5, + }, ], defaultSort: { column: "submit_time", direction: "desc" }, }); diff --git a/src/ui/src/features/workflows/list/lib/workflow-search-fields.test.ts b/src/ui/src/features/workflows/list/lib/workflow-search-fields.test.ts index 99fad43140..e5c4c7eba3 100644 --- a/src/ui/src/features/workflows/list/lib/workflow-search-fields.test.ts +++ b/src/ui/src/features/workflows/list/lib/workflow-search-fields.test.ts @@ -47,6 +47,8 @@ describe("WORKFLOW_STATIC_FIELDS structure", () => { expect(fieldIds).toContain("priority"); expect(fieldIds).toContain("app"); expect(fieldIds).toContain("tag"); + expect(fieldIds).toContain("label"); + expect(fieldIds).toContain("no_label"); }); it("all fields have required properties", () => { @@ -74,6 +76,8 @@ describe("WORKFLOW_STATIC_FIELDS structure", () => { expect(getField("priority").prefix).toBe("priority:"); expect(getField("app").prefix).toBe("app:"); expect(getField("tag").prefix).toBe("tag:"); + expect(getField("label").prefix).toBe("label:"); + expect(getField("no_label").prefix).toBe("no-label:"); }); }); @@ -177,6 +181,27 @@ describe("tag field", () => { }); }); +describe("workflow label fields", () => { + it("accepts exact, glob, and alternative key=value filters as free-form input", () => { + const labelField = getField("label"); + + expect(getFieldValues(labelField, [])).toEqual([]); + expect(labelField.freeFormHint).toContain("key=value"); + expect(labelField.freeFormHint).toContain("key=(team_*|osmo_*)"); + expect(labelField.freeFormHint).toContain("key=team_(a|b)"); + expect(labelField.prefix).toBe("label:"); + expect(labelField.singular).toBeUndefined(); + }); + + it("accepts missing-key filters as free-form input", () => { + const noLabelField = getField("no_label"); + + expect(getFieldValues(noLabelField, [])).toEqual([]); + expect(noLabelField.freeFormHint).toContain("key"); + expect(noLabelField.singular).toBeUndefined(); + }); +}); + describe("STATUS_PRESETS", () => { it("contains expected preset categories", () => { expect(STATUS_PRESETS).toHaveProperty("running"); diff --git a/src/ui/src/features/workflows/list/lib/workflow-search-fields.ts b/src/ui/src/features/workflows/list/lib/workflow-search-fields.ts index e097872b57..ea97f453b3 100644 --- a/src/ui/src/features/workflows/list/lib/workflow-search-fields.ts +++ b/src/ui/src/features/workflows/list/lib/workflow-search-fields.ts @@ -75,6 +75,22 @@ export const WORKFLOW_FIELD: Readonly [], }, + label: { + id: "label", + label: "Label", + hint: "workflow label selector (exact, glob, or alternatives)", + prefix: "label:", + freeFormHint: "Type key=value, key=(team_*|osmo_*), or key=team_(a|b), press Enter", + getValues: () => [], + }, + no_label: { + id: "no_label", + label: "Missing Label", + hint: "workflow missing a label key", + prefix: "no-label:", + freeFormHint: "Type a label key, press Enter", + getValues: () => [], + }, }); export const WORKFLOW_STATIC_FIELDS: readonly SearchField[] = Object.freeze( diff --git a/src/ui/src/lib/api/adapter/types.ts b/src/ui/src/lib/api/adapter/types.ts index 15c7769cf1..b8940c5ec5 100644 --- a/src/ui/src/lib/api/adapter/types.ts +++ b/src/ui/src/lib/api/adapter/types.ts @@ -36,6 +36,8 @@ export type { WorkflowQueryResponse, GroupQueryResponse, TaskQueryResponse, + SubmitResponse, + SubmitWorkflowApiPoolPoolNameWorkflowPostParams, SrcServiceCoreWorkflowObjectsListEntry as WorkflowListEntry, } from "@/lib/api/generated"; diff --git a/src/ui/src/lib/api/adapter/workflows-shim.test.ts b/src/ui/src/lib/api/adapter/workflows-shim.test.ts new file mode 100644 index 0000000000..9e8d9d71e4 --- /dev/null +++ b/src/ui/src/lib/api/adapter/workflows-shim.test.ts @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it } from "vitest"; +import { buildWorkflowApiParams, buildWorkflowsQueryKey } from "@/lib/api/adapter/workflows-shim"; +import { getListWorkflowApiWorkflowGetUrl, getSubmitWorkflowApiPoolPoolNameWorkflowPostUrl } from "@/lib/api/generated"; + +const chips = [ + { field: "label", value: "team=robotics", label: "label: team=robotics" }, + { field: "label", value: "run=42", label: "label: run=42" }, + { field: "no_label", value: "deprecated", label: "no-label: deprecated" }, +]; + +describe("workflow label filters", () => { + it("maps chips to repeatable backend query parameters", () => { + expect(buildWorkflowApiParams(chips, false, 0, 50, "DESC")).toMatchObject({ + label: ["team=robotics", "run=42"], + no_label: ["deprecated"], + }); + }); + + it("keeps label selectors in the stable query key", () => { + expect(buildWorkflowsQueryKey(chips, false, "DESC")).toEqual([ + "workflows", + "paginated", + { + labels: ["run=42", "team=robotics"], + missingLabels: ["deprecated"], + showAllUsers: false, + sortDirection: "DESC", + }, + ]); + }); + + it("serializes repeated list and submit labels as separate query values", () => { + const listUrl = new URL( + getListWorkflowApiWorkflowGetUrl({ label: ["team=robotics", "run=42"] }), + "https://osmo.invalid", + ); + const submitUrl = new URL( + getSubmitWorkflowApiPoolPoolNameWorkflowPostUrl("pool-a", { + label: ["team=robotics", "run=42"], + }), + "https://osmo.invalid", + ); + + expect(listUrl.searchParams.getAll("label")).toEqual(["team=robotics", "run=42"]); + expect(submitUrl.searchParams.getAll("label")).toEqual(["team=robotics", "run=42"]); + }); + + it("forwards wildcard alternatives and inline alternatives unchanged", () => { + const selectors = ["PPP=(team_*|osmo_*)", "PPP=team_(a|b)"]; + const selectorChips = selectors.map((selector) => ({ + field: "label", + value: selector, + label: `label: ${selector}`, + })); + + expect(buildWorkflowApiParams(selectorChips, true, 0, 50, "DESC").label).toEqual(selectors); + + const listUrl = new URL(getListWorkflowApiWorkflowGetUrl({ label: selectors }), "https://osmo.invalid"); + expect(listUrl.searchParams.getAll("label")).toEqual(selectors); + expect(buildWorkflowsQueryKey(selectorChips, true, "DESC")).toEqual([ + "workflows", + "paginated", + { + labels: ["PPP=(team_*|osmo_*)", "PPP=team_(a|b)"], + showAllUsers: true, + sortDirection: "DESC", + }, + ]); + }); +}); diff --git a/src/ui/src/lib/api/adapter/workflows-shim.ts b/src/ui/src/lib/api/adapter/workflows-shim.ts index 11ec34c446..1cdbc62d16 100644 --- a/src/ui/src/lib/api/adapter/workflows-shim.ts +++ b/src/ui/src/lib/api/adapter/workflows-shim.ts @@ -77,7 +77,7 @@ function getFirstChipValue(chips: SearchChip[], field: string): string | undefin /** * Build API parameters from search chips and options. */ -function buildApiParams( +export function buildWorkflowApiParams( chips: SearchChip[], showAllUsers: boolean, offset: number, @@ -90,6 +90,8 @@ function buildApiParams( const userChips = getChipValues(chips, "user"); const priorityChips = getChipValues(chips, "priority"); const tagChips = getChipValues(chips, "tag"); + const labelChips = getChipValues(chips, "label"); + const missingLabelChips = getChipValues(chips, "no_label"); // Resolve submitted date range: chip takes precedence over prop let resolvedAfter = submittedAfter; @@ -114,6 +116,8 @@ function buildApiParams( app: getFirstChipValue(chips, "app"), priority: priorityChips.length > 0 ? (priorityChips as WorkflowPriority[]) : undefined, tags: tagChips.length > 0 ? tagChips : undefined, + label: labelChips.length > 0 ? labelChips : undefined, + no_label: missingLabelChips.length > 0 ? missingLabelChips : undefined, all_users: userChips.length === 0 && showAllUsers ? true : undefined, all_pools: poolChips.length === 0, submitted_after: resolvedAfter, @@ -138,7 +142,7 @@ export async function fetchPaginatedWorkflows( const { offset = 0, limit, searchChips, showAllUsers = false, sortDirection = "DESC", submittedAfter } = params; // Build API params from chips - const apiParams = buildApiParams( + const apiParams = buildWorkflowApiParams( searchChips, showAllUsers, offset, @@ -191,6 +195,8 @@ export function buildWorkflowsQueryKey( const pools = getChipValues(searchChips, "pool").sort(); const priority = getChipValues(searchChips, "priority").sort(); const tags = getChipValues(searchChips, "tag").sort(); + const labels = getChipValues(searchChips, "label").sort(); + const missingLabels = getChipValues(searchChips, "no_label").sort(); const submitted = getFirstChipValue(searchChips, "submitted"); // Build query key - only include filters that have values @@ -202,6 +208,8 @@ export function buildWorkflowsQueryKey( if (pools.length > 0) filters.pools = pools; if (priority.length > 0) filters.priority = priority; if (tags.length > 0) filters.tags = tags; + if (labels.length > 0) filters.labels = labels; + if (missingLabels.length > 0) filters.missingLabels = missingLabels; if (submitted) filters.submitted = submitted; return [ diff --git a/src/ui/src/lib/api/server/workflows.ts b/src/ui/src/lib/api/server/workflows.ts index db209cfa6e..b819114de1 100644 --- a/src/ui/src/lib/api/server/workflows.ts +++ b/src/ui/src/lib/api/server/workflows.ts @@ -66,6 +66,10 @@ export interface WorkflowsQueryParams { all_pools?: boolean; /** ISO date string — only return workflows submitted after this time */ submitted_after?: string; + /** Exact or pattern key=value workflow label filters */ + label?: string[]; + /** Workflow label keys that must be absent */ + no_label?: string[]; } // ============================================================================= @@ -100,6 +104,8 @@ export const fetchWorkflows = cache(async (params: WorkflowsQueryParams = {}): P all_users: params.all_users, all_pools: params.all_pools, submitted_after: params.submitted_after, + label: params.label, + no_label: params.no_label, }; return listWorkflowApiWorkflowGet(apiParams); @@ -217,6 +223,8 @@ export async function prefetchWorkflowsList( const statusFilters = filterChips.filter((c) => c.field === "status").map((c) => c.value as WorkflowStatus); const poolFilters = filterChips.filter((c) => c.field === "pool").map((c) => c.value); const userFilters = filterChips.filter((c) => c.field === "user").map((c) => c.value); + const labelFilters = filterChips.filter((c) => c.field === "label").map((c) => c.value); + const missingLabelFilters = filterChips.filter((c) => c.field === "no_label").map((c) => c.value); const hasUserChips = userFilters.length > 0; const effectiveShowAllUsers = hasUserChips ? false : showAllUsers; @@ -236,6 +244,8 @@ export async function prefetchWorkflowsList( pools: poolFilters.length > 0 ? poolFilters : undefined, users: userFilters.length > 0 ? userFilters : undefined, submitted_after: submittedAfter, + label: labelFilters.length > 0 ? labelFilters : undefined, + no_label: missingLabelFilters.length > 0 ? missingLabelFilters : undefined, }); return { diff --git a/src/ui/src/lib/workflow-labels.test.ts b/src/ui/src/lib/workflow-labels.test.ts new file mode 100644 index 0000000000..6af99b56b2 --- /dev/null +++ b/src/ui/src/lib/workflow-labels.test.ts @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it } from "vitest"; +import { + MAX_WORKFLOW_LABELS, + formatWorkflowLabels, + getChangedWorkflowLabelAssignments, + sortedWorkflowLabelEntries, + validateWorkflowLabelDrafts, + type WorkflowLabelDraft, +} from "@/lib/workflow-labels"; + +const draft = (key: string, value: string): WorkflowLabelDraft => ({ key, value }); + +describe("workflow label drafts", () => { + it("sends only labels changed from a resubmitted workflow", () => { + expect( + getChangedWorkflowLabelAssignments([draft("PPP", "robotics"), draft("team", "simulation"), draft("run", "42")], { + PPP: "robotics", + team: "robotics", + }), + ).toEqual(["team=simulation", "run=42"]); + }); + + it("rejects incomplete and duplicate overrides before submission", () => { + expect(validateWorkflowLabelDrafts([draft("team", "")])).toMatch(/key and value/i); + expect(validateWorkflowLabelDrafts([draft("team", "one"), draft("team", "two")])).toMatch(/duplicate/i); + }); + + it("enforces the shared 16-label UI cap", () => { + const labels = Array.from({ length: MAX_WORKFLOW_LABELS + 1 }, (_, index) => draft(`key${index}`, "value")); + + expect(validateWorkflowLabelDrafts(labels)).toContain(String(MAX_WORKFLOW_LABELS)); + }); + + it("formats canonical labels deterministically", () => { + expect(formatWorkflowLabels({ zeta: "last", alpha: "first" })).toBe("alpha=first, zeta=last"); + expect(formatWorkflowLabels({})).toBe("—"); + }); + + it("sorts label entries deterministically and tolerates missing maps", () => { + expect(sortedWorkflowLabelEntries({ zeta: "last", alpha: "first" })).toEqual([ + ["alpha", "first"], + ["zeta", "last"], + ]); + expect(sortedWorkflowLabelEntries(undefined)).toEqual([]); + }); +}); diff --git a/src/ui/src/lib/workflow-labels.ts b/src/ui/src/lib/workflow-labels.ts new file mode 100644 index 0000000000..790853b959 --- /dev/null +++ b/src/ui/src/lib/workflow-labels.ts @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +export const MAX_WORKFLOW_LABELS = 16; + +export interface WorkflowLabelDraft { + key: string; + value: string; +} + +export function validateWorkflowLabelDrafts(labels: WorkflowLabelDraft[]): string | null { + if (labels.length > MAX_WORKFLOW_LABELS) { + return `A workflow can have at most ${MAX_WORKFLOW_LABELS} label overrides.`; + } + + const keys = new Set(); + for (const label of labels) { + const key = label.key.trim(); + const value = label.value.trim(); + if (!key || !value) { + return "Every workflow label needs both a key and value."; + } + if (keys.has(key)) { + return `Duplicate workflow label key: ${key}`; + } + keys.add(key); + } + return null; +} + +export function getChangedWorkflowLabelAssignments( + labels: WorkflowLabelDraft[], + originalLabels: Record, +): string[] { + return labels + .map(({ key, value }) => ({ key: key.trim(), value: value.trim() })) + .filter(({ key, value }) => originalLabels[key] !== value) + .map(({ key, value }) => `${key}=${value}`); +} + +export function sortedWorkflowLabelEntries(labels: Record | null | undefined): [string, string][] { + return Object.entries(labels ?? {}).sort(([left], [right]) => left.localeCompare(right)); +} + +export function formatWorkflowLabels(labels: Record | null | undefined): string { + const entries = sortedWorkflowLabelEntries(labels); + if (entries.length === 0) return "—"; + return entries.map(([key, value]) => `${key}=${value}`).join(", "); +} From c3d1957bed37c83122226743d16ba7d94df1b2ed Mon Sep 17 00:00:00 2001 From: Jiaen Ren Date: Tue, 21 Jul 2026 22:34:09 -0700 Subject: [PATCH 06/12] Apply UI review simplifications Compute the detail-page label entries once per render, replace a warnings useMemo with a stable module constant, type ResubmitParams priority to drop a cast, and remove two shim tests that exercised generated URL builders already covered end to end. Co-Authored-By: Claude Fable 5 --- .../use-submit-workflow-form.ts | 7 ++-- .../panel/ui/workflow/workflow-details.tsx | 5 ++- .../features/workflows/list/lib/actions.ts | 4 +- .../lib/api/adapter/workflows-shim.test.ts | 40 ------------------- 4 files changed, 8 insertions(+), 48 deletions(-) diff --git a/src/ui/src/components/submit-workflow/use-submit-workflow-form.ts b/src/ui/src/components/submit-workflow/use-submit-workflow-form.ts index f61854afb3..7f6fd8b9dd 100644 --- a/src/ui/src/components/submit-workflow/use-submit-workflow-form.ts +++ b/src/ui/src/components/submit-workflow/use-submit-workflow-form.ts @@ -24,6 +24,8 @@ "use client"; import { useState, useCallback, useMemo } from "react"; + +const NO_WARNINGS: string[] = []; import { toast } from "sonner"; import { useNavigationRouter } from "@/hooks/use-navigation-router"; import { useServices } from "@/contexts/service-context"; @@ -117,10 +119,7 @@ export function useSubmitWorkflowForm(initialSpec = ""): UseSubmitWorkflowFormRe const isValidationFresh = validationState !== null && validationState.spec === spec; const validationOk = isValidationFresh ? (validationState.ok ? true : null) : null; const validationError = isValidationFresh ? validationState.error : null; - const validationWarnings = useMemo( - () => (isValidationFresh ? validationState.warnings : []), - [isValidationFresh, validationState], - ); + const validationWarnings = isValidationFresh ? validationState.warnings : NO_WARNINGS; // ── Mutation hooks ──────────────────────────────────────────────────────── diff --git a/src/ui/src/features/workflows/detail/components/panel/ui/workflow/workflow-details.tsx b/src/ui/src/features/workflows/detail/components/panel/ui/workflow/workflow-details.tsx index 9ffdafa5e0..62e3a003dd 100644 --- a/src/ui/src/features/workflows/detail/components/panel/ui/workflow/workflow-details.tsx +++ b/src/ui/src/features/workflows/detail/components/panel/ui/workflow/workflow-details.tsx @@ -218,6 +218,7 @@ const WorkflowWarnings = memo(function WorkflowWarnings({ warnings }: { warnings /** Details section */ const Details = memo(function Details({ workflow }: { workflow: WorkflowQueryResponse }) { + const labelEntries = sortedWorkflowLabelEntries(workflow.labels); return (

Details

@@ -277,14 +278,14 @@ const Details = memo(function Details({ workflow }: { workflow: WorkflowQueryRes )} - {sortedWorkflowLabelEntries(workflow.labels).length > 0 && ( + {labelEntries.length > 0 && (
Labels
- {sortedWorkflowLabelEntries(workflow.labels).map(([key, value]) => ( + {labelEntries.map(([key, value]) => ( 0 ? labels : undefined, workflow_id: spec ? undefined : workflowId, }; diff --git a/src/ui/src/lib/api/adapter/workflows-shim.test.ts b/src/ui/src/lib/api/adapter/workflows-shim.test.ts index 9e8d9d71e4..3c4a511e9b 100644 --- a/src/ui/src/lib/api/adapter/workflows-shim.test.ts +++ b/src/ui/src/lib/api/adapter/workflows-shim.test.ts @@ -16,7 +16,6 @@ import { describe, expect, it } from "vitest"; import { buildWorkflowApiParams, buildWorkflowsQueryKey } from "@/lib/api/adapter/workflows-shim"; -import { getListWorkflowApiWorkflowGetUrl, getSubmitWorkflowApiPoolPoolNameWorkflowPostUrl } from "@/lib/api/generated"; const chips = [ { field: "label", value: "team=robotics", label: "label: team=robotics" }, @@ -44,43 +43,4 @@ describe("workflow label filters", () => { }, ]); }); - - it("serializes repeated list and submit labels as separate query values", () => { - const listUrl = new URL( - getListWorkflowApiWorkflowGetUrl({ label: ["team=robotics", "run=42"] }), - "https://osmo.invalid", - ); - const submitUrl = new URL( - getSubmitWorkflowApiPoolPoolNameWorkflowPostUrl("pool-a", { - label: ["team=robotics", "run=42"], - }), - "https://osmo.invalid", - ); - - expect(listUrl.searchParams.getAll("label")).toEqual(["team=robotics", "run=42"]); - expect(submitUrl.searchParams.getAll("label")).toEqual(["team=robotics", "run=42"]); - }); - - it("forwards wildcard alternatives and inline alternatives unchanged", () => { - const selectors = ["PPP=(team_*|osmo_*)", "PPP=team_(a|b)"]; - const selectorChips = selectors.map((selector) => ({ - field: "label", - value: selector, - label: `label: ${selector}`, - })); - - expect(buildWorkflowApiParams(selectorChips, true, 0, 50, "DESC").label).toEqual(selectors); - - const listUrl = new URL(getListWorkflowApiWorkflowGetUrl({ label: selectors }), "https://osmo.invalid"); - expect(listUrl.searchParams.getAll("label")).toEqual(selectors); - expect(buildWorkflowsQueryKey(selectorChips, true, "DESC")).toEqual([ - "workflows", - "paginated", - { - labels: ["PPP=(team_*|osmo_*)", "PPP=team_(a|b)"], - showAllUsers: true, - sortDirection: "DESC", - }, - ]); - }); }); From 4d6bc5ea3ab0de8ea6665491cc5dbe8c9a4840ad Mon Sep 17 00:00:00 2001 From: Jiaen Ren Date: Wed, 22 Jul 2026 15:00:08 -0700 Subject: [PATCH 07/12] Polish UI label component docs and copy Move the module constant below imports, refresh stale JSDoc, document the shared label helpers and their Python cap mirror, unify warning toast order and list keys across the two submit surfaces, align copy with the app's terminology, and trim a duplicated e2e assertion. Co-Authored-By: Claude Fable 5 --- .../e2e/journeys/compact-mode-toggle.spec.ts | 27 +- .../journeys/cross-page-navigation.spec.ts | 27 +- src/ui/e2e/journeys/dashboard-errors.spec.ts | 14 +- src/ui/e2e/journeys/dashboard.spec.ts | 39 +-- .../e2e/journeys/display-mode-toggle.spec.ts | 5 +- src/ui/e2e/journeys/empty-states.spec.ts | 8 +- src/ui/e2e/journeys/log-viewer-recent.spec.ts | 24 +- src/ui/e2e/journeys/log-viewer.spec.ts | 5 +- src/ui/e2e/journeys/not-found-page.spec.ts | 5 +- src/ui/e2e/journeys/occupancy-toolbar.spec.ts | 14 +- .../e2e/journeys/occupancy-truncation.spec.ts | 38 ++- src/ui/e2e/journeys/occupancy.spec.ts | 6 +- .../panel-keyboard-interactions.spec.ts | 10 +- src/ui/e2e/journeys/pools.spec.ts | 109 ++++--- .../journeys/resource-panel-content.spec.ts | 23 +- src/ui/e2e/journeys/resources.spec.ts | 288 ++++++++++-------- src/ui/e2e/journeys/status-display.spec.ts | 34 +-- .../e2e/journeys/submit-workflow-form.spec.ts | 2 + .../e2e/journeys/table-column-toggle.spec.ts | 51 +--- src/ui/e2e/journeys/toolbar-refresh.spec.ts | 15 +- .../journeys/workflow-cancel-mutation.spec.ts | 20 +- .../journeys/workflow-detail-actions.spec.ts | 87 ++---- .../journeys/workflow-detail-overview.spec.ts | 9 +- .../e2e/journeys/workflow-detail-tabs.spec.ts | 21 +- src/ui/e2e/journeys/workflow-detail.spec.ts | 33 +- .../e2e/journeys/workflow-pagination.spec.ts | 15 +- .../journeys/workflow-resubmit-panel.spec.ts | 2 + .../e2e/journeys/workflow-spec-viewer.spec.ts | 5 +- src/ui/e2e/utils/mock-setup.ts | 13 +- .../submit-workflow-config-panel.tsx | 4 +- .../use-submit-workflow-form.ts | 4 +- .../workflow/workflow-label-editor.tsx | 5 +- .../components/resubmit/use-resubmit-form.ts | 6 +- .../resubmit/use-resubmit-mutation.ts | 2 +- .../features/workflows/list/lib/actions.ts | 2 +- .../list/lib/workflow-search-fields.ts | 2 + src/ui/src/lib/workflow-labels.ts | 5 + 37 files changed, 396 insertions(+), 583 deletions(-) diff --git a/src/ui/e2e/journeys/compact-mode-toggle.spec.ts b/src/ui/e2e/journeys/compact-mode-toggle.spec.ts index 4908ef6b49..d33d326f6c 100644 --- a/src/ui/e2e/journeys/compact-mode-toggle.spec.ts +++ b/src/ui/e2e/journeys/compact-mode-toggle.spec.ts @@ -15,15 +15,8 @@ // SPDX-License-Identifier: Apache-2.0 import { test, expect } from "@playwright/test"; -import { - createPoolResponse, - PoolStatus, -} from "@/mocks/factories"; -import { - setupDefaultMocks, - setupPools, - setupProfile, -} from "@/e2e/utils/mock-setup"; +import { createPoolResponse, PoolStatus } from "@/mocks/factories"; +import { setupDefaultMocks, setupPools, setupProfile } from "@/e2e/utils/mock-setup"; /** * Compact Mode Toggle Tests @@ -68,9 +61,7 @@ test.describe("Compact Mode Toggle — Pools Page", () => { await page.waitForLoadState("networkidle"); // ASSERT — default is comfortable (not compact) - await expect( - page.getByRole("button", { name: /currently in comfortable view/i }), - ).toBeVisible(); + await expect(page.getByRole("button", { name: /currently in comfortable view/i })).toBeVisible(); }); test("clicking toggle switches to compact view", async ({ page }) => { @@ -83,9 +74,7 @@ test.describe("Compact Mode Toggle — Pools Page", () => { await toggleButton.click(); // ASSERT — now in compact mode - await expect( - page.getByRole("button", { name: /currently in compact view/i }), - ).toBeVisible(); + await expect(page.getByRole("button", { name: /currently in compact view/i })).toBeVisible(); }); test("clicking toggle twice returns to comfortable view", async ({ page }) => { @@ -96,16 +85,12 @@ test.describe("Compact Mode Toggle — Pools Page", () => { // Toggle to compact const toggleButton = page.getByRole("button", { name: /currently in comfortable view/i }); await toggleButton.click(); - await expect( - page.getByRole("button", { name: /currently in compact view/i }), - ).toBeVisible(); + await expect(page.getByRole("button", { name: /currently in compact view/i })).toBeVisible(); // Toggle back to comfortable await page.getByRole("button", { name: /currently in compact view/i }).click(); // ASSERT — back to comfortable - await expect( - page.getByRole("button", { name: /currently in comfortable view/i }), - ).toBeVisible(); + await expect(page.getByRole("button", { name: /currently in comfortable view/i })).toBeVisible(); }); }); diff --git a/src/ui/e2e/journeys/cross-page-navigation.spec.ts b/src/ui/e2e/journeys/cross-page-navigation.spec.ts index 543d7ce9ff..ab71694a50 100644 --- a/src/ui/e2e/journeys/cross-page-navigation.spec.ts +++ b/src/ui/e2e/journeys/cross-page-navigation.spec.ts @@ -15,15 +15,8 @@ // SPDX-License-Identifier: Apache-2.0 import { test, expect } from "@playwright/test"; -import { - createPoolResponse, - PoolStatus, -} from "@/mocks/factories"; -import { - setupDefaultMocks, - setupProfile, - setupPools, -} from "@/e2e/utils/mock-setup"; +import { createPoolResponse, PoolStatus } from "@/mocks/factories"; +import { setupDefaultMocks, setupProfile, setupPools } from "@/e2e/utils/mock-setup"; /** * Pool Quick Links Navigation Tests @@ -51,9 +44,7 @@ test.describe("Pool Quick Links Navigation", () => { ); }); - test("resources quick link navigates to resources filtered by pool", async ({ - page, - }) => { + test("resources quick link navigates to resources filtered by pool", async ({ page }) => { // ACT await page.goto("/pools?all=true&view=prod-gpu"); await page.waitForLoadState("networkidle"); @@ -73,9 +64,7 @@ test.describe("Pool Quick Links Navigation", () => { await expect(page).toHaveURL(/prod-gpu/); }); - test("workflows quick link navigates to workflows filtered by pool", async ({ - page, - }) => { + test("workflows quick link navigates to workflows filtered by pool", async ({ page }) => { // ACT await page.goto("/pools?all=true&view=prod-gpu"); await page.waitForLoadState("networkidle"); @@ -92,9 +81,7 @@ test.describe("Pool Quick Links Navigation", () => { await expect(page).toHaveURL(/prod-gpu/); }); - test("occupancy quick link navigates to occupancy filtered by pool", async ({ - page, - }) => { + test("occupancy quick link navigates to occupancy filtered by pool", async ({ page }) => { // ACT await page.goto("/pools?all=true&view=prod-gpu"); await page.waitForLoadState("networkidle"); @@ -111,9 +98,7 @@ test.describe("Pool Quick Links Navigation", () => { await expect(page).toHaveURL(/prod-gpu/); }); - test("quick links show correct href attributes before clicking", async ({ - page, - }) => { + test("quick links show correct href attributes before clicking", async ({ page }) => { // ACT await page.goto("/pools?all=true&view=prod-gpu"); await page.waitForLoadState("networkidle"); diff --git a/src/ui/e2e/journeys/dashboard-errors.spec.ts b/src/ui/e2e/journeys/dashboard-errors.spec.ts index 7bc1421cc7..a860e41e44 100644 --- a/src/ui/e2e/journeys/dashboard-errors.spec.ts +++ b/src/ui/e2e/journeys/dashboard-errors.spec.ts @@ -15,18 +15,8 @@ // SPDX-License-Identifier: Apache-2.0 import { test, expect } from "@playwright/test"; -import { - createPoolResponse, - createWorkflowsResponse, - PoolStatus, - WorkflowStatus, -} from "@/mocks/factories"; -import { - setupDefaultMocks, - setupProfile, - setupPools, - setupWorkflows, -} from "@/e2e/utils/mock-setup"; +import { createPoolResponse, createWorkflowsResponse, PoolStatus, WorkflowStatus } from "@/mocks/factories"; +import { setupDefaultMocks, setupProfile, setupPools, setupWorkflows } from "@/e2e/utils/mock-setup"; /** * Dashboard Error & Edge Case Tests diff --git a/src/ui/e2e/journeys/dashboard.spec.ts b/src/ui/e2e/journeys/dashboard.spec.ts index 71cd6a702d..cde14f3115 100644 --- a/src/ui/e2e/journeys/dashboard.spec.ts +++ b/src/ui/e2e/journeys/dashboard.spec.ts @@ -15,18 +15,8 @@ // SPDX-License-Identifier: Apache-2.0 import { test, expect } from "@playwright/test"; -import { - createPoolResponse, - createWorkflowsResponse, - PoolStatus, - WorkflowStatus, -} from "@/mocks/factories"; -import { - setupDefaultMocks, - setupPools, - setupProfile, - setupWorkflows, -} from "@/e2e/utils/mock-setup"; +import { createPoolResponse, createWorkflowsResponse, PoolStatus, WorkflowStatus } from "@/mocks/factories"; +import { setupDefaultMocks, setupPools, setupProfile, setupWorkflows } from "@/e2e/utils/mock-setup"; /** * Dashboard Journey Tests @@ -109,9 +99,7 @@ test.describe("Dashboard Recent Workflows", () => { await setupPools(page, createPoolResponse([{ name: "prod", status: PoolStatus.ONLINE }])); await setupWorkflows( page, - createWorkflowsResponse([ - { name: "recent-e2e-workflow", status: WorkflowStatus.COMPLETED, user: "test-user" }, - ]), + createWorkflowsResponse([{ name: "recent-e2e-workflow", status: WorkflowStatus.COMPLETED, user: "test-user" }]), ); // ACT @@ -170,12 +158,7 @@ test.describe("Dashboard Stat Card Links", () => { test("Active Workflows stat card links to workflows filtered by RUNNING status", async ({ page }) => { // ARRANGE await setupPools(page, createPoolResponse([{ name: "prod", status: PoolStatus.ONLINE }])); - await setupWorkflows( - page, - createWorkflowsResponse([ - { name: "running-1", status: WorkflowStatus.RUNNING }, - ]), - ); + await setupWorkflows(page, createWorkflowsResponse([{ name: "running-1", status: WorkflowStatus.RUNNING }])); // ACT await page.goto("/"); @@ -276,12 +259,7 @@ test.describe("Dashboard Edge Cases", () => { test("failed workflows stat card links to workflows filtered by FAILED status", async ({ page }) => { // ARRANGE await setupPools(page, createPoolResponse([{ name: "prod", status: PoolStatus.ONLINE }])); - await setupWorkflows( - page, - createWorkflowsResponse([ - { name: "failed-1", status: WorkflowStatus.FAILED }, - ]), - ); + await setupWorkflows(page, createWorkflowsResponse([{ name: "failed-1", status: WorkflowStatus.FAILED }])); // ACT await page.goto("/"); @@ -299,12 +277,7 @@ test.describe("Dashboard Edge Cases", () => { test("completed workflows stat card links to workflows filtered by COMPLETED status", async ({ page }) => { // ARRANGE await setupPools(page, createPoolResponse([{ name: "prod", status: PoolStatus.ONLINE }])); - await setupWorkflows( - page, - createWorkflowsResponse([ - { name: "completed-1", status: WorkflowStatus.COMPLETED }, - ]), - ); + await setupWorkflows(page, createWorkflowsResponse([{ name: "completed-1", status: WorkflowStatus.COMPLETED }])); // ACT await page.goto("/"); diff --git a/src/ui/e2e/journeys/display-mode-toggle.spec.ts b/src/ui/e2e/journeys/display-mode-toggle.spec.ts index 21c6db2b7c..2ec3d0ac2d 100644 --- a/src/ui/e2e/journeys/display-mode-toggle.spec.ts +++ b/src/ui/e2e/journeys/display-mode-toggle.spec.ts @@ -15,10 +15,7 @@ // SPDX-License-Identifier: Apache-2.0 import { test, expect } from "@playwright/test"; -import { - createResourcesResponse, - BackendResourceType, -} from "@/mocks/factories"; +import { createResourcesResponse, BackendResourceType } from "@/mocks/factories"; import { setupDefaultMocks, setupResources, setupProfile } from "@/e2e/utils/mock-setup"; /** diff --git a/src/ui/e2e/journeys/empty-states.spec.ts b/src/ui/e2e/journeys/empty-states.spec.ts index d2db087b08..6ec54d5de7 100644 --- a/src/ui/e2e/journeys/empty-states.spec.ts +++ b/src/ui/e2e/journeys/empty-states.spec.ts @@ -22,13 +22,7 @@ import { WorkflowStatus, PoolStatus, } from "@/mocks/factories"; -import { - setupDefaultMocks, - setupProfile, - setupWorkflows, - setupPools, - setupResources, -} from "@/e2e/utils/mock-setup"; +import { setupDefaultMocks, setupProfile, setupWorkflows, setupPools, setupResources } from "@/e2e/utils/mock-setup"; /** * Empty State Tests diff --git a/src/ui/e2e/journeys/log-viewer-recent.spec.ts b/src/ui/e2e/journeys/log-viewer-recent.spec.ts index 4440039352..074df1e85d 100644 --- a/src/ui/e2e/journeys/log-viewer-recent.spec.ts +++ b/src/ui/e2e/journeys/log-viewer-recent.spec.ts @@ -15,10 +15,7 @@ // SPDX-License-Identifier: Apache-2.0 import { test, expect } from "@playwright/test"; -import { - setupDefaultMocks, - setupProfile, -} from "@/e2e/utils/mock-setup"; +import { setupDefaultMocks, setupProfile } from "@/e2e/utils/mock-setup"; /** * Log Viewer Recent Workflows Tests @@ -81,10 +78,7 @@ test.describe("Log Viewer Recent Workflows", () => { // ARRANGE await page.goto("/log-viewer"); await page.evaluate(() => { - localStorage.setItem( - "osmo:recent-workflows", - JSON.stringify(["wf-1", "wf-2"]), - ); + localStorage.setItem("osmo:recent-workflows", JSON.stringify(["wf-1", "wf-2"])); }); await page.reload(); await page.waitForLoadState("networkidle"); @@ -103,10 +97,7 @@ test.describe("Log Viewer Recent Workflows", () => { // ARRANGE await page.goto("/log-viewer"); await page.evaluate(() => { - localStorage.setItem( - "osmo:recent-workflows", - JSON.stringify(["my-recent-workflow"]), - ); + localStorage.setItem("osmo:recent-workflows", JSON.stringify(["my-recent-workflow"])); }); await page.reload(); await page.waitForLoadState("networkidle"); @@ -122,10 +113,7 @@ test.describe("Log Viewer Recent Workflows", () => { // ARRANGE await page.goto("/log-viewer"); await page.evaluate(() => { - localStorage.setItem( - "osmo:recent-workflows", - JSON.stringify(["keep-this", "remove-this"]), - ); + localStorage.setItem("osmo:recent-workflows", JSON.stringify(["keep-this", "remove-this"])); }); await page.reload(); await page.waitForLoadState("networkidle"); @@ -137,9 +125,7 @@ test.describe("Log Viewer Recent Workflows", () => { // ACT — hover over "remove-this" to make remove button visible, then click it const removeThisEntry = page.getByText("remove-this").first(); await removeThisEntry.hover(); - await page - .getByRole("button", { name: /remove remove-this from recent workflows/i }) - .click(); + await page.getByRole("button", { name: /remove remove-this from recent workflows/i }).click(); // ASSERT — "remove-this" is gone, "keep-this" remains await expect(page.getByText("remove-this")).not.toBeVisible(); diff --git a/src/ui/e2e/journeys/log-viewer.spec.ts b/src/ui/e2e/journeys/log-viewer.spec.ts index 6b2d5b06af..dc48ba4519 100644 --- a/src/ui/e2e/journeys/log-viewer.spec.ts +++ b/src/ui/e2e/journeys/log-viewer.spec.ts @@ -15,10 +15,7 @@ // SPDX-License-Identifier: Apache-2.0 import { test, expect } from "@playwright/test"; -import { - setupDefaultMocks, - setupProfile, -} from "@/e2e/utils/mock-setup"; +import { setupDefaultMocks, setupProfile } from "@/e2e/utils/mock-setup"; /** * Log Viewer Page Journey Tests diff --git a/src/ui/e2e/journeys/not-found-page.spec.ts b/src/ui/e2e/journeys/not-found-page.spec.ts index c29ba22bde..5033b87494 100644 --- a/src/ui/e2e/journeys/not-found-page.spec.ts +++ b/src/ui/e2e/journeys/not-found-page.spec.ts @@ -85,10 +85,7 @@ test.describe("Not Found Page Content", () => { await expect(dashboardAction).toBeVisible(); // Click and wait for navigation - await Promise.all([ - page.waitForURL(/\/$/), - dashboardAction.click(), - ]); + await Promise.all([page.waitForURL(/\/$/), dashboardAction.click()]); // ASSERT — navigated to home await expect(page).toHaveURL(/\/$/); diff --git a/src/ui/e2e/journeys/occupancy-toolbar.spec.ts b/src/ui/e2e/journeys/occupancy-toolbar.spec.ts index bc17b94e6e..5621b2f7be 100644 --- a/src/ui/e2e/journeys/occupancy-toolbar.spec.ts +++ b/src/ui/e2e/journeys/occupancy-toolbar.spec.ts @@ -140,12 +140,7 @@ test.describe("Occupancy Group By Toggle URL State", () => { test("clicking By User updates URL with groupBy=user", async ({ page }) => { // ARRANGE - await setupOccupancy( - page, - createOccupancySummaries([ - { user: "alice", pool: "prod", gpu: 4 }, - ]), - ); + await setupOccupancy(page, createOccupancySummaries([{ user: "alice", pool: "prod", gpu: 4 }])); // ACT await page.goto("/occupancy"); @@ -160,12 +155,7 @@ test.describe("Occupancy Group By Toggle URL State", () => { test("clicking By Pool after By User removes groupBy from URL", async ({ page }) => { // ARRANGE - await setupOccupancy( - page, - createOccupancySummaries([ - { user: "alice", pool: "prod", gpu: 4 }, - ]), - ); + await setupOccupancy(page, createOccupancySummaries([{ user: "alice", pool: "prod", gpu: 4 }])); // ACT await page.goto("/occupancy?groupBy=user"); diff --git a/src/ui/e2e/journeys/occupancy-truncation.spec.ts b/src/ui/e2e/journeys/occupancy-truncation.spec.ts index 8aacf613e9..4a851e3c93 100644 --- a/src/ui/e2e/journeys/occupancy-truncation.spec.ts +++ b/src/ui/e2e/journeys/occupancy-truncation.spec.ts @@ -15,11 +15,7 @@ // SPDX-License-Identifier: Apache-2.0 import { test, expect } from "@playwright/test"; -import { - setupDefaultMocks, - setupProfile, - setupOccupancy, -} from "@/e2e/utils/mock-setup"; +import { setupDefaultMocks, setupProfile, setupOccupancy } from "@/e2e/utils/mock-setup"; /** * Occupancy Truncation Warning Tests @@ -53,8 +49,24 @@ function createLargeOccupancySummaries(count: number) { function createSmallOccupancySummaries() { return { summaries: [ - { user: "alice", pool: "prod", gpu: 8, cpu: 64, memory: 64 * 1024 * 1024 * 1024, storage: 100 * 1024 * 1024 * 1024, priority: "NORMAL" }, - { user: "bob", pool: "staging", gpu: 4, cpu: 32, memory: 32 * 1024 * 1024 * 1024, storage: 50 * 1024 * 1024 * 1024, priority: "NORMAL" }, + { + user: "alice", + pool: "prod", + gpu: 8, + cpu: 64, + memory: 64 * 1024 * 1024 * 1024, + storage: 100 * 1024 * 1024 * 1024, + priority: "NORMAL", + }, + { + user: "bob", + pool: "staging", + gpu: 4, + cpu: 32, + memory: 32 * 1024 * 1024 * 1024, + storage: 50 * 1024 * 1024 * 1024, + priority: "NORMAL", + }, ], }; } @@ -74,12 +86,8 @@ test.describe("Occupancy Truncation Warning", () => { await page.waitForLoadState("networkidle"); // ASSERT — truncation banner is visible - await expect( - page.getByText(/results may be incomplete/i).first(), - ).toBeVisible(); - await expect( - page.getByText(/10,000 row fetch limit/i).first(), - ).toBeVisible(); + await expect(page.getByText(/results may be incomplete/i).first()).toBeVisible(); + await expect(page.getByText(/10,000 row fetch limit/i).first()).toBeVisible(); }); test("does not show truncation warning for small datasets", async ({ page }) => { @@ -91,9 +99,7 @@ test.describe("Occupancy Truncation Warning", () => { await page.waitForLoadState("networkidle"); // ASSERT — no truncation warning visible - await expect( - page.getByText(/results may be incomplete/i), - ).not.toBeVisible(); + await expect(page.getByText(/results may be incomplete/i)).not.toBeVisible(); }); test("truncation warning does not block table rendering", async ({ page }) => { diff --git a/src/ui/e2e/journeys/occupancy.spec.ts b/src/ui/e2e/journeys/occupancy.spec.ts index 0175da9d98..27e4a3e4ab 100644 --- a/src/ui/e2e/journeys/occupancy.spec.ts +++ b/src/ui/e2e/journeys/occupancy.spec.ts @@ -15,11 +15,7 @@ // SPDX-License-Identifier: Apache-2.0 import { test, expect } from "@playwright/test"; -import { - setupDefaultMocks, - setupProfile, - setupOccupancy, -} from "@/e2e/utils/mock-setup"; +import { setupDefaultMocks, setupProfile, setupOccupancy } from "@/e2e/utils/mock-setup"; /** * Occupancy Page Journey Tests diff --git a/src/ui/e2e/journeys/panel-keyboard-interactions.spec.ts b/src/ui/e2e/journeys/panel-keyboard-interactions.spec.ts index ce5ffb182a..107f6fa1d4 100644 --- a/src/ui/e2e/journeys/panel-keyboard-interactions.spec.ts +++ b/src/ui/e2e/journeys/panel-keyboard-interactions.spec.ts @@ -213,10 +213,7 @@ test.describe("Pool Panel Quick Links Navigation", () => { test("Resources link navigates to resources with pool filter", async ({ page }) => { // ARRANGE - await setupPools( - page, - createPoolResponse([{ name: "nav-pool", status: PoolStatus.ONLINE }]), - ); + await setupPools(page, createPoolResponse([{ name: "nav-pool", status: PoolStatus.ONLINE }])); // ACT await page.goto("/pools?all=true&view=nav-pool"); @@ -237,10 +234,7 @@ test.describe("Pool Panel Quick Links Navigation", () => { test("Workflows link navigates to workflows with pool filter", async ({ page }) => { // ARRANGE - await setupPools( - page, - createPoolResponse([{ name: "wf-pool", status: PoolStatus.ONLINE }]), - ); + await setupPools(page, createPoolResponse([{ name: "wf-pool", status: PoolStatus.ONLINE }])); // ACT await page.goto("/pools?all=true&view=wf-pool"); diff --git a/src/ui/e2e/journeys/pools.spec.ts b/src/ui/e2e/journeys/pools.spec.ts index 5673103f2c..c57046f759 100644 --- a/src/ui/e2e/journeys/pools.spec.ts +++ b/src/ui/e2e/journeys/pools.spec.ts @@ -36,11 +36,14 @@ test.describe("Pools List", () => { }); test("renders all pools", async ({ page }) => { - await setupPools(page, createPoolResponse([ - { name: "production", status: PoolStatus.ONLINE }, - { name: "staging", status: PoolStatus.ONLINE }, - { name: "maintenance", status: PoolStatus.OFFLINE }, - ])); + await setupPools( + page, + createPoolResponse([ + { name: "production", status: PoolStatus.ONLINE }, + { name: "staging", status: PoolStatus.ONLINE }, + { name: "maintenance", status: PoolStatus.OFFLINE }, + ]), + ); await page.goto("/pools?all=true"); await page.waitForLoadState("networkidle"); @@ -78,10 +81,13 @@ test.describe("Pools List", () => { }); test("search creates a filter chip for the typed pool name", async ({ page }) => { - await setupPools(page, createPoolResponse([ - { name: "production", status: PoolStatus.ONLINE }, - { name: "development", status: PoolStatus.ONLINE }, - ])); + await setupPools( + page, + createPoolResponse([ + { name: "production", status: PoolStatus.ONLINE }, + { name: "development", status: PoolStatus.ONLINE }, + ]), + ); await page.goto("/pools?all=true"); await page.waitForLoadState("networkidle"); @@ -117,20 +123,23 @@ test.describe("Pool Panel", () => { }); test("shows GPU quota and capacity sections", async ({ page }) => { - await setupPools(page, createPoolResponse([ - { - name: "gpu-cluster", - status: PoolStatus.ONLINE, - resource_usage: { - quota_used: "50", - quota_free: "50", - quota_limit: "100", - total_usage: "64", - total_capacity: "128", - total_free: "64", + await setupPools( + page, + createPoolResponse([ + { + name: "gpu-cluster", + status: PoolStatus.ONLINE, + resource_usage: { + quota_used: "50", + quota_free: "50", + quota_limit: "100", + total_usage: "64", + total_capacity: "128", + total_free: "64", + }, }, - }, - ])); + ]), + ); await page.goto("/pools?all=true&view=gpu-cluster"); await page.waitForLoadState("networkidle"); @@ -164,9 +173,16 @@ test.describe("Pool Panel", () => { }); test("shows pool description when provided", async ({ page }) => { - await setupPools(page, createPoolResponse([ - { name: "described-pool", status: PoolStatus.ONLINE, description: "High-performance GPU cluster for AI training" }, - ])); + await setupPools( + page, + createPoolResponse([ + { + name: "described-pool", + status: PoolStatus.ONLINE, + description: "High-performance GPU cluster for AI training", + }, + ]), + ); await page.goto("/pools?all=true&view=described-pool"); await page.waitForLoadState("networkidle"); @@ -176,16 +192,19 @@ test.describe("Pool Panel", () => { }); test("shows platform configuration section for pools with platforms", async ({ page }) => { - await setupPools(page, createPoolResponse([ - { - name: "platform-pool", - status: PoolStatus.ONLINE, - platforms: { - dgx: { description: "DGX H100 nodes" }, - cpu: { description: "CPU-only nodes" }, + await setupPools( + page, + createPoolResponse([ + { + name: "platform-pool", + status: PoolStatus.ONLINE, + platforms: { + dgx: { description: "DGX H100 nodes" }, + cpu: { description: "CPU-only nodes" }, + }, }, - }, - ])); + ]), + ); await page.goto("/pools?all=true&view=platform-pool"); await page.waitForLoadState("networkidle"); @@ -217,9 +236,10 @@ test.describe("Pool Edge Cases", () => { }); test("offline pool is visible in the list", async ({ page }) => { - await setupPools(page, createPoolResponse([ - { name: "offline-pool", status: PoolStatus.OFFLINE, description: "Down for maintenance" }, - ])); + await setupPools( + page, + createPoolResponse([{ name: "offline-pool", status: PoolStatus.OFFLINE, description: "Down for maintenance" }]), + ); await page.goto("/pools?all=true"); await page.waitForLoadState("networkidle"); @@ -246,9 +266,7 @@ test.describe("Pool Toolbar", () => { }); test("has toolbar with search and column controls", async ({ page }) => { - await setupPools(page, createPoolResponse([ - { name: "toolbar-pool", status: PoolStatus.ONLINE }, - ])); + await setupPools(page, createPoolResponse([{ name: "toolbar-pool", status: PoolStatus.ONLINE }])); await page.goto("/pools?all=true"); await page.waitForLoadState("networkidle"); @@ -259,10 +277,13 @@ test.describe("Pool Toolbar", () => { }); test("shows results count", async ({ page }) => { - await setupPools(page, createPoolResponse([ - { name: "pool-1", status: PoolStatus.ONLINE }, - { name: "pool-2", status: PoolStatus.OFFLINE }, - ])); + await setupPools( + page, + createPoolResponse([ + { name: "pool-1", status: PoolStatus.ONLINE }, + { name: "pool-2", status: PoolStatus.OFFLINE }, + ]), + ); await page.goto("/pools?all=true"); await page.waitForLoadState("networkidle"); diff --git a/src/ui/e2e/journeys/resource-panel-content.spec.ts b/src/ui/e2e/journeys/resource-panel-content.spec.ts index c69316c13e..1ab10f03a0 100644 --- a/src/ui/e2e/journeys/resource-panel-content.spec.ts +++ b/src/ui/e2e/journeys/resource-panel-content.spec.ts @@ -15,17 +15,8 @@ // SPDX-License-Identifier: Apache-2.0 import { test, expect } from "@playwright/test"; -import { - createPoolResponse, - createResourcesResponse, - PoolStatus, - BackendResourceType, -} from "@/mocks/factories"; -import { - setupDefaultMocks, - setupPools, - setupResources, -} from "@/e2e/utils/mock-setup"; +import { createPoolResponse, createResourcesResponse, PoolStatus, BackendResourceType } from "@/mocks/factories"; +import { setupDefaultMocks, setupPools, setupResources } from "@/e2e/utils/mock-setup"; /** * Resource Panel Content Tests @@ -46,10 +37,7 @@ import { test.describe("Resource Panel Capacity Display", () => { test.beforeEach(async ({ page }) => { await setupDefaultMocks(page); - await setupPools( - page, - createPoolResponse([{ name: "test-pool", status: PoolStatus.ONLINE }]), - ); + await setupPools(page, createPoolResponse([{ name: "test-pool", status: PoolStatus.ONLINE }])); }); test("shows hostname in resource panel", async ({ page }) => { @@ -193,10 +181,7 @@ test.describe("Resource Panel Platform Config", () => { test("shows RESERVED badge for reserved resources", async ({ page }) => { // ARRANGE - await setupPools( - page, - createPoolResponse([{ name: "test-pool", status: PoolStatus.ONLINE }]), - ); + await setupPools(page, createPoolResponse([{ name: "test-pool", status: PoolStatus.ONLINE }])); await setupResources( page, createResourcesResponse([ diff --git a/src/ui/e2e/journeys/resources.spec.ts b/src/ui/e2e/journeys/resources.spec.ts index f4f8ca87fa..7bb3b8b016 100644 --- a/src/ui/e2e/journeys/resources.spec.ts +++ b/src/ui/e2e/journeys/resources.spec.ts @@ -37,18 +37,21 @@ test.describe("Resources List", () => { }); test("shows resources from all pools", async ({ page }) => { - await setupResources(page, createResourcesResponse([ - { - hostname: "node-a.cluster", - exposed_fields: { node: "node-a", "pool/platform": ["pool-a/base"] }, - pool_platform_labels: { "pool-a": ["base"] }, - }, - { - hostname: "node-b.cluster", - exposed_fields: { node: "node-b", "pool/platform": ["pool-b/gpu"] }, - pool_platform_labels: { "pool-b": ["gpu"] }, - }, - ])); + await setupResources( + page, + createResourcesResponse([ + { + hostname: "node-a.cluster", + exposed_fields: { node: "node-a", "pool/platform": ["pool-a/base"] }, + pool_platform_labels: { "pool-a": ["base"] }, + }, + { + hostname: "node-b.cluster", + exposed_fields: { node: "node-b", "pool/platform": ["pool-b/gpu"] }, + pool_platform_labels: { "pool-b": ["gpu"] }, + }, + ]), + ); await page.goto("/resources"); await page.waitForLoadState("networkidle"); @@ -58,23 +61,26 @@ test.describe("Resources List", () => { }); test("search creates a filter chip for the typed resource name", async ({ page }) => { - await setupResources(page, createResourcesResponse([ - { - hostname: "dgx-001.cluster", - exposed_fields: { node: "dgx-001", "pool/platform": ["prod/dgx"] }, - pool_platform_labels: { prod: ["dgx"] }, - }, - { - hostname: "dgx-002.cluster", - exposed_fields: { node: "dgx-002", "pool/platform": ["prod/dgx"] }, - pool_platform_labels: { prod: ["dgx"] }, - }, - { - hostname: "cpu-001.cluster", - exposed_fields: { node: "cpu-001", "pool/platform": ["prod/cpu"] }, - pool_platform_labels: { prod: ["cpu"] }, - }, - ])); + await setupResources( + page, + createResourcesResponse([ + { + hostname: "dgx-001.cluster", + exposed_fields: { node: "dgx-001", "pool/platform": ["prod/dgx"] }, + pool_platform_labels: { prod: ["dgx"] }, + }, + { + hostname: "dgx-002.cluster", + exposed_fields: { node: "dgx-002", "pool/platform": ["prod/dgx"] }, + pool_platform_labels: { prod: ["dgx"] }, + }, + { + hostname: "cpu-001.cluster", + exposed_fields: { node: "cpu-001", "pool/platform": ["prod/cpu"] }, + pool_platform_labels: { prod: ["cpu"] }, + }, + ]), + ); await page.goto("/resources"); await page.waitForLoadState("networkidle"); @@ -93,18 +99,21 @@ test.describe("Resources List", () => { }); test("pool filter chip via URL shows only that pool's resources", async ({ page }) => { - await setupResources(page, createResourcesResponse([ - { - hostname: "prod-node.cluster", - exposed_fields: { node: "prod-node", "pool/platform": ["production/base"] }, - pool_platform_labels: { production: ["base"] }, - }, - { - hostname: "dev-node.cluster", - exposed_fields: { node: "dev-node", "pool/platform": ["development/base"] }, - pool_platform_labels: { development: ["base"] }, - }, - ])); + await setupResources( + page, + createResourcesResponse([ + { + hostname: "prod-node.cluster", + exposed_fields: { node: "prod-node", "pool/platform": ["production/base"] }, + pool_platform_labels: { production: ["base"] }, + }, + { + hostname: "dev-node.cluster", + exposed_fields: { node: "dev-node", "pool/platform": ["development/base"] }, + pool_platform_labels: { development: ["base"] }, + }, + ]), + ); // Navigate with a pool chip pre-applied await page.goto("/resources?f=pool:production"); @@ -115,26 +124,29 @@ test.describe("Resources List", () => { }); test("shows all resource types in the table", async ({ page }) => { - await setupResources(page, createResourcesResponse([ - { - hostname: "shared-node.cluster", - resource_type: BackendResourceType.SHARED, - exposed_fields: { node: "shared-node", "pool/platform": ["prod/base"] }, - pool_platform_labels: { prod: ["base"] }, - }, - { - hostname: "reserved-node.cluster", - resource_type: BackendResourceType.RESERVED, - exposed_fields: { node: "reserved-node", "pool/platform": ["prod/base"] }, - pool_platform_labels: { prod: ["base"] }, - }, - { - hostname: "unused-node.cluster", - resource_type: BackendResourceType.UNUSED, - exposed_fields: { node: "unused-node", "pool/platform": ["prod/base"] }, - pool_platform_labels: { prod: ["base"] }, - }, - ])); + await setupResources( + page, + createResourcesResponse([ + { + hostname: "shared-node.cluster", + resource_type: BackendResourceType.SHARED, + exposed_fields: { node: "shared-node", "pool/platform": ["prod/base"] }, + pool_platform_labels: { prod: ["base"] }, + }, + { + hostname: "reserved-node.cluster", + resource_type: BackendResourceType.RESERVED, + exposed_fields: { node: "reserved-node", "pool/platform": ["prod/base"] }, + pool_platform_labels: { prod: ["base"] }, + }, + { + hostname: "unused-node.cluster", + resource_type: BackendResourceType.UNUSED, + exposed_fields: { node: "unused-node", "pool/platform": ["prod/base"] }, + pool_platform_labels: { prod: ["base"] }, + }, + ]), + ); await page.goto("/resources"); await page.waitForLoadState("networkidle"); @@ -153,13 +165,16 @@ test.describe("Resource Panel", () => { }); test("clicking a resource row opens the details panel", async ({ page }) => { - await setupResources(page, createResourcesResponse([ - { - hostname: "gpu-node.cluster", - exposed_fields: { node: "gpu-node", "pool/platform": ["prod/dgx"] }, - pool_platform_labels: { prod: ["dgx"] }, - }, - ])); + await setupResources( + page, + createResourcesResponse([ + { + hostname: "gpu-node.cluster", + exposed_fields: { node: "gpu-node", "pool/platform": ["prod/dgx"] }, + pool_platform_labels: { prod: ["dgx"] }, + }, + ]), + ); await page.goto("/resources"); await page.waitForLoadState("networkidle"); @@ -175,13 +190,16 @@ test.describe("Resource Panel", () => { }); test("navigating directly to a resource opens its panel", async ({ page }) => { - await setupResources(page, createResourcesResponse([ - { - hostname: "direct-node.cluster", - exposed_fields: { node: "direct-node", "pool/platform": ["prod/dgx"] }, - pool_platform_labels: { prod: ["dgx"] }, - }, - ])); + await setupResources( + page, + createResourcesResponse([ + { + hostname: "direct-node.cluster", + exposed_fields: { node: "direct-node", "pool/platform": ["prod/dgx"] }, + pool_platform_labels: { prod: ["dgx"] }, + }, + ]), + ); await page.goto("/resources?view=direct-node"); await page.waitForLoadState("networkidle"); @@ -192,13 +210,16 @@ test.describe("Resource Panel", () => { }); test("closes with the close button and clears URL state", async ({ page }) => { - await setupResources(page, createResourcesResponse([ - { - hostname: "closeable-node.cluster", - exposed_fields: { node: "closeable-node", "pool/platform": ["prod/base"] }, - pool_platform_labels: { prod: ["base"] }, - }, - ])); + await setupResources( + page, + createResourcesResponse([ + { + hostname: "closeable-node.cluster", + exposed_fields: { node: "closeable-node", "pool/platform": ["prod/base"] }, + pool_platform_labels: { prod: ["base"] }, + }, + ]), + ); await page.goto("/resources?view=closeable-node"); await page.waitForLoadState("networkidle"); @@ -213,13 +234,16 @@ test.describe("Resource Panel", () => { }); test("shows resource name in panel header", async ({ page }) => { - await setupResources(page, createResourcesResponse([ - { - hostname: "named-node.cluster", - exposed_fields: { node: "named-node", "pool/platform": ["prod/dgx"] }, - pool_platform_labels: { prod: ["dgx"] }, - }, - ])); + await setupResources( + page, + createResourcesResponse([ + { + hostname: "named-node.cluster", + exposed_fields: { node: "named-node", "pool/platform": ["prod/dgx"] }, + pool_platform_labels: { prod: ["dgx"] }, + }, + ]), + ); await page.goto("/resources?view=named-node"); await page.waitForLoadState("networkidle"); @@ -246,14 +270,17 @@ test.describe("Resource Edge Cases", () => { }); test("shows resources with node conditions", async ({ page }) => { - await setupResources(page, createResourcesResponse([ - { - hostname: "problematic-node.cluster", - conditions: ["Ready", "SchedulingDisabled", "MemoryPressure"], - exposed_fields: { node: "problematic-node", "pool/platform": ["prod/base"] }, - pool_platform_labels: { prod: ["base"] }, - }, - ])); + await setupResources( + page, + createResourcesResponse([ + { + hostname: "problematic-node.cluster", + conditions: ["Ready", "SchedulingDisabled", "MemoryPressure"], + exposed_fields: { node: "problematic-node", "pool/platform": ["prod/base"] }, + pool_platform_labels: { prod: ["base"] }, + }, + ]), + ); await page.goto("/resources"); await page.waitForLoadState("networkidle"); @@ -262,16 +289,19 @@ test.describe("Resource Edge Cases", () => { }); test("shows CPU-only nodes with zero GPU", async ({ page }) => { - await setupResources(page, createResourcesResponse([ - { - hostname: "cpu-only-node.cluster", - resource_type: BackendResourceType.SHARED, - exposed_fields: { node: "cpu-only-node", "pool/platform": ["prod/cpu"] }, - pool_platform_labels: { prod: ["cpu"] }, - allocatable_fields: { gpu: 0, cpu: 256, memory: 1024 * 1024, storage: 0 }, - usage_fields: { gpu: 0, cpu: 128, memory: 512 * 1024, storage: 0 }, - }, - ])); + await setupResources( + page, + createResourcesResponse([ + { + hostname: "cpu-only-node.cluster", + resource_type: BackendResourceType.SHARED, + exposed_fields: { node: "cpu-only-node", "pool/platform": ["prod/cpu"] }, + pool_platform_labels: { prod: ["cpu"] }, + allocatable_fields: { gpu: 0, cpu: 256, memory: 1024 * 1024, storage: 0 }, + usage_fields: { gpu: 0, cpu: 128, memory: 512 * 1024, storage: 0 }, + }, + ]), + ); await page.goto("/resources"); await page.waitForLoadState("networkidle"); @@ -286,13 +316,16 @@ test.describe("Resource Toolbar", () => { }); test("has toolbar with search controls", async ({ page }) => { - await setupResources(page, createResourcesResponse([ - { - hostname: "toolbar-node.cluster", - exposed_fields: { node: "toolbar-node", "pool/platform": ["prod/base"] }, - pool_platform_labels: { prod: ["base"] }, - }, - ])); + await setupResources( + page, + createResourcesResponse([ + { + hostname: "toolbar-node.cluster", + exposed_fields: { node: "toolbar-node", "pool/platform": ["prod/base"] }, + pool_platform_labels: { prod: ["base"] }, + }, + ]), + ); await page.goto("/resources"); await page.waitForLoadState("networkidle"); @@ -302,18 +335,21 @@ test.describe("Resource Toolbar", () => { }); test("shows results count", async ({ page }) => { - await setupResources(page, createResourcesResponse([ - { - hostname: "count-node-1.cluster", - exposed_fields: { node: "count-node-1", "pool/platform": ["prod/base"] }, - pool_platform_labels: { prod: ["base"] }, - }, - { - hostname: "count-node-2.cluster", - exposed_fields: { node: "count-node-2", "pool/platform": ["prod/base"] }, - pool_platform_labels: { prod: ["base"] }, - }, - ])); + await setupResources( + page, + createResourcesResponse([ + { + hostname: "count-node-1.cluster", + exposed_fields: { node: "count-node-1", "pool/platform": ["prod/base"] }, + pool_platform_labels: { prod: ["base"] }, + }, + { + hostname: "count-node-2.cluster", + exposed_fields: { node: "count-node-2", "pool/platform": ["prod/base"] }, + pool_platform_labels: { prod: ["base"] }, + }, + ]), + ); await page.goto("/resources"); await page.waitForLoadState("networkidle"); diff --git a/src/ui/e2e/journeys/status-display.spec.ts b/src/ui/e2e/journeys/status-display.spec.ts index 3b2b5c475a..b9c958870f 100644 --- a/src/ui/e2e/journeys/status-display.spec.ts +++ b/src/ui/e2e/journeys/status-display.spec.ts @@ -15,18 +15,8 @@ // SPDX-License-Identifier: Apache-2.0 import { test, expect } from "@playwright/test"; -import { - createWorkflowsResponse, - createPoolResponse, - WorkflowStatus, - PoolStatus, -} from "@/mocks/factories"; -import { - setupDefaultMocks, - setupProfile, - setupWorkflows, - setupPools, -} from "@/e2e/utils/mock-setup"; +import { createWorkflowsResponse, createPoolResponse, WorkflowStatus, PoolStatus } from "@/mocks/factories"; +import { setupDefaultMocks, setupProfile, setupWorkflows, setupPools } from "@/e2e/utils/mock-setup"; /** * Workflow Status Display Tests @@ -52,9 +42,7 @@ test.describe("Workflow Status Display", () => { // ARRANGE await setupWorkflows( page, - createWorkflowsResponse([ - { name: "training-job-1", status: WorkflowStatus.RUNNING, user: "alice" }, - ]), + createWorkflowsResponse([{ name: "training-job-1", status: WorkflowStatus.RUNNING, user: "alice" }]), ); // ACT @@ -70,9 +58,7 @@ test.describe("Workflow Status Display", () => { // ARRANGE await setupWorkflows( page, - createWorkflowsResponse([ - { name: "training-done", status: WorkflowStatus.COMPLETED, user: "bob" }, - ]), + createWorkflowsResponse([{ name: "training-done", status: WorkflowStatus.COMPLETED, user: "bob" }]), ); // ACT @@ -88,9 +74,7 @@ test.describe("Workflow Status Display", () => { // ARRANGE await setupWorkflows( page, - createWorkflowsResponse([ - { name: "broken-job", status: WorkflowStatus.FAILED, user: "charlie" }, - ]), + createWorkflowsResponse([{ name: "broken-job", status: WorkflowStatus.FAILED, user: "charlie" }]), ); // ACT @@ -106,9 +90,7 @@ test.describe("Workflow Status Display", () => { // ARRANGE await setupWorkflows( page, - createWorkflowsResponse([ - { name: "queued-job", status: WorkflowStatus.PENDING, user: "dave" }, - ]), + createWorkflowsResponse([{ name: "queued-job", status: WorkflowStatus.PENDING, user: "dave" }]), ); // ACT @@ -124,9 +106,7 @@ test.describe("Workflow Status Display", () => { // ARRANGE await setupWorkflows( page, - createWorkflowsResponse([ - { name: "stopping-job", status: WorkflowStatus.FAILED_CANCELED, user: "eve" }, - ]), + createWorkflowsResponse([{ name: "stopping-job", status: WorkflowStatus.FAILED_CANCELED, user: "eve" }]), ); // ACT diff --git a/src/ui/e2e/journeys/submit-workflow-form.spec.ts b/src/ui/e2e/journeys/submit-workflow-form.spec.ts index d5d9ff333a..035e01edac 100644 --- a/src/ui/e2e/journeys/submit-workflow-form.spec.ts +++ b/src/ui/e2e/journeys/submit-workflow-form.spec.ts @@ -235,6 +235,8 @@ test.describe("Submit Workflow Form Validation", () => { }); test("submits YAML labels in the body without a separate label query override", async ({ page }) => { + // Extended: this journey renders the full detail/submit surface with + // several mocked round trips. test.setTimeout(30_000); let submittedLabels: string[] | null = null; let submittedBody: string | null = null; diff --git a/src/ui/e2e/journeys/table-column-toggle.spec.ts b/src/ui/e2e/journeys/table-column-toggle.spec.ts index 56a8097253..3828bc7c9b 100644 --- a/src/ui/e2e/journeys/table-column-toggle.spec.ts +++ b/src/ui/e2e/journeys/table-column-toggle.spec.ts @@ -23,13 +23,7 @@ import { WorkflowStatus, BackendResourceType, } from "@/mocks/factories"; -import { - setupDefaultMocks, - setupProfile, - setupPools, - setupWorkflows, - setupResources, -} from "@/e2e/utils/mock-setup"; +import { setupDefaultMocks, setupProfile, setupPools, setupWorkflows, setupResources } from "@/e2e/utils/mock-setup"; /** * Table Column Visibility Tests @@ -76,9 +70,7 @@ test.describe("Pools Table Column Toggle", () => { ); }); - test("toggle columns button opens column visibility menu", async ({ - page, - }) => { + test("toggle columns button opens column visibility menu", async ({ page }) => { // ACT await page.goto("/pools?all=true"); await page.waitForLoadState("networkidle"); @@ -87,14 +79,10 @@ test.describe("Pools Table Column Toggle", () => { await page.getByRole("button", { name: /toggle columns/i }).click(); // ASSERT — dropdown menu with column options visible - await expect( - page.getByRole("menuitemcheckbox").first(), - ).toBeVisible(); + await expect(page.getByRole("menuitemcheckbox").first()).toBeVisible(); }); - test("pool table shows Status and Backend columns by default", async ({ - page, - }) => { + test("pool table shows Status and Backend columns by default", async ({ page }) => { // ACT await page.goto("/pools?all=true"); await page.waitForLoadState("networkidle"); @@ -142,9 +130,7 @@ test.describe("Workflows Table Column Toggle", () => { ); }); - test("workflow table toggle columns shows column options", async ({ - page, - }) => { + test("workflow table toggle columns shows column options", async ({ page }) => { // ACT await page.goto("/workflows?all=true"); await page.waitForLoadState("networkidle"); @@ -153,14 +139,10 @@ test.describe("Workflows Table Column Toggle", () => { await page.getByRole("button", { name: /toggle columns/i }).click(); // ASSERT — at least one column checkbox is visible - await expect( - page.getByRole("menuitemcheckbox").first(), - ).toBeVisible(); + await expect(page.getByRole("menuitemcheckbox").first()).toBeVisible(); }); - test("workflows table shows workflow names and status", async ({ - page, - }) => { + test("workflows table shows workflow names and status", async ({ page }) => { // ACT await page.goto("/workflows?all=true"); await page.waitForLoadState("networkidle"); @@ -174,12 +156,7 @@ test.describe("Workflows Table Column Toggle", () => { test.describe("Resources Table Column Toggle", () => { test.beforeEach(async ({ page }) => { await setupDefaultMocks(page); - await setupPools( - page, - createPoolResponse([ - { name: "gpu-pool", status: PoolStatus.ONLINE }, - ]), - ); + await setupPools(page, createPoolResponse([{ name: "gpu-pool", status: PoolStatus.ONLINE }])); await setupResources( page, createResourcesResponse([ @@ -209,9 +186,7 @@ test.describe("Resources Table Column Toggle", () => { ); }); - test("resources table toggle columns shows column options", async ({ - page, - }) => { + test("resources table toggle columns shows column options", async ({ page }) => { // ACT await page.goto("/resources?all=true"); await page.waitForLoadState("networkidle"); @@ -220,9 +195,7 @@ test.describe("Resources Table Column Toggle", () => { await page.getByRole("button", { name: /toggle columns/i }).click(); // ASSERT — at least one column checkbox is visible - await expect( - page.getByRole("menuitemcheckbox").first(), - ).toBeVisible(); + await expect(page.getByRole("menuitemcheckbox").first()).toBeVisible(); }); test("resources table shows node names", async ({ page }) => { @@ -235,9 +208,7 @@ test.describe("Resources Table Column Toggle", () => { await expect(page.getByText("node-b").first()).toBeVisible(); }); - test("resources table distinguishes shared and reserved types", async ({ - page, - }) => { + test("resources table distinguishes shared and reserved types", async ({ page }) => { // ACT await page.goto("/resources?all=true"); await page.waitForLoadState("networkidle"); diff --git a/src/ui/e2e/journeys/toolbar-refresh.spec.ts b/src/ui/e2e/journeys/toolbar-refresh.spec.ts index 8b15abcc39..4682bd9763 100644 --- a/src/ui/e2e/journeys/toolbar-refresh.spec.ts +++ b/src/ui/e2e/journeys/toolbar-refresh.spec.ts @@ -135,10 +135,7 @@ test.describe("Occupancy Column Toggle & Search Presets", () => { test("toggle columns button is visible in occupancy toolbar", async ({ page }) => { // ARRANGE - await setupOccupancy( - page, - createOccupancySummaries([{ user: "alice", pool: "prod", gpu: 8 }]), - ); + await setupOccupancy(page, createOccupancySummaries([{ user: "alice", pool: "prod", gpu: 8 }])); // ACT await page.goto("/occupancy"); @@ -150,10 +147,7 @@ test.describe("Occupancy Column Toggle & Search Presets", () => { test("toggle columns button opens column visibility menu in occupancy", async ({ page }) => { // ARRANGE - await setupOccupancy( - page, - createOccupancySummaries([{ user: "alice", pool: "prod", gpu: 8 }]), - ); + await setupOccupancy(page, createOccupancySummaries([{ user: "alice", pool: "prod", gpu: 8 }])); // ACT await page.goto("/occupancy"); @@ -189,10 +183,7 @@ test.describe("Occupancy Column Toggle & Search Presets", () => { test("occupancy refresh button is visible", async ({ page }) => { // ARRANGE - await setupOccupancy( - page, - createOccupancySummaries([{ user: "alice", pool: "prod", gpu: 8 }]), - ); + await setupOccupancy(page, createOccupancySummaries([{ user: "alice", pool: "prod", gpu: 8 }])); // ACT await page.goto("/occupancy"); diff --git a/src/ui/e2e/journeys/workflow-cancel-mutation.spec.ts b/src/ui/e2e/journeys/workflow-cancel-mutation.spec.ts index 25b2ce7e9f..930e792026 100644 --- a/src/ui/e2e/journeys/workflow-cancel-mutation.spec.ts +++ b/src/ui/e2e/journeys/workflow-cancel-mutation.spec.ts @@ -131,7 +131,10 @@ test.describe("Workflow Cancel Mutation Flow", () => { await page.waitForLoadState("networkidle"); // Open cancel dialog - await page.getByRole("button", { name: /cancel workflow/i }).first().click(); + await page + .getByRole("button", { name: /cancel workflow/i }) + .first() + .click(); await expect(page.getByLabel(/reason/i)).toBeVisible(); // Type a reason @@ -147,7 +150,10 @@ test.describe("Workflow Cancel Mutation Flow", () => { await page.waitForLoadState("networkidle"); // Open cancel dialog - await page.getByRole("button", { name: /cancel workflow/i }).first().click(); + await page + .getByRole("button", { name: /cancel workflow/i }) + .first() + .click(); const forceCheckbox = page.getByRole("checkbox", { name: /force cancel/i }); await expect(forceCheckbox).toBeVisible(); @@ -175,7 +181,10 @@ test.describe("Workflow Cancel Mutation Flow", () => { await page.waitForLoadState("networkidle"); // Open cancel dialog and click confirm - await page.getByRole("button", { name: /cancel workflow/i }).first().click(); + await page + .getByRole("button", { name: /cancel workflow/i }) + .first() + .click(); await page.getByRole("button", { name: /confirm/i }).click(); // ASSERT — button shows "Cancelling..." loading state @@ -188,7 +197,10 @@ test.describe("Workflow Cancel Mutation Flow", () => { await page.waitForLoadState("networkidle"); // Open cancel dialog - await page.getByRole("button", { name: /cancel workflow/i }).first().click(); + await page + .getByRole("button", { name: /cancel workflow/i }) + .first() + .click(); // Hover over the info icon next to force cancel const infoButton = page.getByRole("button", { name: /what is force cancel/i }); diff --git a/src/ui/e2e/journeys/workflow-detail-actions.spec.ts b/src/ui/e2e/journeys/workflow-detail-actions.spec.ts index 0fab749671..e62b47f7fe 100644 --- a/src/ui/e2e/journeys/workflow-detail-actions.spec.ts +++ b/src/ui/e2e/journeys/workflow-detail-actions.spec.ts @@ -72,10 +72,7 @@ function createWorkflowDetailResponse( tags: [], submit_time: oneHourAgo.toISOString(), start_time: oneHourAgo.toISOString(), - end_time: - overrides.status === WorkflowStatus.COMPLETED - ? now.toISOString() - : null, + end_time: overrides.status === WorkflowStatus.COMPLETED ? now.toISOString() : null, exec_timeout: null, queue_timeout: null, duration: 3600, @@ -87,9 +84,7 @@ function createWorkflowDetailResponse( { name: "train", status: "RUNNING", - tasks: [ - { name: "train-task", retry_id: 0, status: "RUNNING" }, - ], + tasks: [{ name: "train-task", retry_id: 0, status: "RUNNING" }], }, ] ).map((g) => ({ @@ -139,9 +134,7 @@ function createWorkflowDetailResponse( async function setupWorkflowDetail( page: Parameters[0], name: string, - data: - | ReturnType - | { status: number; detail: string }, + data: ReturnType | { status: number; detail: string }, ) { const response = "detail" in data @@ -156,9 +149,7 @@ async function setupWorkflowDetail( body: JSON.stringify(data), }; - await page.route(`**/api/workflow/${name}*`, (route) => - route.fulfill(response), - ); + await page.route(`**/api/workflow/${name}*`, (route) => route.fulfill(response)); } test.describe("Workflow Detail Actions", () => { @@ -168,9 +159,7 @@ test.describe("Workflow Detail Actions", () => { await setupProfile(page); }); - test("shows Cancel Workflow button for RUNNING workflow", async ({ - page, - }) => { + test("shows Cancel Workflow button for RUNNING workflow", async ({ page }) => { // ARRANGE const wfName = "running-wf-action"; await setupWorkflowDetail( @@ -186,14 +175,10 @@ test.describe("Workflow Detail Actions", () => { await page.waitForLoadState("networkidle"); // ASSERT — Cancel Workflow button visible in actions - await expect( - page.getByRole("button", { name: /cancel workflow/i }).first(), - ).toBeVisible(); + await expect(page.getByRole("button", { name: /cancel workflow/i }).first()).toBeVisible(); }); - test("Cancel Workflow button is disabled for COMPLETED workflow", async ({ - page, - }) => { + test("Cancel Workflow button is disabled for COMPLETED workflow", async ({ page }) => { // ARRANGE const wfName = "completed-wf-action"; await setupWorkflowDetail( @@ -205,9 +190,7 @@ test.describe("Workflow Detail Actions", () => { { name: "train", status: "COMPLETED", - tasks: [ - { name: "train-task", retry_id: 0, status: "COMPLETED" }, - ], + tasks: [{ name: "train-task", retry_id: 0, status: "COMPLETED" }], }, ], }), @@ -218,16 +201,12 @@ test.describe("Workflow Detail Actions", () => { await page.waitForLoadState("networkidle"); // ASSERT — Cancel Workflow button is visible but disabled - const cancelButton = page - .getByRole("button", { name: /cancel workflow/i }) - .first(); + const cancelButton = page.getByRole("button", { name: /cancel workflow/i }).first(); await expect(cancelButton).toBeVisible(); await expect(cancelButton).toBeDisabled(); }); - test("clicking Cancel Workflow opens confirmation dialog", async ({ - page, - }) => { + test("clicking Cancel Workflow opens confirmation dialog", async ({ page }) => { // ARRANGE const wfName = "cancel-dialog-wf"; await setupWorkflowDetail( @@ -251,17 +230,11 @@ test.describe("Workflow Detail Actions", () => { // ASSERT — dialog opens with workflow name and action buttons await expect(page.getByText("Cancel Workflow").first()).toBeVisible(); await expect(page.getByText(wfName).first()).toBeVisible(); - await expect( - page.getByRole("button", { name: /keep running/i }), - ).toBeVisible(); - await expect( - page.getByRole("button", { name: /confirm/i }), - ).toBeVisible(); + await expect(page.getByRole("button", { name: /keep running/i })).toBeVisible(); + await expect(page.getByRole("button", { name: /confirm/i })).toBeVisible(); }); - test("cancel dialog has reason input and force checkbox", async ({ - page, - }) => { + test("cancel dialog has reason input and force checkbox", async ({ page }) => { // ARRANGE const wfName = "cancel-form-wf"; await setupWorkflowDetail( @@ -285,9 +258,7 @@ test.describe("Workflow Detail Actions", () => { await expect(page.getByRole("checkbox", { name: /force cancel/i })).toBeVisible(); }); - test("Keep Running button dismisses the cancel dialog", async ({ - page, - }) => { + test("Keep Running button dismisses the cancel dialog", async ({ page }) => { // ARRANGE const wfName = "keep-running-wf"; await setupWorkflowDetail( @@ -307,17 +278,13 @@ test.describe("Workflow Detail Actions", () => { .click(); // Wait for dialog to appear - await expect( - page.getByRole("button", { name: /keep running/i }), - ).toBeVisible(); + await expect(page.getByRole("button", { name: /keep running/i })).toBeVisible(); // Click Keep Running await page.getByRole("button", { name: /keep running/i }).click(); // ASSERT — dialog is dismissed - await expect( - page.getByRole("button", { name: /keep running/i }), - ).not.toBeVisible(); + await expect(page.getByRole("button", { name: /keep running/i })).not.toBeVisible(); }); }); @@ -328,16 +295,10 @@ test.describe("Workflow Detail Tabs", () => { await setupProfile(page); }); - test("shows all panel tabs: Overview, Tasks, Logs, Events, Spec", async ({ - page, - }) => { + test("shows all panel tabs: Overview, Tasks, Logs, Events, Spec", async ({ page }) => { // ARRANGE const wfName = "tabs-wf"; - await setupWorkflowDetail( - page, - wfName, - createWorkflowDetailResponse(wfName), - ); + await setupWorkflowDetail(page, wfName, createWorkflowDetailResponse(wfName)); // ACT await page.goto(`/workflows/${wfName}`); @@ -363,9 +324,7 @@ test.describe("Workflow Detail Tabs", () => { { name: "train", status: "COMPLETED", - tasks: [ - { name: "train-task", retry_id: 0, status: "COMPLETED" }, - ], + tasks: [{ name: "train-task", retry_id: 0, status: "COMPLETED" }], }, ], }), @@ -376,9 +335,7 @@ test.describe("Workflow Detail Tabs", () => { await page.waitForLoadState("networkidle"); // ASSERT — Resubmit button visible - await expect( - page.getByRole("button", { name: /resubmit workflow/i }).first(), - ).toBeVisible(); + await expect(page.getByRole("button", { name: /resubmit workflow/i }).first()).toBeVisible(); }); test("workflow detail shows pool and backend info", async ({ page }) => { @@ -397,8 +354,6 @@ test.describe("Workflow Detail Tabs", () => { await page.waitForLoadState("networkidle"); // ASSERT — pool name visible in details - await expect( - page.getByText("production-pool").first(), - ).toBeVisible(); + await expect(page.getByText("production-pool").first()).toBeVisible(); }); }); diff --git a/src/ui/e2e/journeys/workflow-detail-overview.spec.ts b/src/ui/e2e/journeys/workflow-detail-overview.spec.ts index faa89ab1dd..1e9d3a4c1a 100644 --- a/src/ui/e2e/journeys/workflow-detail-overview.spec.ts +++ b/src/ui/e2e/journeys/workflow-detail-overview.spec.ts @@ -343,6 +343,8 @@ test.describe("Workflow Detail Overview — Details Section", () => { }); test("shows immutable workflow labels separately from tags", async ({ page }) => { + // Extended: this journey renders the full detail/submit surface with + // several mocked round trips. test.setTimeout(30_000); const wfName = "labels-wf"; await setupWorkflowDetail( @@ -362,10 +364,6 @@ test.describe("Workflow Detail Overview — Details Section", () => { const experimentLabelLink = page.getByRole("link", { name: "experiment=run42", exact: true }); await expect(teamLabelLink).toHaveAttribute("href", "/workflows?f=label:team%3Drobotics&all=true"); await expect(experimentLabelLink).toHaveAttribute("href", "/workflows?f=label:experiment%3Drun42&all=true"); - - const teamLabelUrl = new URL((await teamLabelLink.getAttribute("href"))!, "https://osmo.invalid"); - expect(teamLabelUrl.searchParams.get("f")).toBe("label:team=robotics"); - expect(teamLabelUrl.searchParams.get("all")).toBe("true"); await expect(page.getByText("Tags", { exact: true })).toBeVisible(); }); @@ -373,8 +371,7 @@ test.describe("Workflow Detail Overview — Details Section", () => { // The backend recomputes warnings from the current policy for every // status, including COMPLETED, so users see violations on finished runs. const wfName = "warnings-wf"; - const warning = - "Workflow is missing label 'PPP'; add it now to avoid rejected submissions once it is required."; + const warning = "Workflow is missing label 'PPP'; add it now to avoid rejected submissions once it is required."; await setupWorkflowDetail( page, wfName, diff --git a/src/ui/e2e/journeys/workflow-detail-tabs.spec.ts b/src/ui/e2e/journeys/workflow-detail-tabs.spec.ts index 19bfbb420c..87c59efc42 100644 --- a/src/ui/e2e/journeys/workflow-detail-tabs.spec.ts +++ b/src/ui/e2e/journeys/workflow-detail-tabs.spec.ts @@ -73,8 +73,7 @@ function createWorkflowDetailResponse( tags: overrides.tags ?? ["training", "v2"], submit_time: oneHourAgo.toISOString(), start_time: oneHourAgo.toISOString(), - end_time: - overrides.status === WorkflowStatus.COMPLETED ? now.toISOString() : null, + end_time: overrides.status === WorkflowStatus.COMPLETED ? now.toISOString() : null, exec_timeout: null, queue_timeout: null, duration: 3600, @@ -110,9 +109,7 @@ function createWorkflowDetailResponse( remaining_upstream_groups: [], downstream_groups: g.downstream_groups ?? [], failure_message: null, - tasks: ( - g.tasks ?? [{ name: `${g.name}-task`, retry_id: 0, status: g.status ?? "RUNNING" }] - ).map((t) => ({ + tasks: (g.tasks ?? [{ name: `${g.name}-task`, retry_id: 0, status: g.status ?? "RUNNING" }]).map((t) => ({ name: t.name, retry_id: t.retry_id ?? 0, status: t.status ?? "RUNNING", @@ -157,11 +154,7 @@ test.describe("Workflow Detail Tab Content", () => { await page.emulateMedia({ reducedMotion: "reduce" }); await setupDefaultMocks(page); await setupProfile(page); - await setupWorkflowDetail( - page, - wfName, - createWorkflowDetailResponse(wfName), - ); + await setupWorkflowDetail(page, wfName, createWorkflowDetailResponse(wfName)); }); test("Overview tab shows timeline and details sections", async ({ page }) => { @@ -235,12 +228,8 @@ test.describe("Workflow Detail Tab Content", () => { const toolbar = page.getByRole("toolbar", { name: "Spec viewer controls" }); await expect(toolbar).toBeVisible({ timeout: 10_000 }); - await expect( - page.getByRole("radio", { name: /yaml/i }), - ).toBeVisible(); - await expect( - page.getByRole("radio", { name: /template/i }), - ).toBeVisible(); + await expect(page.getByRole("radio", { name: /yaml/i })).toBeVisible(); + await expect(page.getByRole("radio", { name: /template/i })).toBeVisible(); }); test("Events tab renders event viewer component", async ({ page }) => { diff --git a/src/ui/e2e/journeys/workflow-detail.spec.ts b/src/ui/e2e/journeys/workflow-detail.spec.ts index 9e8408a3fa..69fc0d291b 100644 --- a/src/ui/e2e/journeys/workflow-detail.spec.ts +++ b/src/ui/e2e/journeys/workflow-detail.spec.ts @@ -16,10 +16,7 @@ import { test, expect } from "@playwright/test"; import { WorkflowStatus } from "@/mocks/factories"; -import { - setupDefaultMocks, - setupProfile, -} from "@/e2e/utils/mock-setup"; +import { setupDefaultMocks, setupProfile } from "@/e2e/utils/mock-setup"; /** * Workflow Detail Page Journey Tests @@ -77,22 +74,22 @@ function createWorkflowDetailResponse( tags: [], submit_time: oneHourAgo.toISOString(), start_time: oneHourAgo.toISOString(), - end_time: overrides.status === WorkflowStatus.COMPLETED - ? now.toISOString() - : null, + end_time: overrides.status === WorkflowStatus.COMPLETED ? now.toISOString() : null, exec_timeout: null, queue_timeout: null, duration: 3600, queued_time: 5, status: overrides.status ?? WorkflowStatus.RUNNING, outputs: "", - groups: (overrides.groups ?? [ - { - name: "train", - status: "RUNNING", - tasks: [{ name: "train-task", retry_id: 0, status: "RUNNING" }], - }, - ]).map((g) => ({ + groups: ( + overrides.groups ?? [ + { + name: "train", + status: "RUNNING", + tasks: [{ name: "train-task", retry_id: 0, status: "RUNNING" }], + }, + ] + ).map((g) => ({ name: g.name, status: g.status ?? "RUNNING", start_time: oneHourAgo.toISOString(), @@ -203,9 +200,7 @@ test.describe("Workflow Detail Page", () => { // ASSERT — page must not crash, should show error state // The SSR prefetch may fail silently, then the client-side fetch triggers error await expect(page.locator("body")).not.toBeEmpty(); - await expect( - page.getByText(/error|unable to load|not found/i).first() - ).toBeVisible({ timeout: 15_000 }); + await expect(page.getByText(/error|unable to load|not found/i).first()).toBeVisible({ timeout: 15_000 }); }); test("Workflows breadcrumb link navigates back to workflows list", async ({ page }) => { @@ -410,8 +405,6 @@ test.describe("Workflow Detail 404", () => { // ASSERT — should show not-found or error message await expect(page.locator("body")).not.toBeEmpty(); - await expect( - page.getByText(/not found|error|does not exist/i).first() - ).toBeVisible({ timeout: 15_000 }); + await expect(page.getByText(/not found|error|does not exist/i).first()).toBeVisible({ timeout: 15_000 }); }); }); diff --git a/src/ui/e2e/journeys/workflow-pagination.spec.ts b/src/ui/e2e/journeys/workflow-pagination.spec.ts index f84e4238b1..f2460d802f 100644 --- a/src/ui/e2e/journeys/workflow-pagination.spec.ts +++ b/src/ui/e2e/journeys/workflow-pagination.spec.ts @@ -15,15 +15,8 @@ // SPDX-License-Identifier: Apache-2.0 import { test, expect } from "@playwright/test"; -import { - createWorkflowsResponse, - WorkflowStatus, -} from "@/mocks/factories"; -import { - setupDefaultMocks, - setupProfile, - setupWorkflows, -} from "@/e2e/utils/mock-setup"; +import { createWorkflowsResponse, WorkflowStatus } from "@/mocks/factories"; +import { setupDefaultMocks, setupProfile, setupWorkflows } from "@/e2e/utils/mock-setup"; /** * Workflow Pagination / Infinite Scroll Tests @@ -71,9 +64,7 @@ test.describe("Workflow Pagination", () => { await setupWorkflows( page, createWorkflowsResponse( - [ - { name: "wf-single", status: WorkflowStatus.COMPLETED, user: "alice" }, - ], + [{ name: "wf-single", status: WorkflowStatus.COMPLETED, user: "alice" }], false, // moreEntries = false ), ); diff --git a/src/ui/e2e/journeys/workflow-resubmit-panel.spec.ts b/src/ui/e2e/journeys/workflow-resubmit-panel.spec.ts index 6a9b8a9bbd..b18a8bfd7a 100644 --- a/src/ui/e2e/journeys/workflow-resubmit-panel.spec.ts +++ b/src/ui/e2e/journeys/workflow-resubmit-panel.spec.ts @@ -244,6 +244,8 @@ test.describe("Workflow Resubmit Panel", () => { }); test("resubmit preserves existing label keys while allowing value overrides", async ({ page }) => { + // Extended: this journey renders the full detail/submit surface with + // several mocked round trips. test.setTimeout(30_000); await page.goto(`/workflows/${wfName}`); await page.waitForLoadState("networkidle"); diff --git a/src/ui/e2e/journeys/workflow-spec-viewer.spec.ts b/src/ui/e2e/journeys/workflow-spec-viewer.spec.ts index 52b5f55619..672cc8c112 100644 --- a/src/ui/e2e/journeys/workflow-spec-viewer.spec.ts +++ b/src/ui/e2e/journeys/workflow-spec-viewer.spec.ts @@ -104,10 +104,7 @@ function createWorkflowDetailForSpec(name: string) { }; } -async function setupWorkflowAndSpec( - page: Parameters[0], - name: string, -) { +async function setupWorkflowAndSpec(page: Parameters[0], name: string) { const data = createWorkflowDetailForSpec(name); // Mock the spec endpoint — registered BEFORE the catch-all so LIFO gives these priority diff --git a/src/ui/e2e/utils/mock-setup.ts b/src/ui/e2e/utils/mock-setup.ts index b6b9f02a99..e06adc7a64 100644 --- a/src/ui/e2e/utils/mock-setup.ts +++ b/src/ui/e2e/utils/mock-setup.ts @@ -96,10 +96,7 @@ export async function setupPools(page: Page, data: PoolResponse | ApiError): Pro await page.route("**/api/pool_quota*", (route) => route.fulfill(response)); } -export async function setupResources( - page: Page, - data: ResourcesResponse | ApiError, -): Promise { +export async function setupResources(page: Page, data: ResourcesResponse | ApiError): Promise { if ("detail" in data) { const response = { status: data.status, @@ -118,8 +115,7 @@ export async function setupResources( const filtered = { resources: data.resources?.filter((r) => { - const pp = - ((r.exposed_fields as Record)?.["pool/platform"] as string[]) ?? []; + const pp = ((r.exposed_fields as Record)?.["pool/platform"] as string[]) ?? []; return pp.some((p) => pools.some((pool) => p.startsWith(`${pool}/`))); }) ?? [], }; @@ -163,10 +159,7 @@ export async function setupWorkflows( // ── Occupancy (task summary) ───────────────────────────────────────────────── -export async function setupOccupancy( - page: Page, - data: { summaries: unknown[] } | ApiError, -): Promise { +export async function setupOccupancy(page: Page, data: { summaries: unknown[] } | ApiError): Promise { const response = "detail" in data ? { status: data.status, contentType: CT_JSON, body: JSON.stringify({ detail: data.detail }) } diff --git a/src/ui/src/components/submit-workflow/submit-workflow-config-panel.tsx b/src/ui/src/components/submit-workflow/submit-workflow-config-panel.tsx index 2033ad8567..3d7d453867 100644 --- a/src/ui/src/components/submit-workflow/submit-workflow-config-panel.tsx +++ b/src/ui/src/components/submit-workflow/submit-workflow-config-panel.tsx @@ -201,8 +201,8 @@ export const SubmitWorkflowConfigPanel = memo(function SubmitWorkflowConfigPanel className="space-y-1 rounded border border-amber-200 bg-amber-50 px-3 py-2 text-[11px] text-amber-800 dark:border-amber-700/50 dark:bg-amber-900/20 dark:text-amber-300" role="status" > - {validationWarnings.map((warning) => ( -

{warning}

+ {validationWarnings.map((warning, index) => ( +

{warning}

))}
)} diff --git a/src/ui/src/components/submit-workflow/use-submit-workflow-form.ts b/src/ui/src/components/submit-workflow/use-submit-workflow-form.ts index 7f6fd8b9dd..bd432490ee 100644 --- a/src/ui/src/components/submit-workflow/use-submit-workflow-form.ts +++ b/src/ui/src/components/submit-workflow/use-submit-workflow-form.ts @@ -24,8 +24,6 @@ "use client"; import { useState, useCallback, useMemo } from "react"; - -const NO_WARNINGS: string[] = []; import { toast } from "sonner"; import { useNavigationRouter } from "@/hooks/use-navigation-router"; import { useServices } from "@/contexts/service-context"; @@ -35,6 +33,8 @@ import { useProfile } from "@/lib/api/adapter/hooks"; import { usePoolSelection } from "@/components/workflow/use-pool-selection"; import { detectLocalpathUsage, type LocalpathWarnings } from "@/components/submit-workflow/detect-localpath"; +const NO_WARNINGS: string[] = []; + /** Extract a human-readable error message from various error shapes. */ function extractErrorMessage(err: unknown): string { if (!err) return "Unknown error"; diff --git a/src/ui/src/components/workflow/workflow-label-editor.tsx b/src/ui/src/components/workflow/workflow-label-editor.tsx index aa6b28a67f..8f4f50b7d0 100644 --- a/src/ui/src/components/workflow/workflow-label-editor.tsx +++ b/src/ui/src/components/workflow/workflow-label-editor.tsx @@ -26,6 +26,7 @@ export interface WorkflowLabelEditorProps { onChange: (labels: WorkflowLabelDraft[]) => void; disabled?: boolean; error?: string | null; + /** Count of leading drafts seeded from the workflow's own labels; their keys are locked. */ lockedLabelCount?: number; } @@ -47,7 +48,7 @@ export function WorkflowLabelEditor({ return (

- Per-run overrides take precedence over labels in the workflow YAML. + Per-run overrides take precedence over labels in the workflow specification. {lockedLabelCount > 0 && " Existing keys cannot be removed here; edit the workflow specification to remove one."}

@@ -94,7 +95,7 @@ export function WorkflowLabelEditor({ type="button" variant="outline" size="sm" - aria-label="Add workflow label" + aria-label="Add label" disabled={disabled || labels.length >= MAX_WORKFLOW_LABELS} onClick={() => onChange([...labels, { key: "", value: "" }])} > diff --git a/src/ui/src/features/workflows/detail/components/resubmit/use-resubmit-form.ts b/src/ui/src/features/workflows/detail/components/resubmit/use-resubmit-form.ts index 3f6b1adc97..eb6eebcabc 100644 --- a/src/ui/src/features/workflows/detail/components/resubmit/use-resubmit-form.ts +++ b/src/ui/src/features/workflows/detail/components/resubmit/use-resubmit-form.ts @@ -95,6 +95,9 @@ export function useResubmitForm({ workflow, onSuccess }: UseResubmitFormOptions) ? `Workflow resubmitted as ${newWorkflowName}` : "Workflow resubmitted successfully"; + for (const warning of warnings) { + toast.warning(warning); + } toast.success(message, { action: newWorkflowName ? { @@ -103,9 +106,6 @@ export function useResubmitForm({ workflow, onSuccess }: UseResubmitFormOptions) } : undefined, }); - for (const warning of warnings) { - toast.warning(warning); - } onSuccess?.(); }, diff --git a/src/ui/src/features/workflows/detail/components/resubmit/use-resubmit-mutation.ts b/src/ui/src/features/workflows/detail/components/resubmit/use-resubmit-mutation.ts index e9c35e7764..88045c7230 100644 --- a/src/ui/src/features/workflows/detail/components/resubmit/use-resubmit-mutation.ts +++ b/src/ui/src/features/workflows/detail/components/resubmit/use-resubmit-mutation.ts @@ -21,7 +21,7 @@ import { useServices } from "@/contexts/service-context"; import { resubmitWorkflow, type ResubmitParams } from "@/features/workflows/list/lib/actions"; export interface UseResubmitMutationOptions { - /** Called on successful resubmission with the new workflow name */ + /** Called on successful resubmission with the new workflow name and any admission warnings */ onSuccess?: (newWorkflowName: string | undefined, warnings: string[]) => void; } diff --git a/src/ui/src/features/workflows/list/lib/actions.ts b/src/ui/src/features/workflows/list/lib/actions.ts index d487a46fdb..da4e5b55d6 100644 --- a/src/ui/src/features/workflows/list/lib/actions.ts +++ b/src/ui/src/features/workflows/list/lib/actions.ts @@ -302,7 +302,7 @@ export interface ResubmitParams { * - If spec is provided: sends template_spec in body (custom workflow) * - If spec is NOT provided: sends workflow_id query param (reuses original spec) * - * @param params - Resubmit configuration (workflowId, poolName, priority, optional spec) + * @param params - Resubmit configuration (workflowId, poolName, priority, labels, optional spec) * @returns Result with the new workflow name on success, or error message */ export async function resubmitWorkflow(params: ResubmitParams): Promise { diff --git a/src/ui/src/features/workflows/list/lib/workflow-search-fields.ts b/src/ui/src/features/workflows/list/lib/workflow-search-fields.ts index ea97f453b3..81370f539e 100644 --- a/src/ui/src/features/workflows/list/lib/workflow-search-fields.ts +++ b/src/ui/src/features/workflows/list/lib/workflow-search-fields.ts @@ -84,6 +84,8 @@ export const WORKFLOW_FIELD: Readonly [], }, no_label: { + // The id matches the backend query parameter; the typed prefix stays + // kebab-case like the other chips. id: "no_label", label: "Missing Label", hint: "workflow missing a label key", diff --git a/src/ui/src/lib/workflow-labels.ts b/src/ui/src/lib/workflow-labels.ts index 790853b959..b317ea51ca 100644 --- a/src/ui/src/lib/workflow-labels.ts +++ b/src/ui/src/lib/workflow-labels.ts @@ -1,3 +1,7 @@ +/** + * Workflow label formatting, draft editing, and validation helpers + * shared by the detail, resubmit, and submit surfaces. + */ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,6 +18,7 @@ // // SPDX-License-Identifier: Apache-2.0 +// Mirrors MAX_WORKFLOW_LABELS in src/lib/utils/validation.py. export const MAX_WORKFLOW_LABELS = 16; export interface WorkflowLabelDraft { From eea0c343b41360fb6f4c0d80363b12641fd3a4af Mon Sep 17 00:00:00 2001 From: Jiaen Ren Date: Mon, 27 Jul 2026 17:02:52 -0700 Subject: [PATCH 08/12] Regenerate the OpenAPI contract for the label feature; generic label key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The UI slice owns the single regeneration of the OpenAPI-derived artifacts (openapi.json + generated client + mocks) for the whole label stack — the backend slices (#1220 and the B4-B8 PRs) deliberately carry none, so this avoids spreading generator drift across the stack. Regenerated from the full server surface (config policy, admission, list filters). Also replace the NVIDIA-internal 'PPP' example key with the generic 'project' in the UI tests and e2e journeys. Verified: tsc, eslint, vitest (1017), and production build all pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../e2e/journeys/submit-workflow-form.spec.ts | 6 +- .../journeys/workflow-detail-overview.spec.ts | 2 +- src/ui/e2e/journeys/workflow-filters.spec.ts | 4 +- .../journeys/workflow-resubmit-panel.spec.ts | 4 +- src/ui/openapi.json | 231 ++- .../workflows/list/lib/actions.test.ts | 12 +- src/ui/src/lib/api/generated.ts | 1746 +++++++++-------- src/ui/src/lib/workflow-labels.test.ts | 4 +- src/ui/src/mocks/generated-mocks.ts | 1513 +++++++------- 9 files changed, 2036 insertions(+), 1486 deletions(-) diff --git a/src/ui/e2e/journeys/submit-workflow-form.spec.ts b/src/ui/e2e/journeys/submit-workflow-form.spec.ts index 035e01edac..221eb90ade 100644 --- a/src/ui/e2e/journeys/submit-workflow-form.spec.ts +++ b/src/ui/e2e/journeys/submit-workflow-form.spec.ts @@ -262,7 +262,7 @@ test.describe("Submit Workflow Form Validation", () => { await waitForPoolSelected(overlay, "test-pool"); const editor = overlay.getByRole("textbox", { name: "YAML workflow specification editor" }); await editor.click(); - await page.keyboard.insertText("workflow:\n labels:\n PPP: robotics\n tasks:\n - name: hello"); + await page.keyboard.insertText("workflow:\n labels:\n project: robotics\n tasks:\n - name: hello"); await expect(overlay.getByText("Workflow Labels", { exact: true })).toHaveCount(0); await expect(overlay.getByRole("button", { name: "Add workflow label" })).toHaveCount(0); @@ -270,12 +270,12 @@ test.describe("Submit Workflow Form Validation", () => { await expect.poll(() => submittedLabels).toEqual([]); await expect.poll(() => submittedBody).toContain("labels:"); - await expect.poll(() => submittedBody).toContain("PPP: robotics"); + await expect.poll(() => submittedBody).toContain("project: robotics"); await expect(page.getByText("Workflow submitted as yaml-labels")).toBeVisible(); }); test("shows workflow policy warnings returned by validation", async ({ page }) => { - const warning = "Workflow is missing label 'PPP'; add it now to avoid rejected submissions once it is required."; + const warning = "Workflow is missing label 'project'; add it now to avoid rejected submissions once it is required."; let submittedLabels: string[] | null = null; await page.route("**/api/pool/test-pool/workflow*", (route) => { const url = new URL(route.request().url()); diff --git a/src/ui/e2e/journeys/workflow-detail-overview.spec.ts b/src/ui/e2e/journeys/workflow-detail-overview.spec.ts index 1e9d3a4c1a..d985eb57e6 100644 --- a/src/ui/e2e/journeys/workflow-detail-overview.spec.ts +++ b/src/ui/e2e/journeys/workflow-detail-overview.spec.ts @@ -371,7 +371,7 @@ test.describe("Workflow Detail Overview — Details Section", () => { // The backend recomputes warnings from the current policy for every // status, including COMPLETED, so users see violations on finished runs. const wfName = "warnings-wf"; - const warning = "Workflow is missing label 'PPP'; add it now to avoid rejected submissions once it is required."; + const warning = "Workflow is missing label 'project'; add it now to avoid rejected submissions once it is required."; await setupWorkflowDetail( page, wfName, diff --git a/src/ui/e2e/journeys/workflow-filters.spec.ts b/src/ui/e2e/journeys/workflow-filters.spec.ts index b98ecd0022..1d2868fc86 100644 --- a/src/ui/e2e/journeys/workflow-filters.spec.ts +++ b/src/ui/e2e/journeys/workflow-filters.spec.ts @@ -249,7 +249,7 @@ test.describe("Workflow URL Filter State", () => { test("forwards wildcard and inline-alternative workflow label selectors unchanged", async ({ page }) => { const response = createWorkflowsResponse([ - { name: "label-wf", status: WorkflowStatus.RUNNING, user: "test-user", labels: { PPP: "robotics_team" } }, + { name: "label-wf", status: WorkflowStatus.RUNNING, user: "test-user", labels: { project: "robotics_team" } }, ]); const observedLabelSelectors: string[][] = []; await page.route("**/api/workflow?*", (route) => { @@ -264,7 +264,7 @@ test.describe("Workflow URL Filter State", () => { await page.goto("/workflows?all=true"); await page.waitForLoadState("networkidle"); - const selectors = ["PPP=(team_*|osmo_*)", "PPP=team_(a|b)"]; + const selectors = ["project=(team_*|osmo_*)", "project=team_(a|b)"]; const filterInput = page.getByRole("combobox", { name: /search and filter/i }); for (const selector of selectors) { await filterInput.fill(`label:${selector}`); diff --git a/src/ui/e2e/journeys/workflow-resubmit-panel.spec.ts b/src/ui/e2e/journeys/workflow-resubmit-panel.spec.ts index b18a8bfd7a..22bda614f6 100644 --- a/src/ui/e2e/journeys/workflow-resubmit-panel.spec.ts +++ b/src/ui/e2e/journeys/workflow-resubmit-panel.spec.ts @@ -110,7 +110,7 @@ function createCompletedWorkflow(name: string) { app_name: null, app_version: null, plugins: { rsync: false }, - labels: { PPP: "robotics" }, + labels: { project: "robotics" }, }; } @@ -255,7 +255,7 @@ test.describe("Workflow Resubmit Panel", () => { .click(); const panel = page.locator(`[aria-label="Resubmit workflow: ${wfName}"]`); - await expect(panel.getByRole("textbox", { name: "Workflow label key 1" })).toHaveValue("PPP"); + await expect(panel.getByRole("textbox", { name: "Workflow label key 1" })).toHaveValue("project"); await expect(panel.getByRole("textbox", { name: "Workflow label key 1" })).toBeDisabled(); await expect(panel.getByRole("button", { name: "Remove workflow label 1" })).toBeDisabled(); await panel.getByRole("textbox", { name: "Workflow label value 1" }).fill("simulation"); diff --git a/src/ui/openapi.json b/src/ui/openapi.json index 02127094ed..a287047d5a 100644 --- a/src/ui/openapi.json +++ b/src/ui/openapi.json @@ -2641,7 +2641,7 @@ { "type": "array", "items": { - "$ref": "#/components/schemas/src__lib__utils__config_history__ConfigHistoryType" + "$ref": "#/components/schemas/ConfigHistoryType" } }, { @@ -3007,7 +3007,7 @@ "in": "query", "required": true, "schema": { - "$ref": "#/components/schemas/src__lib__utils__config_history__ConfigHistoryType" + "$ref": "#/components/schemas/ConfigHistoryType" } }, { @@ -5137,6 +5137,50 @@ }, "explode": true }, + { + "name": "label", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ], + "description": "Workflow label selector: key=value with optional * wildcards and (a|b) alternatives, for example key=(team_*|osmo_*) or key=team_(a|b). Repeat for AND semantics.", + "title": "Label" + }, + "description": "Workflow label selector: key=value with optional * wildcards and (a|b) alternatives, for example key=(team_*|osmo_*) or key=team_(a|b). Repeat for AND semantics.", + "explode": true + }, + { + "name": "no_label", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ], + "description": "Label key that must be absent from the workflow; workflows without any labels match. Repeat for AND semantics.", + "title": "No Label" + }, + "description": "Label key that must be absent from the workflow; workflows without any labels match. Repeat for AND semantics.", + "explode": true + }, { "name": "x-osmo-user", "in": "header", @@ -6898,6 +6942,20 @@ }, "explode": true }, + { + "name": "label", + "in": "query", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "default": [], + "title": "Label" + }, + "explode": true + }, { "name": "x-osmo-user", "in": "header", @@ -7976,7 +8034,7 @@ "ConfigHistory": { "properties": { "config_type": { - "$ref": "#/components/schemas/src__lib__utils__config_history__ConfigHistoryType" + "$ref": "#/components/schemas/ConfigHistoryType" }, "name": { "type": "string", @@ -8029,6 +8087,23 @@ "title": "ConfigHistory", "description": "Object storing config history." }, + "ConfigHistoryType": { + "type": "string", + "enum": [ + "DATASET", + "SERVICE", + "WORKFLOW", + "BACKEND", + "POOL", + "POD_TEMPLATE", + "GROUP_TEMPLATE", + "RESOURCE_VALIDATION", + "BACKEND_TEST", + "ROLE" + ], + "title": "ConfigHistoryType", + "description": "Type of configs supported by config history " + }, "ConfigsRequest": { "properties": { "description": { @@ -8643,6 +8718,75 @@ "title": "JwtTokenResponse", "description": "Response for JWT token creation endpoints." }, + "LabelEnforcement": { + "type": "string", + "enum": [ + "off", + "warn", + "enforce" + ], + "title": "LabelEnforcement", + "description": "Per-key policy strictness: 'off' skips checking, 'warn' surfaces\nmissing or unlisted values as submission warnings, and 'enforce'\nrejects them." + }, + "LabelPolicy": { + "properties": { + "key": { + "type": "string", + "title": "Key" + }, + "allow_list": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Allow List", + "default": [] + }, + "enforcement": { + "$ref": "#/components/schemas/LabelEnforcement", + "default": "off" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "key" + ], + "title": "LabelPolicy", + "description": "Configuration for one admin-designated workflow label key.\n\nAn empty allow_list accepts any well-formed value; enforcement then\napplies only to the key being present." + }, + "LabelsConfig-Input": { + "properties": { + "policy": { + "items": { + "$ref": "#/components/schemas/LabelPolicy" + }, + "type": "array", + "title": "Policy", + "default": [] + } + }, + "additionalProperties": false, + "type": "object", + "title": "LabelsConfig", + "description": "Curated workflow label policy; empty by default, so no policy\napplies until configured." + }, + "LabelsConfig-Output": { + "properties": { + "policy": { + "items": { + "$ref": "#/components/schemas/LabelPolicy" + }, + "type": "array", + "title": "Policy", + "default": [] + } + }, + "additionalProperties": false, + "type": "object", + "title": "LabelsConfig", + "description": "Curated workflow label policy; empty by default, so no policy\napplies until configured." + }, "ListBackendsResponse": { "properties": { "backends": { @@ -9123,6 +9267,22 @@ "type": "object", "title": "NotificationConfig" }, + "OperableConfigHistoryType": { + "type": "string", + "enum": [ + "SERVICE", + "WORKFLOW", + "BACKEND", + "POOL", + "POD_TEMPLATE", + "GROUP_TEMPLATE", + "RESOURCE_VALIDATION", + "BACKEND_TEST", + "ROLE" + ], + "title": "OperableConfigHistoryType", + "description": "Type of configs supported by config history mutations." + }, "OperatorType": { "type": "string", "enum": [ @@ -11679,7 +11839,7 @@ "title": "Tags" }, "config_type": { - "$ref": "#/components/schemas/src__lib__utils__config_history__ConfigHistoryType" + "$ref": "#/components/schemas/OperableConfigHistoryType" }, "revision": { "type": "integer", @@ -12010,6 +12170,13 @@ } ], "title": "Dashboard Url" + }, + "warnings": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Warnings" } }, "additionalProperties": false, @@ -12954,6 +13121,13 @@ "type": { "type": "string", "title": "Error Type" + }, + "input": { + "title": "Input" + }, + "ctx": { + "type": "object", + "title": "Context" } }, "type": "object", @@ -13088,6 +13262,12 @@ } } }, + "labels_config": { + "$ref": "#/components/schemas/LabelsConfig-Input", + "default": { + "policy": [] + } + }, "max_num_tasks": { "type": "integer", "title": "Max Num Tasks", @@ -13249,6 +13429,12 @@ } } }, + "labels_config": { + "$ref": "#/components/schemas/LabelsConfig-Output", + "default": { + "policy": [] + } + }, "max_num_tasks": { "type": "integer", "title": "Max Num Tasks", @@ -13612,6 +13798,20 @@ "priority": { "type": "string", "title": "Priority" + }, + "labels": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "title": "Labels" + }, + "warnings": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Warnings" } }, "additionalProperties": false, @@ -13658,22 +13858,6 @@ "title": "WorkflowStatus", "description": "Represents the status of a workflow. " }, - "src__lib__utils__config_history__ConfigHistoryType": { - "type": "string", - "enum": [ - "SERVICE", - "WORKFLOW", - "BACKEND", - "POOL", - "POD_TEMPLATE", - "GROUP_TEMPLATE", - "RESOURCE_VALIDATION", - "BACKEND_TEST", - "ROLE" - ], - "title": "ConfigHistoryType", - "description": "Type of configs supported by config history " - }, "src__service__core__app__objects__ListEntry": { "properties": { "uuid": { @@ -13884,6 +14068,13 @@ "priority": { "type": "string", "title": "Priority" + }, + "labels": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "title": "Labels" } }, "additionalProperties": false, diff --git a/src/ui/src/features/workflows/list/lib/actions.test.ts b/src/ui/src/features/workflows/list/lib/actions.test.ts index b0866ffd07..ddeca2dac0 100644 --- a/src/ui/src/features/workflows/list/lib/actions.test.ts +++ b/src/ui/src/features/workflows/list/lib/actions.test.ts @@ -27,8 +27,8 @@ vi.mock("@/lib/api/fetcher", () => ({ customFetch })); import { resubmitWorkflow } from "@/features/workflows/list/lib/actions"; -const WARN_MISSING_PPP_MESSAGE = - "Workflow is missing label 'PPP'; add it now to avoid rejected submissions once it is required."; +const WARN_MISSING_PROJECT_MESSAGE = + "Workflow is missing label 'project'; add it now to avoid rejected submissions once it is required."; describe("resubmit workflow labels", () => { beforeEach(() => { @@ -38,22 +38,22 @@ describe("resubmit workflow labels", () => { it("returns the raw submit name and warnings and sends repeated labels", async () => { customFetch.mockResolvedValue({ name: "workflow-copy-2", - warnings: [WARN_MISSING_PPP_MESSAGE], + warnings: [WARN_MISSING_PROJECT_MESSAGE], }); const result = await resubmitWorkflow({ workflowId: "workflow-1", poolName: "pool-a", priority: "NORMAL", - labels: ["PPP=robotics", "run=42"], + labels: ["project=robotics", "run=42"], }); const endpoint = new URL(customFetch.mock.calls[0][0], "https://osmo.invalid"); - expect(endpoint.searchParams.getAll("label")).toEqual(["PPP=robotics", "run=42"]); + expect(endpoint.searchParams.getAll("label")).toEqual(["project=robotics", "run=42"]); expect(result).toMatchObject({ success: true, newWorkflowName: "workflow-copy-2", - warnings: [WARN_MISSING_PPP_MESSAGE], + warnings: [WARN_MISSING_PROJECT_MESSAGE], }); }); }); diff --git a/src/ui/src/lib/api/generated.ts b/src/ui/src/lib/api/generated.ts index c7f457b8cb..051249514b 100644 --- a/src/ui/src/lib/api/generated.ts +++ b/src/ui/src/lib/api/generated.ts @@ -1,5 +1,5 @@ /** - * Generated by orval v8.5.3 🍺 + * Generated by orval v8.17.0 🍺 * Do not edit manually. * FastAPI * OpenAPI spec version: 0.1.0 @@ -235,10 +235,11 @@ export interface ConfigDiffResponse { /** * Type of configs supported by config history */ -export type SrcLibUtilsConfigHistoryConfigHistoryType = typeof SrcLibUtilsConfigHistoryConfigHistoryType[keyof typeof SrcLibUtilsConfigHistoryConfigHistoryType]; +export type ConfigHistoryType = typeof ConfigHistoryType[keyof typeof ConfigHistoryType]; -export const SrcLibUtilsConfigHistoryConfigHistoryType = { +export const ConfigHistoryType = { + DATASET: 'DATASET', SERVICE: 'SERVICE', WORKFLOW: 'WORKFLOW', BACKEND: 'BACKEND', @@ -254,7 +255,7 @@ export const SrcLibUtilsConfigHistoryConfigHistoryType = { * Object storing config history. */ export interface ConfigHistory { - config_type: SrcLibUtilsConfigHistoryConfigHistoryType; + config_type: ConfigHistoryType; name: string; revision: number; username: string; @@ -309,6 +310,18 @@ export interface UserRegistryCredential { auth: string; } +/** + * S3 request addressing style for S3-compatible providers. Use 'virtual' for providers such as CoreWeave CAIOS that reject path-style requests, or 'path' for localstack/MinIO-style endpoints. + */ +export type UserDataCredentialAddressingStyle = typeof UserDataCredentialAddressingStyle[keyof typeof UserDataCredentialAddressingStyle] | null; + + +export const UserDataCredentialAddressingStyle = { + virtual: 'virtual', + path: 'path', + auto: 'auto', +} as const; + /** * Authentication information for a data service. */ @@ -320,7 +333,7 @@ export interface UserDataCredential { /** HTTP service URL for S3-compatible providers (e.g., http://minio:9000, https://s3-compat.example.com). Leave unset for native AWS S3, GCS, Azure, etc. */ override_url?: string | null; /** S3 request addressing style for S3-compatible providers. Use 'virtual' for providers such as CoreWeave CAIOS that reject path-style requests, or 'path' for localstack/MinIO-style endpoints. */ - addressing_style?: 'virtual' | 'path' | 'auto' | null; + addressing_style?: UserDataCredentialAddressingStyle; /** The authentication key for a data backend */ access_key_id: string; /** The authentication secret for a data backend */ @@ -352,6 +365,18 @@ export interface CredentialOptions { generic_credential?: UserCredential | null; } +/** + * S3 request addressing style for S3-compatible providers. Use 'virtual' for providers such as CoreWeave CAIOS that reject path-style requests, or 'path' for localstack/MinIO-style endpoints. + */ +export type StaticDataCredentialAddressingStyle = typeof StaticDataCredentialAddressingStyle[keyof typeof StaticDataCredentialAddressingStyle] | null; + + +export const StaticDataCredentialAddressingStyle = { + virtual: 'virtual', + path: 'path', + auto: 'auto', +} as const; + /** * Static data credentials (i.e. credentials with access_key_id and access_key) for a data backend. */ @@ -363,7 +388,7 @@ export interface StaticDataCredential { /** HTTP service URL for S3-compatible providers (e.g., http://minio:9000, https://s3-compat.example.com). Leave unset for native AWS S3, GCS, Azure, etc. */ override_url?: string | null; /** S3 request addressing style for S3-compatible providers. Use 'virtual' for providers such as CoreWeave CAIOS that reject path-style requests, or 'path' for localstack/MinIO-style endpoints. */ - addressing_style?: 'virtual' | 'path' | 'auto' | null; + addressing_style?: StaticDataCredentialAddressingStyle; /** The authentication key for a data backend */ access_key_id: string; /** The encrypted authentication secret for a data backend */ @@ -371,13 +396,25 @@ export interface StaticDataCredential { } /** - * Data credential that delegates resolution to the underlying SDK. + * S3 request addressing style for S3-compatible providers. Use 'virtual' for providers such as CoreWeave CAIOS that reject path-style requests, or 'path' for localstack/MinIO-style endpoints. + */ +export type DefaultDataCredentialAddressingStyle = typeof DefaultDataCredentialAddressingStyle[keyof typeof DefaultDataCredentialAddressingStyle] | null; -Uses the SDK's default credential chain (e.g., Azure's DefaultAzureCredential, -boto3's credential resolution) which may include environment variables, -workload identity, instance metadata, and other provider-specific methods. -Intentionally left empty as all credential resolution is handled by the SDK. +export const DefaultDataCredentialAddressingStyle = { + virtual: 'virtual', + path: 'path', + auto: 'auto', +} as const; + +/** + * Data credential that delegates resolution to the underlying SDK. + * + * Uses the SDK's default credential chain (e.g., Azure's DefaultAzureCredential, + * boto3's credential resolution) which may include environment variables, + * workload identity, instance metadata, and other provider-specific methods. + * + * Intentionally left empty as all credential resolution is handled by the SDK. */ export interface DefaultDataCredential { /** The OSMO storage URI for the data service (e.g., s3://bucket). For S3-compatible services with HTTP endpoints, set 'override_url' separately rather than pasting the full HTTPS URL here. */ @@ -387,7 +424,7 @@ export interface DefaultDataCredential { /** HTTP service URL for S3-compatible providers (e.g., http://minio:9000, https://s3-compat.example.com). Leave unset for native AWS S3, GCS, Azure, etc. */ override_url?: string | null; /** S3 request addressing style for S3-compatible providers. Use 'virtual' for providers such as CoreWeave CAIOS that reject path-style requests, or 'path' for localstack/MinIO-style endpoints. */ - addressing_style?: 'virtual' | 'path' | 'auto' | null; + addressing_style?: DefaultDataCredentialAddressingStyle; } /** @@ -442,7 +479,7 @@ export const PoolStatus = { /** * Resources like GPU or CPU that have a discrete number. For guarantee and maximum, a value of -1 -indicates that there is no limit. + * indicates that there is no limit. */ export interface PoolResourceCountable { guarantee?: number; @@ -614,10 +651,14 @@ export interface GroupQueryResponse { tasks?: TaskQueryResponse[]; } +export type ValidationErrorCtx = { [key: string]: unknown }; + export interface ValidationError { loc: (string | number)[]; msg: string; type: string; + input?: unknown; + ctx?: ValidationErrorCtx; } export interface HTTPValidationError { @@ -633,6 +674,48 @@ export interface JwtTokenResponse { error?: string | null; } +/** + * Per-key policy strictness: 'off' skips checking, 'warn' surfaces + * missing or unlisted values as submission warnings, and 'enforce' + * rejects them. + */ +export type LabelEnforcement = typeof LabelEnforcement[keyof typeof LabelEnforcement]; + + +export const LabelEnforcement = { + off: 'off', + warn: 'warn', + enforce: 'enforce', +} as const; + +/** + * Configuration for one admin-designated workflow label key. + * + * An empty allow_list accepts any well-formed value; enforcement then + * applies only to the key being present. + */ +export interface LabelPolicy { + key: string; + allow_list?: string[]; + enforcement?: LabelEnforcement; +} + +/** + * Curated workflow label policy; empty by default, so no policy + * applies until configured. + */ +export interface LabelsConfigInput { + policy?: LabelPolicy[]; +} + +/** + * Curated workflow label policy; empty by default, so no policy + * applies until configured. + */ +export interface LabelsConfigOutput { + policy?: LabelPolicy[]; +} + /** * Object storing info for all backends. */ @@ -777,6 +860,24 @@ export interface NotificationConfig { smtp_settings?: SMTPConfig; } +/** + * Type of configs supported by config history mutations. + */ +export type OperableConfigHistoryType = typeof OperableConfigHistoryType[keyof typeof OperableConfigHistoryType]; + + +export const OperableConfigHistoryType = { + SERVICE: 'SERVICE', + WORKFLOW: 'WORKFLOW', + BACKEND: 'BACKEND', + POOL: 'POOL', + POD_TEMPLATE: 'POD_TEMPLATE', + GROUP_TEMPLATE: 'GROUP_TEMPLATE', + RESOURCE_VALIDATION: 'RESOURCE_VALIDATION', + BACKEND_TEST: 'BACKEND_TEST', + ROLE: 'ROLE', +} as const; + export type OperatorType = typeof OperatorType[keyof typeof OperatorType]; @@ -802,7 +903,7 @@ export interface RegistryCredential { /** * Dynamic Config for storing the image URLs for service images and the credentials needed -to pull them. + * to pull them. */ export interface OsmoImageConfig { init?: string; @@ -934,35 +1035,35 @@ export interface RsyncConfig { enabled?: boolean; enable_telemetry?: boolean; /** - * User pod's rsync read bandwidth limit in bytes per second, zero means no limit - * @minimum 0 - */ + * User pod's rsync read bandwidth limit in bytes per second, zero means no limit + * @minimum 0 + */ read_bandwidth_limit?: number; /** - * User pod's rsync write bandwidth limit in bytes per second, zero means no limit - * @minimum 0 - */ + * User pod's rsync write bandwidth limit in bytes per second, zero means no limit + * @minimum 0 + */ write_bandwidth_limit?: number; allowed_paths?: RsyncConfigAllowedPaths; /** - * Daemon debounce delay for rsync in seconds - * @exclusiveMinimum 0 - */ + * Daemon debounce delay for rsync in seconds + * @exclusiveMinimum 0 + */ daemon_debounce_delay?: number; /** - * Daemon poll interval for rsync in seconds - * @exclusiveMinimum 0 - */ + * Daemon poll interval for rsync in seconds + * @exclusiveMinimum 0 + */ daemon_poll_interval?: number; /** - * Daemon reconcile interval for rsync in seconds - * @exclusiveMinimum 0 - */ + * Daemon reconcile interval for rsync in seconds + * @exclusiveMinimum 0 + */ daemon_reconcile_interval?: number; /** - * Client upload rate limit for rsync in bytes per second, zero means no limit - * @minimum 0 - */ + * Client upload rate limit for rsync in bytes per second, zero means no limit + * @minimum 0 + */ client_upload_rate_limit?: number; } @@ -1180,7 +1281,7 @@ export interface TokenIdentity { /** * Profile and identity info. When token header is set, roles/pools are the -token's; otherwise they are the user's. JSON is self-explanatory for CLI. + * token's; otherwise they are the user's. JSON is self-explanatory for CLI. */ export interface ProfileResponse { profile: UserProfile; @@ -1308,13 +1409,13 @@ export interface PutResourceValidationsRequest { /** * Single Role Policy Entry. - -Contains a list of actions (semantic format "resource:Action") and optional -resources the policy applies to. If effect is Deny and the policy matches, -access is denied even if another policy allows it. - -Actions are validated via regex; API/DB still use [{"action": "..."}] for -compatibility with the Go authz_sidecar. + * + * Contains a list of actions (semantic format "resource:Action") and optional + * resources the policy applies to. If effect is Deny and the policy matches, + * access is denied even if another policy allows it. + * + * Actions are validated via regex; API/DB still use [{"action": "..."}] for + * compatibility with the Go authz_sidecar. */ export interface RolePolicy { effect?: PolicyEffect; @@ -1324,10 +1425,10 @@ export interface RolePolicy { /** * Sync mode for role assignments. - -- FORCE: Always apply this role to all users (e.g., for system roles) -- IMPORT: Role is imported from IDP claims or user_roles table (default) -- IGNORE: Ignore this role in IDP sync (role is managed manually) + * + * - FORCE: Always apply this role to all users (e.g., for system roles) + * - IMPORT: Role is imported from IDP claims or user_roles table (default) + * - IGNORE: Ignore this role in IDP sync (role is managed manually) */ export type SyncMode = typeof SyncMode[keyof typeof SyncMode]; @@ -1340,9 +1441,9 @@ export const SyncMode = { /** * Single Role Entry. - -Note: Authorization checking is now handled by the authz_sidecar (Go service). -This Python class is only used for role CRUD operations. + * + * Note: Authorization checking is now handled by the authz_sidecar (Go service). + * This Python class is only used for role CRUD operations. */ export interface RoleInput { name: string; @@ -1401,7 +1502,7 @@ export interface WorkflowInfo { /** * Stores workflow limits per user. Default is None, which means no limit. -If a limit is set, it must be greater than 0. + * If a limit is set, it must be greater than 0. */ export interface UserWorkflowLimitConfig { max_num_workflows?: number | null; @@ -1424,6 +1525,7 @@ export interface WorkflowConfigInput { credential_config?: CredentialConfig; user_workflow_limits?: UserWorkflowLimitConfig; plugins_config?: PluginsConfigInput; + labels_config?: LabelsConfigInput; max_num_tasks?: number; max_num_ports_per_task?: number; max_retry_per_task?: number; @@ -1520,9 +1622,9 @@ export interface ResourcesResponse { /** * Single Role Entry. - -Note: Authorization checking is now handled by the authz_sidecar (Go service). -This Python class is only used for role CRUD operations. + * + * Note: Authorization checking is now handled by the authz_sidecar (Go service). + * This Python class is only used for role CRUD operations. */ export interface RoleOutput { name: string; @@ -1549,11 +1651,11 @@ export interface RoleUsersResponse { export interface RollbackConfigRequest { description?: string | null; tags?: string[] | null; - config_type: SrcLibUtilsConfigHistoryConfigHistoryType; + config_type: OperableConfigHistoryType; /** - * Revision to roll back to - * @exclusiveMinimum 0 - */ + * Revision to roll back to + * @exclusiveMinimum 0 + */ revision: number; } @@ -1586,6 +1688,7 @@ export interface SubmitResponse { logs?: string | null; spec?: string | null; dashboard_url?: string | null; + warnings?: string[]; } /** @@ -1719,6 +1822,7 @@ export interface WorkflowConfigOutput { credential_config?: CredentialConfig; user_workflow_limits?: UserWorkflowLimitConfig; plugins_config?: PluginsConfigOutput; + labels_config?: LabelsConfigOutput; max_num_tasks?: number; max_num_ports_per_task?: number; max_retry_per_task?: number; @@ -1755,6 +1859,8 @@ export const WorkflowPriority = { LOW: 'LOW', } as const; +export type WorkflowQueryResponseLabels = {[key: string]: string}; + /** * Represents the status of a workflow. */ @@ -1815,6 +1921,8 @@ export interface WorkflowQueryResponse { app_version?: number | null; plugins: WorkflowPlugins; priority: string; + labels?: WorkflowQueryResponseLabels; + warnings?: string[]; } export interface SrcServiceCoreAppObjectsListEntry { @@ -1831,6 +1939,8 @@ export interface SrcServiceCoreAppObjectsListResponse { more_entries: boolean; } +export type SrcServiceCoreWorkflowObjectsListEntryLabels = {[key: string]: string}; + /** * Entry for list API results. */ @@ -1854,6 +1964,7 @@ export interface SrcServiceCoreWorkflowObjectsListEntry { app_name?: string | null; app_version?: number | null; priority: string; + labels?: SrcServiceCoreWorkflowObjectsListEntryLabels; } export interface SrcServiceCoreWorkflowObjectsListResponse { @@ -1916,7 +2027,7 @@ order?: ListOrder; /** * Filter by config types */ -config_types?: SrcLibUtilsConfigHistoryConfigHistoryType[] | null; +config_types?: ConfigHistoryType[] | null; /** * Filter by config name */ @@ -1948,7 +2059,7 @@ omit_data?: boolean; }; export type GetConfigDiffApiConfigsDiffGetParams = { -config_type: SrcLibUtilsConfigHistoryConfigHistoryType; +config_type: ConfigHistoryType; /** * First revision to compare * @exclusiveMinimum 0 @@ -2049,6 +2160,14 @@ submitted_after?: string | null; tags?: string[] | null; app?: string | null; priority?: WorkflowPriority[] | null; +/** + * Workflow label selector: key=value with optional * wildcards and (a|b) alternatives, for example key=(team_*|osmo_*) or key=team_(a|b). Repeat for AND semantics. + */ +label?: string[] | null; +/** + * Label key that must be absent from the workflow; workflows without any labels match. Repeat for AND semantics. + */ +no_label?: string[] | null; }; export type ListTaskApiTaskGetParams = { @@ -2146,6 +2265,7 @@ dry_run?: boolean; validation_only?: boolean; priority?: WorkflowPriority; env_vars?: string[]; +label?: string[]; }; export type SetNotificationSettingsApiProfileSettingsPostParams = { @@ -2160,10 +2280,6 @@ type SecondParameter unknown> = Parameters[1]; -/** - * Read all the service configurations - * @summary Read Service Configs - */ export const getReadServiceConfigsApiConfigsServiceGetUrl = () => { @@ -2172,6 +2288,10 @@ export const getReadServiceConfigsApiConfigsServiceGetUrl = () => { return `/api/configs/service` } +/** + * Read all the service configurations + * @summary Read Service Configs + */ export const readServiceConfigsApiConfigsServiceGet = async ( options?: RequestInit): Promise => { return customFetch(getReadServiceConfigsApiConfigsServiceGetUrl(), @@ -2271,10 +2391,8 @@ export const invalidateReadServiceConfigsApiConfigsServiceGet = async ( -/** - * Put service configurations - * @summary Put Service Configs - */ + + export const getPutServiceConfigsApiConfigsServicePutUrl = () => { @@ -2283,6 +2401,10 @@ export const getPutServiceConfigsApiConfigsServicePutUrl = () => { return `/api/configs/service` } +/** + * Put service configurations + * @summary Put Service Configs + */ export const putServiceConfigsApiConfigsServicePut = async (putServiceRequest: PutServiceRequest, options?: RequestInit): Promise => { return customFetch(getPutServiceConfigsApiConfigsServicePutUrl(), @@ -2290,8 +2412,7 @@ export const putServiceConfigsApiConfigsServicePut = async (putServiceRequest: P ...options, method: 'PUT', headers: { 'Content-Type': 'application/json', ...options?.headers }, - body: JSON.stringify( - putServiceRequest,) + body: JSON.stringify(putServiceRequest) } );} @@ -2343,10 +2464,6 @@ export const usePutServiceConfigsApiConfigsServicePut = { @@ -2355,6 +2472,10 @@ export const getPatchServiceConfigsApiConfigsServicePatchUrl = () => { return `/api/configs/service` } +/** + * Patch service configurations + * @summary Patch Service Configs + */ export const patchServiceConfigsApiConfigsServicePatch = async (patchConfigRequest: PatchConfigRequest, options?: RequestInit): Promise => { return customFetch(getPatchServiceConfigsApiConfigsServicePatchUrl(), @@ -2362,8 +2483,7 @@ export const patchServiceConfigsApiConfigsServicePatch = async (patchConfigReque ...options, method: 'PATCH', headers: { 'Content-Type': 'application/json', ...options?.headers }, - body: JSON.stringify( - patchConfigRequest,) + body: JSON.stringify(patchConfigRequest) } );} @@ -2415,10 +2535,6 @@ export const usePatchServiceConfigsApiConfigsServicePatch = { @@ -2427,6 +2543,10 @@ export const getReadWorkflowConfigsApiConfigsWorkflowGetUrl = () => { return `/api/configs/workflow` } +/** + * Read all the workflow configurations + * @summary Read Workflow Configs + */ export const readWorkflowConfigsApiConfigsWorkflowGet = async ( options?: RequestInit): Promise => { return customFetch(getReadWorkflowConfigsApiConfigsWorkflowGetUrl(), @@ -2526,10 +2646,8 @@ export const invalidateReadWorkflowConfigsApiConfigsWorkflowGet = async ( -/** - * Put workflow configurations - * @summary Put Workflow Configs - */ + + export const getPutWorkflowConfigsApiConfigsWorkflowPutUrl = () => { @@ -2538,6 +2656,10 @@ export const getPutWorkflowConfigsApiConfigsWorkflowPutUrl = () => { return `/api/configs/workflow` } +/** + * Put workflow configurations + * @summary Put Workflow Configs + */ export const putWorkflowConfigsApiConfigsWorkflowPut = async (putWorkflowRequest: PutWorkflowRequest, options?: RequestInit): Promise => { return customFetch(getPutWorkflowConfigsApiConfigsWorkflowPutUrl(), @@ -2545,8 +2667,7 @@ export const putWorkflowConfigsApiConfigsWorkflowPut = async (putWorkflowRequest ...options, method: 'PUT', headers: { 'Content-Type': 'application/json', ...options?.headers }, - body: JSON.stringify( - putWorkflowRequest,) + body: JSON.stringify(putWorkflowRequest) } );} @@ -2598,10 +2719,6 @@ export const usePutWorkflowConfigsApiConfigsWorkflowPut = { @@ -2610,6 +2727,10 @@ export const getPatchWorkflowConfigsApiConfigsWorkflowPatchUrl = () => { return `/api/configs/workflow` } +/** + * Patch workflow configurations + * @summary Patch Workflow Configs + */ export const patchWorkflowConfigsApiConfigsWorkflowPatch = async (patchConfigRequest: PatchConfigRequest, options?: RequestInit): Promise => { return customFetch(getPatchWorkflowConfigsApiConfigsWorkflowPatchUrl(), @@ -2617,8 +2738,7 @@ export const patchWorkflowConfigsApiConfigsWorkflowPatch = async (patchConfigReq ...options, method: 'PATCH', headers: { 'Content-Type': 'application/json', ...options?.headers }, - body: JSON.stringify( - patchConfigRequest,) + body: JSON.stringify(patchConfigRequest) } );} @@ -2670,10 +2790,6 @@ export const usePatchWorkflowConfigsApiConfigsWorkflowPatch = { @@ -2682,6 +2798,10 @@ export const getListBackendsApiConfigsBackendGetUrl = () => { return `/api/configs/backend` } +/** + * List all backends. + * @summary List Backends + */ export const listBackendsApiConfigsBackendGet = async ( options?: RequestInit): Promise => { return customFetch(getListBackendsApiConfigsBackendGetUrl(), @@ -2781,10 +2901,8 @@ export const invalidateListBackendsApiConfigsBackendGet = async ( -/** - * Override the config for a specific backend. - * @summary Update Backend - */ + + export const getUpdateBackendApiConfigsBackendNamePostUrl = (name: string,) => { @@ -2793,6 +2911,10 @@ export const getUpdateBackendApiConfigsBackendNamePostUrl = (name: string,) => { return `/api/configs/backend/${name}` } +/** + * Override the config for a specific backend. + * @summary Update Backend + */ export const updateBackendApiConfigsBackendNamePost = async (name: string, postBackendRequest: PostBackendRequest, options?: RequestInit): Promise => { @@ -2801,8 +2923,7 @@ export const updateBackendApiConfigsBackendNamePost = async (name: string, ...options, method: 'POST', headers: { 'Content-Type': 'application/json', ...options?.headers }, - body: JSON.stringify( - postBackendRequest,) + body: JSON.stringify(postBackendRequest) } );} @@ -2854,10 +2975,6 @@ export const useUpdateBackendApiConfigsBackendNamePost = { @@ -2866,6 +2983,10 @@ export const getGetBackendApiConfigsBackendNameGetUrl = (name: string,) => { return `/api/configs/backend/${name}` } +/** + * Get info for a specific backend. + * @summary Get Backend + */ export const getBackendApiConfigsBackendNameGet = async (name: string, options?: RequestInit): Promise => { return customFetch(getGetBackendApiConfigsBackendNameGetUrl(name), @@ -2903,7 +3024,7 @@ const {query: queryOptions, request: requestOptions} = options ?? {}; - return { queryKey, queryFn, enabled: !!(name), ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } + return { queryKey, queryFn, enabled: name !== null && name !== undefined, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } } export type GetBackendApiConfigsBackendNameGetQueryResult = NonNullable>> @@ -2965,10 +3086,8 @@ export const invalidateGetBackendApiConfigsBackendNameGet = async ( -/** - * Remove a backend. - * @summary Delete Backend - */ + + export const getDeleteBackendApiConfigsBackendNameDeleteUrl = (name: string,) => { @@ -2977,6 +3096,10 @@ export const getDeleteBackendApiConfigsBackendNameDeleteUrl = (name: string,) => return `/api/configs/backend/${name}` } +/** + * Remove a backend. + * @summary Delete Backend + */ export const deleteBackendApiConfigsBackendNameDelete = async (name: string, deleteBackendRequest: DeleteBackendRequest, options?: RequestInit): Promise => { @@ -2985,8 +3108,7 @@ export const deleteBackendApiConfigsBackendNameDelete = async (name: string, ...options, method: 'DELETE', headers: { 'Content-Type': 'application/json', ...options?.headers }, - body: JSON.stringify( - deleteBackendRequest,) + body: JSON.stringify(deleteBackendRequest) } );} @@ -3038,17 +3160,13 @@ export const useDeleteBackendApiConfigsBackendNameDelete = { const normalizedParams = new URLSearchParams(); Object.entries(params || {}).forEach(([key, value]) => { if (value !== undefined) { - normalizedParams.append(key, value === null ? 'null' : value.toString()) + normalizedParams.append(key, value === null ? 'null' : String(value)) } }); @@ -3057,6 +3175,10 @@ export const getListPoolsApiConfigsPoolGetUrl = (params?: ListPoolsApiConfigsPoo return stringifiedParams.length > 0 ? `/api/configs/pool?${stringifiedParams}` : `/api/configs/pool` } +/** + * List all Pools + * @summary List Pools + */ export const listPoolsApiConfigsPoolGet = async (params?: ListPoolsApiConfigsPoolGetParams, options?: RequestInit): Promise => { return customFetch(getListPoolsApiConfigsPoolGetUrl(params), @@ -3156,10 +3278,8 @@ export const invalidateListPoolsApiConfigsPoolGet = async ( -/** - * Put Pool configurations - * @summary Put Pools - */ + + export const getPutPoolsApiConfigsPoolPutUrl = () => { @@ -3168,6 +3288,10 @@ export const getPutPoolsApiConfigsPoolPutUrl = () => { return `/api/configs/pool` } +/** + * Put Pool configurations + * @summary Put Pools + */ export const putPoolsApiConfigsPoolPut = async (putPoolsRequest: PutPoolsRequest, options?: RequestInit): Promise => { return customFetch(getPutPoolsApiConfigsPoolPutUrl(), @@ -3175,8 +3299,7 @@ export const putPoolsApiConfigsPoolPut = async (putPoolsRequest: PutPoolsRequest ...options, method: 'PUT', headers: { 'Content-Type': 'application/json', ...options?.headers }, - body: JSON.stringify( - putPoolsRequest,) + body: JSON.stringify(putPoolsRequest) } );} @@ -3228,13 +3351,6 @@ export const usePutPoolsApiConfigsPoolPut = { const normalizedParams = new URLSearchParams(); @@ -3242,7 +3358,7 @@ export const getReadPoolApiConfigsPoolNameGetUrl = (name: string, Object.entries(params || {}).forEach(([key, value]) => { if (value !== undefined) { - normalizedParams.append(key, value === null ? 'null' : value.toString()) + normalizedParams.append(key, value === null ? 'null' : String(value)) } }); @@ -3251,6 +3367,13 @@ export const getReadPoolApiConfigsPoolNameGetUrl = (name: string, return stringifiedParams.length > 0 ? `/api/configs/pool/${name}?${stringifiedParams}` : `/api/configs/pool/${name}` } +/** + * Read Pool configuration + * + * Return type Any to prevent unwanted artifacts between Pool and PoolEditable outputs + * Should return Pool or PoolEditable objects + * @summary Read Pool + */ export const readPoolApiConfigsPoolNameGet = async (name: string, params?: ReadPoolApiConfigsPoolNameGetParams, options?: RequestInit): Promise => { @@ -3291,7 +3414,7 @@ const {query: queryOptions, request: requestOptions} = options ?? {}; - return { queryKey, queryFn, enabled: !!(name), ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } + return { queryKey, queryFn, enabled: name !== null && name !== undefined, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } } export type ReadPoolApiConfigsPoolNameGetQueryResult = NonNullable>> @@ -3358,10 +3481,8 @@ export const invalidateReadPoolApiConfigsPoolNameGet = async ( -/** - * Put Pool configurations - * @summary Put Pool - */ + + export const getPutPoolApiConfigsPoolNamePutUrl = (name: string,) => { @@ -3370,6 +3491,10 @@ export const getPutPoolApiConfigsPoolNamePutUrl = (name: string,) => { return `/api/configs/pool/${name}` } +/** + * Put Pool configurations + * @summary Put Pool + */ export const putPoolApiConfigsPoolNamePut = async (name: string, putPoolRequest: PutPoolRequest, options?: RequestInit): Promise => { @@ -3378,8 +3503,7 @@ export const putPoolApiConfigsPoolNamePut = async (name: string, ...options, method: 'PUT', headers: { 'Content-Type': 'application/json', ...options?.headers }, - body: JSON.stringify( - putPoolRequest,) + body: JSON.stringify(putPoolRequest) } );} @@ -3431,10 +3555,6 @@ export const usePutPoolApiConfigsPoolNamePut = { @@ -3443,6 +3563,10 @@ export const getPatchPoolApiConfigsPoolNamePatchUrl = (name: string,) => { return `/api/configs/pool/${name}` } +/** + * Patch Pool configurations + * @summary Patch Pool + */ export const patchPoolApiConfigsPoolNamePatch = async (name: string, patchPoolRequest: PatchPoolRequest, options?: RequestInit): Promise => { @@ -3451,8 +3575,7 @@ export const patchPoolApiConfigsPoolNamePatch = async (name: string, ...options, method: 'PATCH', headers: { 'Content-Type': 'application/json', ...options?.headers }, - body: JSON.stringify( - patchPoolRequest,) + body: JSON.stringify(patchPoolRequest) } );} @@ -3504,10 +3627,6 @@ export const usePatchPoolApiConfigsPoolNamePatch = { @@ -3516,6 +3635,10 @@ export const getDeletePoolApiConfigsPoolNameDeleteUrl = (name: string,) => { return `/api/configs/pool/${name}` } +/** + * Delete Pool configurations + * @summary Delete Pool + */ export const deletePoolApiConfigsPoolNameDelete = async (name: string, configsRequest: ConfigsRequest, options?: RequestInit): Promise => { @@ -3524,8 +3647,7 @@ export const deletePoolApiConfigsPoolNameDelete = async (name: string, ...options, method: 'DELETE', headers: { 'Content-Type': 'application/json', ...options?.headers }, - body: JSON.stringify( - configsRequest,) + body: JSON.stringify(configsRequest) } );} @@ -3577,10 +3699,6 @@ export const useDeletePoolApiConfigsPoolNameDelete = { @@ -3589,6 +3707,10 @@ export const getRenamePoolApiConfigsPoolNameRenamePutUrl = (name: string,) => { return `/api/configs/pool/${name}/rename` } +/** + * Rename Pool + * @summary Rename Pool + */ export const renamePoolApiConfigsPoolNameRenamePut = async (name: string, renamePoolRequest: RenamePoolRequest, options?: RequestInit): Promise => { @@ -3597,8 +3719,7 @@ export const renamePoolApiConfigsPoolNameRenamePut = async (name: string, ...options, method: 'PUT', headers: { 'Content-Type': 'application/json', ...options?.headers }, - body: JSON.stringify( - renamePoolRequest,) + body: JSON.stringify(renamePoolRequest) } );} @@ -3650,10 +3771,6 @@ export const useRenamePoolApiConfigsPoolNameRenamePut = { const normalizedParams = new URLSearchParams(); @@ -3661,7 +3778,7 @@ export const getListPlatformsInPoolApiConfigsPoolNamePlatformGetUrl = (name: str Object.entries(params || {}).forEach(([key, value]) => { if (value !== undefined) { - normalizedParams.append(key, value === null ? 'null' : value.toString()) + normalizedParams.append(key, value === null ? 'null' : String(value)) } }); @@ -3670,6 +3787,10 @@ export const getListPlatformsInPoolApiConfigsPoolNamePlatformGetUrl = (name: str return stringifiedParams.length > 0 ? `/api/configs/pool/${name}/platform?${stringifiedParams}` : `/api/configs/pool/${name}/platform` } +/** + * List all Platforms + * @summary List Platforms In Pool + */ export const listPlatformsInPoolApiConfigsPoolNamePlatformGet = async (name: string, params?: ListPlatformsInPoolApiConfigsPoolNamePlatformGetParams, options?: RequestInit): Promise => { @@ -3710,7 +3831,7 @@ const {query: queryOptions, request: requestOptions} = options ?? {}; - return { queryKey, queryFn, enabled: !!(name), ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } + return { queryKey, queryFn, enabled: name !== null && name !== undefined, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } } export type ListPlatformsInPoolApiConfigsPoolNamePlatformGetQueryResult = NonNullable>> @@ -3777,10 +3898,8 @@ export const invalidateListPlatformsInPoolApiConfigsPoolNamePlatformGet = async -/** - * Read Platform - * @summary Read Platform In Pool - */ + + export const getReadPlatformInPoolApiConfigsPoolNamePlatformPlatformNameGetUrl = (name: string, platformName: string, params?: ReadPlatformInPoolApiConfigsPoolNamePlatformPlatformNameGetParams,) => { @@ -3789,7 +3908,7 @@ export const getReadPlatformInPoolApiConfigsPoolNamePlatformPlatformNameGetUrl = Object.entries(params || {}).forEach(([key, value]) => { if (value !== undefined) { - normalizedParams.append(key, value === null ? 'null' : value.toString()) + normalizedParams.append(key, value === null ? 'null' : String(value)) } }); @@ -3798,6 +3917,10 @@ export const getReadPlatformInPoolApiConfigsPoolNamePlatformPlatformNameGetUrl = return stringifiedParams.length > 0 ? `/api/configs/pool/${name}/platform/${platformName}?${stringifiedParams}` : `/api/configs/pool/${name}/platform/${platformName}` } +/** + * Read Platform + * @summary Read Platform In Pool + */ export const readPlatformInPoolApiConfigsPoolNamePlatformPlatformNameGet = async (name: string, platformName: string, params?: ReadPlatformInPoolApiConfigsPoolNamePlatformPlatformNameGetParams, options?: RequestInit): Promise => { @@ -3841,7 +3964,7 @@ const {query: queryOptions, request: requestOptions} = options ?? {}; - return { queryKey, queryFn, enabled: !!(name && platformName), ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } + return { queryKey, queryFn, enabled: name !== null && name !== undefined && platformName !== null && platformName !== undefined, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } } export type ReadPlatformInPoolApiConfigsPoolNamePlatformPlatformNameGetQueryResult = NonNullable>> @@ -3913,10 +4036,8 @@ export const invalidateReadPlatformInPoolApiConfigsPoolNamePlatformPlatformNameG -/** - * Put Platform configurations - * @summary Put Platform In Pool - */ + + export const getPutPlatformInPoolApiConfigsPoolNamePlatformPlatformNamePutUrl = (name: string, platformName: string,) => { @@ -3926,6 +4047,10 @@ export const getPutPlatformInPoolApiConfigsPoolNamePlatformPlatformNamePutUrl = return `/api/configs/pool/${name}/platform/${platformName}` } +/** + * Put Platform configurations + * @summary Put Platform In Pool + */ export const putPlatformInPoolApiConfigsPoolNamePlatformPlatformNamePut = async (name: string, platformName: string, putPoolPlatformRequest: PutPoolPlatformRequest, options?: RequestInit): Promise => { @@ -3935,8 +4060,7 @@ export const putPlatformInPoolApiConfigsPoolNamePlatformPlatformNamePut = async ...options, method: 'PUT', headers: { 'Content-Type': 'application/json', ...options?.headers }, - body: JSON.stringify( - putPoolPlatformRequest,) + body: JSON.stringify(putPoolPlatformRequest) } );} @@ -3988,10 +4112,6 @@ export const usePutPlatformInPoolApiConfigsPoolNamePlatformPlatformNamePut = { @@ -4001,6 +4121,10 @@ export const getRenamePlatformInPoolApiConfigsPoolNamePlatformPlatformNameRename return `/api/configs/pool/${name}/platform/${platformName}/rename` } +/** + * Rename Platform + * @summary Rename Platform In Pool + */ export const renamePlatformInPoolApiConfigsPoolNamePlatformPlatformNameRenamePut = async (name: string, platformName: string, renamePoolPlatformRequest: RenamePoolPlatformRequest, options?: RequestInit): Promise => { @@ -4010,8 +4134,7 @@ export const renamePlatformInPoolApiConfigsPoolNamePlatformPlatformNameRenamePut ...options, method: 'PUT', headers: { 'Content-Type': 'application/json', ...options?.headers }, - body: JSON.stringify( - renamePoolPlatformRequest,) + body: JSON.stringify(renamePoolPlatformRequest) } );} @@ -4063,10 +4186,6 @@ export const useRenamePlatformInPoolApiConfigsPoolNamePlatformPlatformNameRename return useMutation(getRenamePlatformInPoolApiConfigsPoolNamePlatformPlatformNameRenamePutMutationOptions(options), queryClient); } -/** - * List all Pod Template configurations - * @summary List Pod Templates - */ export const getListPodTemplatesApiConfigsPodTemplateGetUrl = () => { @@ -4075,9 +4194,13 @@ export const getListPodTemplatesApiConfigsPodTemplateGetUrl = () => { return `/api/configs/pod_template` } -export const listPodTemplatesApiConfigsPodTemplateGet = async ( options?: RequestInit): Promise => { - - return customFetch(getListPodTemplatesApiConfigsPodTemplateGetUrl(), +/** + * List all Pod Template configurations + * @summary List Pod Templates + */ +export const listPodTemplatesApiConfigsPodTemplateGet = async ( options?: RequestInit): Promise => { + + return customFetch(getListPodTemplatesApiConfigsPodTemplateGetUrl(), { ...options, method: 'GET' @@ -4174,10 +4297,8 @@ export const invalidateListPodTemplatesApiConfigsPodTemplateGet = async ( -/** - * Set Dict of Pod Templates configurations - * @summary Put Pod Templates - */ + + export const getPutPodTemplatesApiConfigsPodTemplatePutUrl = () => { @@ -4186,6 +4307,10 @@ export const getPutPodTemplatesApiConfigsPodTemplatePutUrl = () => { return `/api/configs/pod_template` } +/** + * Set Dict of Pod Templates configurations + * @summary Put Pod Templates + */ export const putPodTemplatesApiConfigsPodTemplatePut = async (putPodTemplatesRequest: PutPodTemplatesRequest, options?: RequestInit): Promise => { return customFetch(getPutPodTemplatesApiConfigsPodTemplatePutUrl(), @@ -4193,8 +4318,7 @@ export const putPodTemplatesApiConfigsPodTemplatePut = async (putPodTemplatesReq ...options, method: 'PUT', headers: { 'Content-Type': 'application/json', ...options?.headers }, - body: JSON.stringify( - putPodTemplatesRequest,) + body: JSON.stringify(putPodTemplatesRequest) } );} @@ -4246,10 +4370,6 @@ export const usePutPodTemplatesApiConfigsPodTemplatePut = { @@ -4258,6 +4378,10 @@ export const getReadPodTemplateApiConfigsPodTemplateNameGetUrl = (name: string,) return `/api/configs/pod_template/${name}` } +/** + * Read Pod Template configurations + * @summary Read Pod Template + */ export const readPodTemplateApiConfigsPodTemplateNameGet = async (name: string, options?: RequestInit): Promise => { return customFetch(getReadPodTemplateApiConfigsPodTemplateNameGetUrl(name), @@ -4295,7 +4419,7 @@ const {query: queryOptions, request: requestOptions} = options ?? {}; - return { queryKey, queryFn, enabled: !!(name), ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } + return { queryKey, queryFn, enabled: name !== null && name !== undefined, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } } export type ReadPodTemplateApiConfigsPodTemplateNameGetQueryResult = NonNullable>> @@ -4357,10 +4481,8 @@ export const invalidateReadPodTemplateApiConfigsPodTemplateNameGet = async ( -/** - * Put Pod Template configurations - * @summary Put Pod Template - */ + + export const getPutPodTemplateApiConfigsPodTemplateNamePutUrl = (name: string,) => { @@ -4369,6 +4491,10 @@ export const getPutPodTemplateApiConfigsPodTemplateNamePutUrl = (name: string,) return `/api/configs/pod_template/${name}` } +/** + * Put Pod Template configurations + * @summary Put Pod Template + */ export const putPodTemplateApiConfigsPodTemplateNamePut = async (name: string, putPodTemplateRequest: PutPodTemplateRequest, options?: RequestInit): Promise => { @@ -4377,8 +4503,7 @@ export const putPodTemplateApiConfigsPodTemplateNamePut = async (name: string, ...options, method: 'PUT', headers: { 'Content-Type': 'application/json', ...options?.headers }, - body: JSON.stringify( - putPodTemplateRequest,) + body: JSON.stringify(putPodTemplateRequest) } );} @@ -4430,10 +4555,6 @@ export const usePutPodTemplateApiConfigsPodTemplateNamePut = { @@ -4442,6 +4563,10 @@ export const getDeletePodTemplateApiConfigsPodTemplateNameDeleteUrl = (name: str return `/api/configs/pod_template/${name}` } +/** + * Delete Pod Template configurations + * @summary Delete Pod Template + */ export const deletePodTemplateApiConfigsPodTemplateNameDelete = async (name: string, configsRequest: ConfigsRequest, options?: RequestInit): Promise => { @@ -4450,8 +4575,7 @@ export const deletePodTemplateApiConfigsPodTemplateNameDelete = async (name: str ...options, method: 'DELETE', headers: { 'Content-Type': 'application/json', ...options?.headers }, - body: JSON.stringify( - configsRequest,) + body: JSON.stringify(configsRequest) } );} @@ -4503,10 +4627,6 @@ export const useDeletePodTemplateApiConfigsPodTemplateNameDelete = { @@ -4515,6 +4635,10 @@ export const getListGroupTemplatesApiConfigsGroupTemplateGetUrl = () => { return `/api/configs/group_template` } +/** + * List all Group Template configurations + * @summary List Group Templates + */ export const listGroupTemplatesApiConfigsGroupTemplateGet = async ( options?: RequestInit): Promise => { return customFetch(getListGroupTemplatesApiConfigsGroupTemplateGetUrl(), @@ -4614,10 +4738,8 @@ export const invalidateListGroupTemplatesApiConfigsGroupTemplateGet = async ( -/** - * Set Dict of Group Templates configurations - * @summary Put Group Templates - */ + + export const getPutGroupTemplatesApiConfigsGroupTemplatePutUrl = () => { @@ -4626,6 +4748,10 @@ export const getPutGroupTemplatesApiConfigsGroupTemplatePutUrl = () => { return `/api/configs/group_template` } +/** + * Set Dict of Group Templates configurations + * @summary Put Group Templates + */ export const putGroupTemplatesApiConfigsGroupTemplatePut = async (putGroupTemplatesRequest: PutGroupTemplatesRequest, options?: RequestInit): Promise => { return customFetch(getPutGroupTemplatesApiConfigsGroupTemplatePutUrl(), @@ -4633,8 +4759,7 @@ export const putGroupTemplatesApiConfigsGroupTemplatePut = async (putGroupTempla ...options, method: 'PUT', headers: { 'Content-Type': 'application/json', ...options?.headers }, - body: JSON.stringify( - putGroupTemplatesRequest,) + body: JSON.stringify(putGroupTemplatesRequest) } );} @@ -4686,10 +4811,6 @@ export const usePutGroupTemplatesApiConfigsGroupTemplatePut = { @@ -4698,6 +4819,10 @@ export const getReadGroupTemplateApiConfigsGroupTemplateNameGetUrl = (name: stri return `/api/configs/group_template/${name}` } +/** + * Read Group Template configurations + * @summary Read Group Template + */ export const readGroupTemplateApiConfigsGroupTemplateNameGet = async (name: string, options?: RequestInit): Promise => { return customFetch(getReadGroupTemplateApiConfigsGroupTemplateNameGetUrl(name), @@ -4735,7 +4860,7 @@ const {query: queryOptions, request: requestOptions} = options ?? {}; - return { queryKey, queryFn, enabled: !!(name), ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } + return { queryKey, queryFn, enabled: name !== null && name !== undefined, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } } export type ReadGroupTemplateApiConfigsGroupTemplateNameGetQueryResult = NonNullable>> @@ -4797,10 +4922,8 @@ export const invalidateReadGroupTemplateApiConfigsGroupTemplateNameGet = async ( -/** - * Put Group Template configurations - * @summary Put Group Template - */ + + export const getPutGroupTemplateApiConfigsGroupTemplateNamePutUrl = (name: string,) => { @@ -4809,6 +4932,10 @@ export const getPutGroupTemplateApiConfigsGroupTemplateNamePutUrl = (name: strin return `/api/configs/group_template/${name}` } +/** + * Put Group Template configurations + * @summary Put Group Template + */ export const putGroupTemplateApiConfigsGroupTemplateNamePut = async (name: string, putGroupTemplateRequest: PutGroupTemplateRequest, options?: RequestInit): Promise => { @@ -4817,8 +4944,7 @@ export const putGroupTemplateApiConfigsGroupTemplateNamePut = async (name: strin ...options, method: 'PUT', headers: { 'Content-Type': 'application/json', ...options?.headers }, - body: JSON.stringify( - putGroupTemplateRequest,) + body: JSON.stringify(putGroupTemplateRequest) } );} @@ -4870,10 +4996,6 @@ export const usePutGroupTemplateApiConfigsGroupTemplateNamePut = { @@ -4882,6 +5004,10 @@ export const getDeleteGroupTemplateApiConfigsGroupTemplateNameDeleteUrl = (name: return `/api/configs/group_template/${name}` } +/** + * Delete Group Template configurations + * @summary Delete Group Template + */ export const deleteGroupTemplateApiConfigsGroupTemplateNameDelete = async (name: string, configsRequest: ConfigsRequest, options?: RequestInit): Promise => { @@ -4890,8 +5016,7 @@ export const deleteGroupTemplateApiConfigsGroupTemplateNameDelete = async (name: ...options, method: 'DELETE', headers: { 'Content-Type': 'application/json', ...options?.headers }, - body: JSON.stringify( - configsRequest,) + body: JSON.stringify(configsRequest) } );} @@ -4943,10 +5068,6 @@ export const useDeleteGroupTemplateApiConfigsGroupTemplateNameDelete = { @@ -4955,6 +5076,10 @@ export const getListResourceValidationsApiConfigsResourceValidationGetUrl = () = return `/api/configs/resource_validation` } +/** + * List all Resource Validation configurations + * @summary List Resource Validations + */ export const listResourceValidationsApiConfigsResourceValidationGet = async ( options?: RequestInit): Promise => { return customFetch(getListResourceValidationsApiConfigsResourceValidationGetUrl(), @@ -5054,10 +5179,8 @@ export const invalidateListResourceValidationsApiConfigsResourceValidationGet = -/** - * Put Resource Validation configurations - * @summary Put Resource Validations - */ + + export const getPutResourceValidationsApiConfigsResourceValidationPutUrl = () => { @@ -5066,6 +5189,10 @@ export const getPutResourceValidationsApiConfigsResourceValidationPutUrl = () => return `/api/configs/resource_validation` } +/** + * Put Resource Validation configurations + * @summary Put Resource Validations + */ export const putResourceValidationsApiConfigsResourceValidationPut = async (putResourceValidationsRequest: PutResourceValidationsRequest, options?: RequestInit): Promise => { return customFetch(getPutResourceValidationsApiConfigsResourceValidationPutUrl(), @@ -5073,8 +5200,7 @@ export const putResourceValidationsApiConfigsResourceValidationPut = async (putR ...options, method: 'PUT', headers: { 'Content-Type': 'application/json', ...options?.headers }, - body: JSON.stringify( - putResourceValidationsRequest,) + body: JSON.stringify(putResourceValidationsRequest) } );} @@ -5126,10 +5252,6 @@ export const usePutResourceValidationsApiConfigsResourceValidationPut = { @@ -5138,6 +5260,10 @@ export const getReadResourceValidationApiConfigsResourceValidationNameGetUrl = ( return `/api/configs/resource_validation/${name}` } +/** + * Read Resource Validation configurations + * @summary Read Resource Validation + */ export const readResourceValidationApiConfigsResourceValidationNameGet = async (name: string, options?: RequestInit): Promise => { return customFetch(getReadResourceValidationApiConfigsResourceValidationNameGetUrl(name), @@ -5175,7 +5301,7 @@ const {query: queryOptions, request: requestOptions} = options ?? {}; - return { queryKey, queryFn, enabled: !!(name), ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } + return { queryKey, queryFn, enabled: name !== null && name !== undefined, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } } export type ReadResourceValidationApiConfigsResourceValidationNameGetQueryResult = NonNullable>> @@ -5237,10 +5363,8 @@ export const invalidateReadResourceValidationApiConfigsResourceValidationNameGet -/** - * Put Resource Validation configurations - * @summary Put Resource Validation - */ + + export const getPutResourceValidationApiConfigsResourceValidationNamePutUrl = (name: string,) => { @@ -5249,6 +5373,10 @@ export const getPutResourceValidationApiConfigsResourceValidationNamePutUrl = (n return `/api/configs/resource_validation/${name}` } +/** + * Put Resource Validation configurations + * @summary Put Resource Validation + */ export const putResourceValidationApiConfigsResourceValidationNamePut = async (name: string, putResourceValidationRequest: PutResourceValidationRequest, options?: RequestInit): Promise => { @@ -5257,8 +5385,7 @@ export const putResourceValidationApiConfigsResourceValidationNamePut = async (n ...options, method: 'PUT', headers: { 'Content-Type': 'application/json', ...options?.headers }, - body: JSON.stringify( - putResourceValidationRequest,) + body: JSON.stringify(putResourceValidationRequest) } );} @@ -5310,10 +5437,6 @@ export const usePutResourceValidationApiConfigsResourceValidationNamePut = { @@ -5322,6 +5445,10 @@ export const getDeleteResourceValidationApiConfigsResourceValidationNameDeleteUr return `/api/configs/resource_validation/${name}` } +/** + * Delete Resource Validation configurations + * @summary Delete Resource Validation + */ export const deleteResourceValidationApiConfigsResourceValidationNameDelete = async (name: string, configsRequest: ConfigsRequest, options?: RequestInit): Promise => { @@ -5330,8 +5457,7 @@ export const deleteResourceValidationApiConfigsResourceValidationNameDelete = as ...options, method: 'DELETE', headers: { 'Content-Type': 'application/json', ...options?.headers }, - body: JSON.stringify( - configsRequest,) + body: JSON.stringify(configsRequest) } );} @@ -5383,10 +5509,6 @@ export const useDeleteResourceValidationApiConfigsResourceValidationNameDelete = return useMutation(getDeleteResourceValidationApiConfigsResourceValidationNameDeleteMutationOptions(options), queryClient); } -/** - * List all Roles - * @summary List Roles - */ export const getListRolesApiConfigsRoleGetUrl = () => { @@ -5395,6 +5517,10 @@ export const getListRolesApiConfigsRoleGetUrl = () => { return `/api/configs/role` } +/** + * List all Roles + * @summary List Roles + */ export const listRolesApiConfigsRoleGet = async ( options?: RequestInit): Promise => { return customFetch(getListRolesApiConfigsRoleGetUrl(), @@ -5494,10 +5620,8 @@ export const invalidateListRolesApiConfigsRoleGet = async ( -/** - * Put Roles - * @summary Put Roles - */ + + export const getPutRolesApiConfigsRolePutUrl = () => { @@ -5506,6 +5630,10 @@ export const getPutRolesApiConfigsRolePutUrl = () => { return `/api/configs/role` } +/** + * Put Roles + * @summary Put Roles + */ export const putRolesApiConfigsRolePut = async (putRolesRequest: PutRolesRequest, options?: RequestInit): Promise => { return customFetch(getPutRolesApiConfigsRolePutUrl(), @@ -5513,8 +5641,7 @@ export const putRolesApiConfigsRolePut = async (putRolesRequest: PutRolesRequest ...options, method: 'PUT', headers: { 'Content-Type': 'application/json', ...options?.headers }, - body: JSON.stringify( - putRolesRequest,) + body: JSON.stringify(putRolesRequest) } );} @@ -5566,10 +5693,6 @@ export const usePutRolesApiConfigsRolePut = { @@ -5578,6 +5701,10 @@ export const getReadRoleApiConfigsRoleNameGetUrl = (name: string,) => { return `/api/configs/role/${name}` } +/** + * Read Role + * @summary Read Role + */ export const readRoleApiConfigsRoleNameGet = async (name: string, options?: RequestInit): Promise => { return customFetch(getReadRoleApiConfigsRoleNameGetUrl(name), @@ -5615,7 +5742,7 @@ const {query: queryOptions, request: requestOptions} = options ?? {}; - return { queryKey, queryFn, enabled: !!(name), ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } + return { queryKey, queryFn, enabled: name !== null && name !== undefined, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } } export type ReadRoleApiConfigsRoleNameGetQueryResult = NonNullable>> @@ -5677,10 +5804,8 @@ export const invalidateReadRoleApiConfigsRoleNameGet = async ( -/** - * Patch Role configurations - * @summary Put Role - */ + + export const getPutRoleApiConfigsRoleNamePutUrl = (name: string,) => { @@ -5689,6 +5814,10 @@ export const getPutRoleApiConfigsRoleNamePutUrl = (name: string,) => { return `/api/configs/role/${name}` } +/** + * Patch Role configurations + * @summary Put Role + */ export const putRoleApiConfigsRoleNamePut = async (name: string, putRoleRequest: PutRoleRequest, options?: RequestInit): Promise => { @@ -5697,8 +5826,7 @@ export const putRoleApiConfigsRoleNamePut = async (name: string, ...options, method: 'PUT', headers: { 'Content-Type': 'application/json', ...options?.headers }, - body: JSON.stringify( - putRoleRequest,) + body: JSON.stringify(putRoleRequest) } );} @@ -5750,10 +5878,6 @@ export const usePutRoleApiConfigsRoleNamePut = { @@ -5762,6 +5886,10 @@ export const getDeleteRoleApiConfigsRoleNameDeleteUrl = (name: string,) => { return `/api/configs/role/${name}` } +/** + * Delete Role + * @summary Delete Role + */ export const deleteRoleApiConfigsRoleNameDelete = async (name: string, configsRequest: ConfigsRequest, options?: RequestInit): Promise => { @@ -5770,8 +5898,7 @@ export const deleteRoleApiConfigsRoleNameDelete = async (name: string, ...options, method: 'DELETE', headers: { 'Content-Type': 'application/json', ...options?.headers }, - body: JSON.stringify( - configsRequest,) + body: JSON.stringify(configsRequest) } );} @@ -5823,10 +5950,6 @@ export const useDeleteRoleApiConfigsRoleNameDelete = { @@ -5835,6 +5958,10 @@ export const getListBackendTestsApiConfigsBackendTestGetUrl = () => { return `/api/configs/backend_test` } +/** + * List all backend test configurations + * @summary List Backend Tests + */ export const listBackendTestsApiConfigsBackendTestGet = async ( options?: RequestInit): Promise => { return customFetch(getListBackendTestsApiConfigsBackendTestGetUrl(), @@ -5934,10 +6061,8 @@ export const invalidateListBackendTestsApiConfigsBackendTestGet = async ( -/** - * Put backend test configurations - * @summary Put Backend Tests - */ + + export const getPutBackendTestsApiConfigsBackendTestPutUrl = () => { @@ -5946,6 +6071,10 @@ export const getPutBackendTestsApiConfigsBackendTestPutUrl = () => { return `/api/configs/backend_test` } +/** + * Put backend test configurations + * @summary Put Backend Tests + */ export const putBackendTestsApiConfigsBackendTestPut = async (putBackendTestsRequest: PutBackendTestsRequest, options?: RequestInit): Promise => { return customFetch(getPutBackendTestsApiConfigsBackendTestPutUrl(), @@ -5953,8 +6082,7 @@ export const putBackendTestsApiConfigsBackendTestPut = async (putBackendTestsReq ...options, method: 'PUT', headers: { 'Content-Type': 'application/json', ...options?.headers }, - body: JSON.stringify( - putBackendTestsRequest,) + body: JSON.stringify(putBackendTestsRequest) } );} @@ -6006,10 +6134,6 @@ export const usePutBackendTestsApiConfigsBackendTestPut = { @@ -6018,6 +6142,10 @@ export const getReadBackendTestApiConfigsBackendTestNameGetUrl = (name: string,) return `/api/configs/backend_test/${name}` } +/** + * Read backend test configuration + * @summary Read Backend Test + */ export const readBackendTestApiConfigsBackendTestNameGet = async (name: string, options?: RequestInit): Promise => { return customFetch(getReadBackendTestApiConfigsBackendTestNameGetUrl(name), @@ -6055,7 +6183,7 @@ const {query: queryOptions, request: requestOptions} = options ?? {}; - return { queryKey, queryFn, enabled: !!(name), ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } + return { queryKey, queryFn, enabled: name !== null && name !== undefined, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } } export type ReadBackendTestApiConfigsBackendTestNameGetQueryResult = NonNullable>> @@ -6117,10 +6245,8 @@ export const invalidateReadBackendTestApiConfigsBackendTestNameGet = async ( -/** - * Put backend test configuration - * @summary Put Backend Test - */ + + export const getPutBackendTestApiConfigsBackendTestNamePutUrl = (name: string,) => { @@ -6129,6 +6255,10 @@ export const getPutBackendTestApiConfigsBackendTestNamePutUrl = (name: string,) return `/api/configs/backend_test/${name}` } +/** + * Put backend test configuration + * @summary Put Backend Test + */ export const putBackendTestApiConfigsBackendTestNamePut = async (name: string, putBackendTestRequest: PutBackendTestRequest, options?: RequestInit): Promise => { @@ -6137,8 +6267,7 @@ export const putBackendTestApiConfigsBackendTestNamePut = async (name: string, ...options, method: 'PUT', headers: { 'Content-Type': 'application/json', ...options?.headers }, - body: JSON.stringify( - putBackendTestRequest,) + body: JSON.stringify(putBackendTestRequest) } );} @@ -6190,10 +6319,6 @@ export const usePutBackendTestApiConfigsBackendTestNamePut = { @@ -6202,6 +6327,10 @@ export const getPatchBackendTestApiConfigsBackendTestNamePatchUrl = (name: strin return `/api/configs/backend_test/${name}` } +/** + * Patch backend test configuration + * @summary Patch Backend Test + */ export const patchBackendTestApiConfigsBackendTestNamePatch = async (name: string, patchBackendTestRequest: PatchBackendTestRequest, options?: RequestInit): Promise => { @@ -6210,8 +6339,7 @@ export const patchBackendTestApiConfigsBackendTestNamePatch = async (name: strin ...options, method: 'PATCH', headers: { 'Content-Type': 'application/json', ...options?.headers }, - body: JSON.stringify( - patchBackendTestRequest,) + body: JSON.stringify(patchBackendTestRequest) } );} @@ -6263,10 +6391,6 @@ export const usePatchBackendTestApiConfigsBackendTestNamePatch = { @@ -6275,6 +6399,10 @@ export const getDeleteBackendTestApiConfigsBackendTestNameDeleteUrl = (name: str return `/api/configs/backend_test/${name}` } +/** + * Delete test configuration + * @summary Delete Backend Test + */ export const deleteBackendTestApiConfigsBackendTestNameDelete = async (name: string, configsRequest: ConfigsRequest, options?: RequestInit): Promise => { @@ -6283,8 +6411,7 @@ export const deleteBackendTestApiConfigsBackendTestNameDelete = async (name: str ...options, method: 'DELETE', headers: { 'Content-Type': 'application/json', ...options?.headers }, - body: JSON.stringify( - configsRequest,) + body: JSON.stringify(configsRequest) } );} @@ -6336,10 +6463,6 @@ export const useDeleteBackendTestApiConfigsBackendTestNameDelete = { const normalizedParams = new URLSearchParams(); @@ -6348,13 +6471,13 @@ export const getGetConfigsHistoryApiConfigsHistoryGetUrl = (params?: GetConfigsH if (Array.isArray(value) && explodeParameters.includes(key)) { value.forEach((v) => { - normalizedParams.append(key, v === null ? 'null' : v.toString()); + normalizedParams.append(key, v === null ? 'null' : String(v)); }); return; } if (value !== undefined) { - normalizedParams.append(key, value === null ? 'null' : value.toString()) + normalizedParams.append(key, value === null ? 'null' : String(value)) } }); @@ -6363,6 +6486,10 @@ export const getGetConfigsHistoryApiConfigsHistoryGetUrl = (params?: GetConfigsH return stringifiedParams.length > 0 ? `/api/configs/history?${stringifiedParams}` : `/api/configs/history` } +/** + * List history of all configs + * @summary Get Configs History + */ export const getConfigsHistoryApiConfigsHistoryGet = async (params?: GetConfigsHistoryApiConfigsHistoryGetParams, options?: RequestInit): Promise => { return customFetch(getGetConfigsHistoryApiConfigsHistoryGetUrl(params), @@ -6462,10 +6589,8 @@ export const invalidateGetConfigsHistoryApiConfigsHistoryGet = async ( -/** - * Roll back a config to a particular revision. - * @summary Rollback Config - */ + + export const getRollbackConfigApiConfigsHistoryRollbackPostUrl = () => { @@ -6474,6 +6599,10 @@ export const getRollbackConfigApiConfigsHistoryRollbackPostUrl = () => { return `/api/configs/history/rollback` } +/** + * Roll back a config to a particular revision. + * @summary Rollback Config + */ export const rollbackConfigApiConfigsHistoryRollbackPost = async (rollbackConfigRequest: RollbackConfigRequest, options?: RequestInit): Promise => { return customFetch(getRollbackConfigApiConfigsHistoryRollbackPostUrl(), @@ -6481,8 +6610,7 @@ export const rollbackConfigApiConfigsHistoryRollbackPost = async (rollbackConfig ...options, method: 'POST', headers: { 'Content-Type': 'application/json', ...options?.headers }, - body: JSON.stringify( - rollbackConfigRequest,) + body: JSON.stringify(rollbackConfigRequest) } );} @@ -6534,18 +6662,6 @@ export const useRollbackConfigApiConfigsHistoryRollbackPost = { @@ -6555,6 +6671,18 @@ export const getDeleteConfigHistoryRevisionApiConfigsHistoryConfigTypeRevisionRe return `/api/configs/history/${configType}/revision/${revision}` } +/** + * Delete a specific config history revision. This performs a soft delete of the revision. + * + * Args: + * config_type: Type of config to delete + * revision: Revision number to delete (must be greater than 0) + * username: Username of the person performing the delete + * + * Raises: + * OSMOUserError: If the revision doesn't exist or is the current revision + * @summary Delete Config History Revision + */ export const deleteConfigHistoryRevisionApiConfigsHistoryConfigTypeRevisionRevisionDelete = async (configType: string, revision: number, options?: RequestInit): Promise => { @@ -6615,19 +6743,6 @@ export const useDeleteConfigHistoryRevisionApiConfigsHistoryConfigTypeRevisionRe return useMutation(getDeleteConfigHistoryRevisionApiConfigsHistoryConfigTypeRevisionRevisionDeleteMutationOptions(options), queryClient); } -/** - * Update tags for a specific config history revision. - -Args: - config_type: Type of config to update - revision: Revision number to update (must be greater than 0) - request: Request containing tags to add and delete - username: Username of the person performing the update - -Raises: - OSMOUserError: If the revision doesn't exist or is invalid - * @summary Update Config History Tags - */ export const getUpdateConfigHistoryTagsApiConfigsHistoryConfigTypeRevisionRevisionTagsPostUrl = (configType: string, revision: number,) => { @@ -6637,6 +6752,19 @@ export const getUpdateConfigHistoryTagsApiConfigsHistoryConfigTypeRevisionRevisi return `/api/configs/history/${configType}/revision/${revision}/tags` } +/** + * Update tags for a specific config history revision. + * + * Args: + * config_type: Type of config to update + * revision: Revision number to update (must be greater than 0) + * request: Request containing tags to add and delete + * username: Username of the person performing the update + * + * Raises: + * OSMOUserError: If the revision doesn't exist or is invalid + * @summary Update Config History Tags + */ export const updateConfigHistoryTagsApiConfigsHistoryConfigTypeRevisionRevisionTagsPost = async (configType: string, revision: number, updateConfigTagsRequest: UpdateConfigTagsRequest, options?: RequestInit): Promise => { @@ -6646,8 +6774,7 @@ export const updateConfigHistoryTagsApiConfigsHistoryConfigTypeRevisionRevisionT ...options, method: 'POST', headers: { 'Content-Type': 'application/json', ...options?.headers }, - body: JSON.stringify( - updateConfigTagsRequest,) + body: JSON.stringify(updateConfigTagsRequest) } );} @@ -6699,28 +6826,13 @@ export const useUpdateConfigHistoryTagsApiConfigsHistoryConfigTypeRevisionRevisi return useMutation(getUpdateConfigHistoryTagsApiConfigsHistoryConfigTypeRevisionRevisionTagsPostMutationOptions(options), queryClient); } -/** - * Returns two config revisions, similar to -GET /api/configs/history/{config_type}/revision/{revision}, but with obfuscated secret strings -that say if a secret string is changed. Intended for use with the `diff` command. - -Args: - request: Request containing config type and revisions to compare - -Returns: - ConfigDiffResponse containing the two revisions - -Raises: - OSMOUserError: If either revision doesn't exist or is invalid - * @summary Get Config Diff - */ export const getGetConfigDiffApiConfigsDiffGetUrl = (params: GetConfigDiffApiConfigsDiffGetParams,) => { const normalizedParams = new URLSearchParams(); Object.entries(params || {}).forEach(([key, value]) => { if (value !== undefined) { - normalizedParams.append(key, value === null ? 'null' : value.toString()) + normalizedParams.append(key, value === null ? 'null' : String(value)) } }); @@ -6729,6 +6841,21 @@ export const getGetConfigDiffApiConfigsDiffGetUrl = (params: GetConfigDiffApiCon return stringifiedParams.length > 0 ? `/api/configs/diff?${stringifiedParams}` : `/api/configs/diff` } +/** + * Returns two config revisions, similar to + * GET /api/configs/history/{config_type}/revision/{revision}, but with obfuscated secret strings + * that say if a secret string is changed. Intended for use with the `diff` command. + * + * Args: + * request: Request containing config type and revisions to compare + * + * Returns: + * ConfigDiffResponse containing the two revisions + * + * Raises: + * OSMOUserError: If either revision doesn't exist or is invalid + * @summary Get Config Diff + */ export const getConfigDiffApiConfigsDiffGet = async (params: GetConfigDiffApiConfigsDiffGetParams, options?: RequestInit): Promise => { return customFetch(getGetConfigDiffApiConfigsDiffGetUrl(params), @@ -6828,19 +6955,15 @@ export const invalidateGetConfigDiffApiConfigsDiffGet = async ( -/** - * API to fetch for a new access token using a refresh token. -Deprecated: Use POST /api/auth/jwt/refresh_token instead. - * @summary Get New Jwt Token - */ + export const getGetNewJwtTokenApiAuthJwtRefreshTokenGetUrl = (params: GetNewJwtTokenApiAuthJwtRefreshTokenGetParams,) => { const normalizedParams = new URLSearchParams(); Object.entries(params || {}).forEach(([key, value]) => { if (value !== undefined) { - normalizedParams.append(key, value === null ? 'null' : value.toString()) + normalizedParams.append(key, value === null ? 'null' : String(value)) } }); @@ -6849,6 +6972,12 @@ export const getGetNewJwtTokenApiAuthJwtRefreshTokenGetUrl = (params: GetNewJwtT return stringifiedParams.length > 0 ? `/api/auth/jwt/refresh_token?${stringifiedParams}` : `/api/auth/jwt/refresh_token` } +/** + * API to fetch for a new access token using a refresh token. + * + * Deprecated: Use POST /api/auth/jwt/refresh_token instead. + * @summary Get New Jwt Token + */ export const getNewJwtTokenApiAuthJwtRefreshTokenGet = async (params: GetNewJwtTokenApiAuthJwtRefreshTokenGetParams, options?: RequestInit): Promise => { return customFetch(getGetNewJwtTokenApiAuthJwtRefreshTokenGetUrl(params), @@ -6948,17 +7077,15 @@ export const invalidateGetNewJwtTokenApiAuthJwtRefreshTokenGet = async ( -/** - * API to fetch for a new access token using a refresh token. - * @summary Post New Jwt Token - */ + + export const getPostNewJwtTokenApiAuthJwtRefreshTokenPostUrl = (params: PostNewJwtTokenApiAuthJwtRefreshTokenPostParams,) => { const normalizedParams = new URLSearchParams(); Object.entries(params || {}).forEach(([key, value]) => { if (value !== undefined) { - normalizedParams.append(key, value === null ? 'null' : value.toString()) + normalizedParams.append(key, value === null ? 'null' : String(value)) } }); @@ -6967,6 +7094,10 @@ export const getPostNewJwtTokenApiAuthJwtRefreshTokenPostUrl = (params: PostNewJ return stringifiedParams.length > 0 ? `/api/auth/jwt/refresh_token?${stringifiedParams}` : `/api/auth/jwt/refresh_token` } +/** + * API to fetch for a new access token using a refresh token. + * @summary Post New Jwt Token + */ export const postNewJwtTokenApiAuthJwtRefreshTokenPost = async (tokenRequest: TokenRequest, params: PostNewJwtTokenApiAuthJwtRefreshTokenPostParams, options?: RequestInit): Promise => { @@ -6975,8 +7106,7 @@ export const postNewJwtTokenApiAuthJwtRefreshTokenPost = async (tokenRequest: To ...options, method: 'POST', headers: { 'Content-Type': 'application/json', ...options?.headers }, - body: JSON.stringify( - tokenRequest,) + body: JSON.stringify(tokenRequest) } );} @@ -7028,19 +7158,13 @@ export const usePostNewJwtTokenApiAuthJwtRefreshTokenPost = { const normalizedParams = new URLSearchParams(); Object.entries(params || {}).forEach(([key, value]) => { if (value !== undefined) { - normalizedParams.append(key, value === null ? 'null' : value.toString()) + normalizedParams.append(key, value === null ? 'null' : String(value)) } }); @@ -7049,6 +7173,12 @@ export const getGetJwtTokenFromAccessTokenApiAuthJwtAccessTokenGetUrl = (params: return stringifiedParams.length > 0 ? `/api/auth/jwt/access_token?${stringifiedParams}` : `/api/auth/jwt/access_token` } +/** + * API to create a new jwt token from an access token. + * + * Deprecated: Use POST /api/auth/jwt/access_token instead. + * @summary Get Jwt Token From Access Token + */ export const getJwtTokenFromAccessTokenApiAuthJwtAccessTokenGet = async (params: GetJwtTokenFromAccessTokenApiAuthJwtAccessTokenGetParams, options?: RequestInit): Promise => { return customFetch(getGetJwtTokenFromAccessTokenApiAuthJwtAccessTokenGetUrl(params), @@ -7148,10 +7278,8 @@ export const invalidateGetJwtTokenFromAccessTokenApiAuthJwtAccessTokenGet = asyn -/** - * API to create a new jwt token from an access token. - * @summary Post Jwt Token From Access Token - */ + + export const getPostJwtTokenFromAccessTokenApiAuthJwtAccessTokenPostUrl = () => { @@ -7160,6 +7288,10 @@ export const getPostJwtTokenFromAccessTokenApiAuthJwtAccessTokenPostUrl = () => return `/api/auth/jwt/access_token` } +/** + * API to create a new jwt token from an access token. + * @summary Post Jwt Token From Access Token + */ export const postJwtTokenFromAccessTokenApiAuthJwtAccessTokenPost = async (tokenRequest: TokenRequest, options?: RequestInit): Promise => { return customFetch(getPostJwtTokenFromAccessTokenApiAuthJwtAccessTokenPostUrl(), @@ -7167,8 +7299,7 @@ export const postJwtTokenFromAccessTokenApiAuthJwtAccessTokenPost = async (token ...options, method: 'POST', headers: { 'Content-Type': 'application/json', ...options?.headers }, - body: JSON.stringify( - tokenRequest,) + body: JSON.stringify(tokenRequest) } );} @@ -7220,15 +7351,6 @@ export const usePostJwtTokenFromAccessTokenApiAuthJwtAccessTokenPost = { const normalizedParams = new URLSearchParams(); @@ -7238,13 +7360,13 @@ export const getCreateAccessTokenApiAuthAccessTokenTokenNamePostUrl = (tokenName if (Array.isArray(value) && explodeParameters.includes(key)) { value.forEach((v) => { - normalizedParams.append(key, v === null ? 'null' : v.toString()); + normalizedParams.append(key, v === null ? 'null' : String(v)); }); return; } if (value !== undefined) { - normalizedParams.append(key, value === null ? 'null' : value.toString()) + normalizedParams.append(key, value === null ? 'null' : String(value)) } }); @@ -7253,6 +7375,15 @@ export const getCreateAccessTokenApiAuthAccessTokenTokenNamePostUrl = (tokenName return stringifiedParams.length > 0 ? `/api/auth/access_token/${tokenName}?${stringifiedParams}` : `/api/auth/access_token/${tokenName}` } +/** + * API to create a new access token. + * + * If roles are specified, all specified roles must be assigned to the user. + * If any role is not assigned to the user, the request fails and no token + * is created. If no roles are specified, the access token inherits all of the user's + * current roles from the user_roles table. + * @summary Create Access Token + */ export const createAccessTokenApiAuthAccessTokenTokenNamePost = async (tokenName: string, params: CreateAccessTokenApiAuthAccessTokenTokenNamePostParams, options?: RequestInit): Promise => { @@ -7313,10 +7444,6 @@ export const useCreateAccessTokenApiAuthAccessTokenTokenNamePost = { @@ -7325,6 +7452,10 @@ export const getDeleteAccessTokenApiAuthAccessTokenTokenNameDeleteUrl = (tokenNa return `/api/auth/access_token/${tokenName}` } +/** + * API to delete an access token. + * @summary Delete Access Token + */ export const deleteAccessTokenApiAuthAccessTokenTokenNameDelete = async (tokenName: string, options?: RequestInit): Promise => { return customFetch(getDeleteAccessTokenApiAuthAccessTokenTokenNameDeleteUrl(tokenName), @@ -7384,17 +7515,6 @@ export const useDeleteAccessTokenApiAuthAccessTokenTokenNameDelete = { @@ -7403,6 +7523,17 @@ export const getListAccessTokenRolesApiAuthAccessTokenTokenNameRolesGetUrl = (to return `/api/auth/access_token/${tokenName}/roles` } +/** + * List all roles assigned to an access token. + * + * Args: + * token_name: The token name + * user_name: Authenticated user (owner of the token) + * + * Returns: + * AccessTokenRolesResponse with list of role assignments + * @summary List Access Token Roles + */ export const listAccessTokenRolesApiAuthAccessTokenTokenNameRolesGet = async (tokenName: string, options?: RequestInit): Promise => { return customFetch(getListAccessTokenRolesApiAuthAccessTokenTokenNameRolesGetUrl(tokenName), @@ -7440,7 +7571,7 @@ const {query: queryOptions, request: requestOptions} = options ?? {}; - return { queryKey, queryFn, enabled: !!(tokenName), ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } + return { queryKey, queryFn, enabled: tokenName !== null && tokenName !== undefined, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } } export type ListAccessTokenRolesApiAuthAccessTokenTokenNameRolesGetQueryResult = NonNullable>> @@ -7502,10 +7633,8 @@ export const invalidateListAccessTokenRolesApiAuthAccessTokenTokenNameRolesGet = -/** - * API to list all access tokens for a user, including their assigned roles. - * @summary List Access Tokens - */ + + export const getListAccessTokensApiAuthAccessTokenGetUrl = () => { @@ -7514,6 +7643,10 @@ export const getListAccessTokensApiAuthAccessTokenGetUrl = () => { return `/api/auth/access_token` } +/** + * API to list all access tokens for a user, including their assigned roles. + * @summary List Access Tokens + */ export const listAccessTokensApiAuthAccessTokenGet = async ( options?: RequestInit): Promise => { return customFetch(getListAccessTokensApiAuthAccessTokenGetUrl(), @@ -7613,29 +7746,8 @@ export const invalidateListAccessTokensApiAuthAccessTokenGet = async ( -/** - * Admin API to create an access token for a specific user. - -This endpoint allows administrators to create an access token -on behalf of any user in the system. -If roles are specified, all specified roles must be assigned to the target -user. If any role is not assigned to the user, the request fails and no -token is created. If no roles are specified, the access token inherits all of the -target user's current roles from the user_roles table. -Args: - user_id: The user ID to create the token for - token_name: Name for the access token - expires_at: Expiration date in YYYY-MM-DD format - description: Optional description for the token - roles: Optional list of roles to assign (must all be assigned to user) - admin_user: Authenticated admin user making the request - -Returns: - The generated access token string - * @summary Admin Create Access Token - */ export const getAdminCreateAccessTokenApiAuthUserUserIdAccessTokenTokenNamePostUrl = (userId: string, tokenName: string, params: AdminCreateAccessTokenApiAuthUserUserIdAccessTokenTokenNamePostParams,) => { @@ -7646,13 +7758,13 @@ export const getAdminCreateAccessTokenApiAuthUserUserIdAccessTokenTokenNamePostU if (Array.isArray(value) && explodeParameters.includes(key)) { value.forEach((v) => { - normalizedParams.append(key, v === null ? 'null' : v.toString()); + normalizedParams.append(key, v === null ? 'null' : String(v)); }); return; } if (value !== undefined) { - normalizedParams.append(key, value === null ? 'null' : value.toString()) + normalizedParams.append(key, value === null ? 'null' : String(value)) } }); @@ -7661,6 +7773,29 @@ export const getAdminCreateAccessTokenApiAuthUserUserIdAccessTokenTokenNamePostU return stringifiedParams.length > 0 ? `/api/auth/user/${userId}/access_token/${tokenName}?${stringifiedParams}` : `/api/auth/user/${userId}/access_token/${tokenName}` } +/** + * Admin API to create an access token for a specific user. + * + * This endpoint allows administrators to create an access token + * on behalf of any user in the system. + * + * If roles are specified, all specified roles must be assigned to the target + * user. If any role is not assigned to the user, the request fails and no + * token is created. If no roles are specified, the access token inherits all of the + * target user's current roles from the user_roles table. + * + * Args: + * user_id: The user ID to create the token for + * token_name: Name for the access token + * expires_at: Expiration date in YYYY-MM-DD format + * description: Optional description for the token + * roles: Optional list of roles to assign (must all be assigned to user) + * admin_user: Authenticated admin user making the request + * + * Returns: + * The generated access token string + * @summary Admin Create Access Token + */ export const adminCreateAccessTokenApiAuthUserUserIdAccessTokenTokenNamePost = async (userId: string, tokenName: string, params: AdminCreateAccessTokenApiAuthUserUserIdAccessTokenTokenNamePostParams, options?: RequestInit): Promise => { @@ -7722,14 +7857,6 @@ export const useAdminCreateAccessTokenApiAuthUserUserIdAccessTokenTokenNamePost return useMutation(getAdminCreateAccessTokenApiAuthUserUserIdAccessTokenTokenNamePostMutationOptions(options), queryClient); } -/** - * Admin API to delete an access token for a specific user. - -Args: - user_id: The user ID who owns the token - token_name: Name of the token to delete - * @summary Admin Delete Access Token - */ export const getAdminDeleteAccessTokenApiAuthUserUserIdAccessTokenTokenNameDeleteUrl = (userId: string, tokenName: string,) => { @@ -7739,6 +7866,14 @@ export const getAdminDeleteAccessTokenApiAuthUserUserIdAccessTokenTokenNameDelet return `/api/auth/user/${userId}/access_token/${tokenName}` } +/** + * Admin API to delete an access token for a specific user. + * + * Args: + * user_id: The user ID who owns the token + * token_name: Name of the token to delete + * @summary Admin Delete Access Token + */ export const adminDeleteAccessTokenApiAuthUserUserIdAccessTokenTokenNameDelete = async (userId: string, tokenName: string, options?: RequestInit): Promise => { @@ -7799,16 +7934,6 @@ export const useAdminDeleteAccessTokenApiAuthUserUserIdAccessTokenTokenNameDelet return useMutation(getAdminDeleteAccessTokenApiAuthUserUserIdAccessTokenTokenNameDeleteMutationOptions(options), queryClient); } -/** - * Admin API to list all access tokens for a specific user, including their assigned roles. - -Args: - user_id: The user ID to list tokens for - -Returns: - List of AccessTokenWithRoles objects - * @summary Admin List Access Tokens - */ export const getAdminListAccessTokensApiAuthUserUserIdAccessTokenGetUrl = (userId: string,) => { @@ -7817,6 +7942,16 @@ export const getAdminListAccessTokensApiAuthUserUserIdAccessTokenGetUrl = (userI return `/api/auth/user/${userId}/access_token` } +/** + * Admin API to list all access tokens for a specific user, including their assigned roles. + * + * Args: + * user_id: The user ID to list tokens for + * + * Returns: + * List of AccessTokenWithRoles objects + * @summary Admin List Access Tokens + */ export const adminListAccessTokensApiAuthUserUserIdAccessTokenGet = async (userId: string, options?: RequestInit): Promise => { return customFetch(getAdminListAccessTokensApiAuthUserUserIdAccessTokenGetUrl(userId), @@ -7854,7 +7989,7 @@ const {query: queryOptions, request: requestOptions} = options ?? {}; - return { queryKey, queryFn, enabled: !!(userId), ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } + return { queryKey, queryFn, enabled: userId !== null && userId !== undefined, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } } export type AdminListAccessTokensApiAuthUserUserIdAccessTokenGetQueryResult = NonNullable>> @@ -7916,20 +8051,8 @@ export const invalidateAdminListAccessTokensApiAuthUserUserIdAccessTokenGet = as -/** - * List all users with optional filtering. -Args: - start_index: Pagination start (1-based, default: 1) - count: Results per page (default: 100, max: 1000) - id_prefix: Filter users whose ID starts with this prefix - roles: List of role names. Returns users who have ANY of these roles. - Use multiple query params: ?roles=admin&roles=user -Returns: - UserListResponse with paginated user list - * @summary List Users - */ export const getListUsersApiAuthUserGetUrl = (params?: ListUsersApiAuthUserGetParams,) => { const normalizedParams = new URLSearchParams(); @@ -7938,13 +8061,13 @@ export const getListUsersApiAuthUserGetUrl = (params?: ListUsersApiAuthUserGetPa if (Array.isArray(value) && explodeParameters.includes(key)) { value.forEach((v) => { - normalizedParams.append(key, v === null ? 'null' : v.toString()); + normalizedParams.append(key, v === null ? 'null' : String(v)); }); return; } if (value !== undefined) { - normalizedParams.append(key, value === null ? 'null' : value.toString()) + normalizedParams.append(key, value === null ? 'null' : String(value)) } }); @@ -7953,6 +8076,20 @@ export const getListUsersApiAuthUserGetUrl = (params?: ListUsersApiAuthUserGetPa return stringifiedParams.length > 0 ? `/api/auth/user?${stringifiedParams}` : `/api/auth/user` } +/** + * List all users with optional filtering. + * + * Args: + * start_index: Pagination start (1-based, default: 1) + * count: Results per page (default: 100, max: 1000) + * id_prefix: Filter users whose ID starts with this prefix + * roles: List of role names. Returns users who have ANY of these roles. + * Use multiple query params: ?roles=admin&roles=user + * + * Returns: + * UserListResponse with paginated user list + * @summary List Users + */ export const listUsersApiAuthUserGet = async (params?: ListUsersApiAuthUserGetParams, options?: RequestInit): Promise => { return customFetch(getListUsersApiAuthUserGetUrl(params), @@ -8052,17 +8189,8 @@ export const invalidateListUsersApiAuthUserGet = async ( -/** - * Create a new user. -Args: - request: CreateUserRequest with user details - created_by: Authenticated user making the request -Returns: - Created User object - * @summary Create User - */ export const getCreateUserApiAuthUserPostUrl = () => { @@ -8071,6 +8199,17 @@ export const getCreateUserApiAuthUserPostUrl = () => { return `/api/auth/user` } +/** + * Create a new user. + * + * Args: + * request: CreateUserRequest with user details + * created_by: Authenticated user making the request + * + * Returns: + * Created User object + * @summary Create User + */ export const createUserApiAuthUserPost = async (createUserRequest: CreateUserRequest, options?: RequestInit): Promise => { return customFetch(getCreateUserApiAuthUserPostUrl(), @@ -8078,8 +8217,7 @@ export const createUserApiAuthUserPost = async (createUserRequest: CreateUserReq ...options, method: 'POST', headers: { 'Content-Type': 'application/json', ...options?.headers }, - body: JSON.stringify( - createUserRequest,) + body: JSON.stringify(createUserRequest) } );} @@ -8131,16 +8269,6 @@ export const useCreateUserApiAuthUserPost = { @@ -8149,6 +8277,16 @@ export const getGetUserApiAuthUserUserIdGetUrl = (userId: string,) => { return `/api/auth/user/${userId}` } +/** + * Get a specific user's details including their roles. + * + * Args: + * user_id: The user ID to fetch + * + * Returns: + * UserWithRoles object + * @summary Get User + */ export const getUserApiAuthUserUserIdGet = async (userId: string, options?: RequestInit): Promise => { return customFetch(getGetUserApiAuthUserUserIdGetUrl(userId), @@ -8186,7 +8324,7 @@ const {query: queryOptions, request: requestOptions} = options ?? {}; - return { queryKey, queryFn, enabled: !!(userId), ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } + return { queryKey, queryFn, enabled: userId !== null && userId !== undefined, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } } export type GetUserApiAuthUserUserIdGetQueryResult = NonNullable>> @@ -8248,13 +8386,8 @@ export const invalidateGetUserApiAuthUserUserIdGet = async ( -/** - * Delete a user and all associated role assignments and PATs. -Args: - user_id: The user ID to delete - * @summary Delete User - */ + export const getDeleteUserApiAuthUserUserIdDeleteUrl = (userId: string,) => { @@ -8263,6 +8396,13 @@ export const getDeleteUserApiAuthUserUserIdDeleteUrl = (userId: string,) => { return `/api/auth/user/${userId}` } +/** + * Delete a user and all associated role assignments and PATs. + * + * Args: + * user_id: The user ID to delete + * @summary Delete User + */ export const deleteUserApiAuthUserUserIdDelete = async (userId: string, options?: RequestInit): Promise => { return customFetch(getDeleteUserApiAuthUserUserIdDeleteUrl(userId), @@ -8322,16 +8462,6 @@ export const useDeleteUserApiAuthUserUserIdDelete = { @@ -8340,6 +8470,16 @@ export const getListUserRolesApiAuthUserUserIdRolesGetUrl = (userId: string,) => return `/api/auth/user/${userId}/roles` } +/** + * List all roles assigned to a user. + * + * Args: + * user_id: The user ID + * + * Returns: + * UserRolesResponse with list of role assignments + * @summary List User Roles + */ export const listUserRolesApiAuthUserUserIdRolesGet = async (userId: string, options?: RequestInit): Promise => { return customFetch(getListUserRolesApiAuthUserUserIdRolesGetUrl(userId), @@ -8377,7 +8517,7 @@ const {query: queryOptions, request: requestOptions} = options ?? {}; - return { queryKey, queryFn, enabled: !!(userId), ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } + return { queryKey, queryFn, enabled: userId !== null && userId !== undefined, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } } export type ListUserRolesApiAuthUserUserIdRolesGetQueryResult = NonNullable>> @@ -8439,18 +8579,8 @@ export const invalidateListUserRolesApiAuthUserUserIdRolesGet = async ( -/** - * Assign a role to a user. -Args: - user_id: The user ID - request: AssignRoleRequest with role_name - assigned_by: Authenticated user making the request -Returns: - UserRoleAssignment with assignment details - * @summary Assign Role To User - */ export const getAssignRoleToUserApiAuthUserUserIdRolesPostUrl = (userId: string,) => { @@ -8459,6 +8589,18 @@ export const getAssignRoleToUserApiAuthUserUserIdRolesPostUrl = (userId: string, return `/api/auth/user/${userId}/roles` } +/** + * Assign a role to a user. + * + * Args: + * user_id: The user ID + * request: AssignRoleRequest with role_name + * assigned_by: Authenticated user making the request + * + * Returns: + * UserRoleAssignment with assignment details + * @summary Assign Role To User + */ export const assignRoleToUserApiAuthUserUserIdRolesPost = async (userId: string, assignRoleRequest: AssignRoleRequest, options?: RequestInit): Promise => { @@ -8467,8 +8609,7 @@ export const assignRoleToUserApiAuthUserUserIdRolesPost = async (userId: string, ...options, method: 'POST', headers: { 'Content-Type': 'application/json', ...options?.headers }, - body: JSON.stringify( - assignRoleRequest,) + body: JSON.stringify(assignRoleRequest) } );} @@ -8520,17 +8661,6 @@ export const useAssignRoleToUserApiAuthUserUserIdRolesPost = { @@ -8540,6 +8670,17 @@ export const getRemoveRoleFromUserApiAuthUserUserIdRolesRoleNameDeleteUrl = (use return `/api/auth/user/${userId}/roles/${roleName}` } +/** + * Remove a role from a user and all their PATs. + * + * When a role is removed from a user, it is automatically removed from all PATs + * owned by that user via the FK cascade from access_token_roles to user_roles. + * + * Args: + * user_id: The user ID + * role_name: The role to remove + * @summary Remove Role From User + */ export const removeRoleFromUserApiAuthUserUserIdRolesRoleNameDelete = async (userId: string, roleName: string, options?: RequestInit): Promise => { @@ -8600,16 +8741,6 @@ export const useRemoveRoleFromUserApiAuthUserUserIdRolesRoleNameDelete = { @@ -8618,6 +8749,16 @@ export const getListUsersWithRoleApiAuthRolesRoleNameUsersGetUrl = (roleName: st return `/api/auth/roles/${roleName}/users` } +/** + * List all users who have a specific role. + * + * Args: + * role_name: The role name + * + * Returns: + * RoleUsersResponse with list of users + * @summary List Users With Role + */ export const listUsersWithRoleApiAuthRolesRoleNameUsersGet = async (roleName: string, options?: RequestInit): Promise => { return customFetch(getListUsersWithRoleApiAuthRolesRoleNameUsersGetUrl(roleName), @@ -8655,7 +8796,7 @@ const {query: queryOptions, request: requestOptions} = options ?? {}; - return { queryKey, queryFn, enabled: !!(roleName), ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } + return { queryKey, queryFn, enabled: roleName !== null && roleName !== undefined, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } } export type ListUsersWithRoleApiAuthRolesRoleNameUsersGetQueryResult = NonNullable>> @@ -8717,18 +8858,8 @@ export const invalidateListUsersWithRoleApiAuthRolesRoleNameUsersGet = async ( -/** - * Bulk assign a role to multiple users. -Args: - role_name: The role to assign - request: BulkAssignRequest with list of user_ids - assigned_by: Authenticated user making the request -Returns: - BulkAssignResponse with results - * @summary Bulk Assign Role - */ export const getBulkAssignRoleApiAuthRolesRoleNameUsersPostUrl = (roleName: string,) => { @@ -8737,6 +8868,18 @@ export const getBulkAssignRoleApiAuthRolesRoleNameUsersPostUrl = (roleName: stri return `/api/auth/roles/${roleName}/users` } +/** + * Bulk assign a role to multiple users. + * + * Args: + * role_name: The role to assign + * request: BulkAssignRequest with list of user_ids + * assigned_by: Authenticated user making the request + * + * Returns: + * BulkAssignResponse with results + * @summary Bulk Assign Role + */ export const bulkAssignRoleApiAuthRolesRoleNameUsersPost = async (roleName: string, bulkAssignRequest: BulkAssignRequest, options?: RequestInit): Promise => { @@ -8745,8 +8888,7 @@ export const bulkAssignRoleApiAuthRolesRoleNameUsersPost = async (roleName: stri ...options, method: 'POST', headers: { 'Content-Type': 'application/json', ...options?.headers }, - body: JSON.stringify( - bulkAssignRequest,) + body: JSON.stringify(bulkAssignRequest) } );} @@ -8798,9 +8940,6 @@ export const useBulkAssignRoleApiAuthRolesRoleNameUsersPost = { const normalizedParams = new URLSearchParams(); @@ -8809,13 +8948,13 @@ export const getListAppsApiAppGetUrl = (params?: ListAppsApiAppGetParams,) => { if (Array.isArray(value) && explodeParameters.includes(key)) { value.forEach((v) => { - normalizedParams.append(key, v === null ? 'null' : v.toString()); + normalizedParams.append(key, v === null ? 'null' : String(v)); }); return; } if (value !== undefined) { - normalizedParams.append(key, value === null ? 'null' : value.toString()) + normalizedParams.append(key, value === null ? 'null' : String(value)) } }); @@ -8824,6 +8963,9 @@ export const getListAppsApiAppGetUrl = (params?: ListAppsApiAppGetParams,) => { return stringifiedParams.length > 0 ? `/api/app?${stringifiedParams}` : `/api/app` } +/** + * @summary List Apps + */ export const listAppsApiAppGet = async (params?: ListAppsApiAppGetParams, options?: RequestInit): Promise => { return customFetch(getListAppsApiAppGetUrl(params), @@ -8923,9 +9065,8 @@ export const invalidateListAppsApiAppGet = async ( -/** - * @summary Get App - */ + + export const getGetAppApiAppUserNameGetUrl = (name: string, params?: GetAppApiAppUserNameGetParams,) => { const normalizedParams = new URLSearchParams(); @@ -8933,7 +9074,7 @@ export const getGetAppApiAppUserNameGetUrl = (name: string, Object.entries(params || {}).forEach(([key, value]) => { if (value !== undefined) { - normalizedParams.append(key, value === null ? 'null' : value.toString()) + normalizedParams.append(key, value === null ? 'null' : String(value)) } }); @@ -8942,6 +9083,9 @@ export const getGetAppApiAppUserNameGetUrl = (name: string, return stringifiedParams.length > 0 ? `/api/app/user/${name}?${stringifiedParams}` : `/api/app/user/${name}` } +/** + * @summary Get App + */ export const getAppApiAppUserNameGet = async (name: string, params?: GetAppApiAppUserNameGetParams, options?: RequestInit): Promise => { @@ -8982,7 +9126,7 @@ const {query: queryOptions, request: requestOptions} = options ?? {}; - return { queryKey, queryFn, enabled: !!(name), ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } + return { queryKey, queryFn, enabled: name !== null && name !== undefined, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } } export type GetAppApiAppUserNameGetQueryResult = NonNullable>> @@ -9049,9 +9193,8 @@ export const invalidateGetAppApiAppUserNameGet = async ( -/** - * @summary Create App - */ + + export const getCreateAppApiAppUserNamePostUrl = (name: string, params: CreateAppApiAppUserNamePostParams,) => { const normalizedParams = new URLSearchParams(); @@ -9059,7 +9202,7 @@ export const getCreateAppApiAppUserNamePostUrl = (name: string, Object.entries(params || {}).forEach(([key, value]) => { if (value !== undefined) { - normalizedParams.append(key, value === null ? 'null' : value.toString()) + normalizedParams.append(key, value === null ? 'null' : String(value)) } }); @@ -9068,6 +9211,9 @@ export const getCreateAppApiAppUserNamePostUrl = (name: string, return stringifiedParams.length > 0 ? `/api/app/user/${name}?${stringifiedParams}` : `/api/app/user/${name}` } +/** + * @summary Create App + */ export const createAppApiAppUserNamePost = async (name: string, createAppApiAppUserNamePostBody: string, params: CreateAppApiAppUserNamePostParams, options?: RequestInit): Promise => { @@ -9077,8 +9223,7 @@ export const createAppApiAppUserNamePost = async (name: string, ...options, method: 'POST', headers: { 'Content-Type': 'application/json', ...options?.headers }, - body: JSON.stringify( - createAppApiAppUserNamePostBody,) + body: JSON.stringify(createAppApiAppUserNamePostBody) } );} @@ -9130,9 +9275,6 @@ export const useCreateAppApiAppUserNamePost = { @@ -9141,6 +9283,9 @@ export const getUpdateAppApiAppUserNamePatchUrl = (name: string,) => { return `/api/app/user/${name}` } +/** + * @summary Update App + */ export const updateAppApiAppUserNamePatch = async (name: string, updateAppApiAppUserNamePatchBody: string, options?: RequestInit): Promise => { @@ -9149,8 +9294,7 @@ export const updateAppApiAppUserNamePatch = async (name: string, ...options, method: 'PATCH', headers: { 'Content-Type': 'application/json', ...options?.headers }, - body: JSON.stringify( - updateAppApiAppUserNamePatchBody,) + body: JSON.stringify(updateAppApiAppUserNamePatchBody) } );} @@ -9202,9 +9346,6 @@ export const useUpdateAppApiAppUserNamePatch = { const normalizedParams = new URLSearchParams(); @@ -9212,7 +9353,7 @@ export const getDeleteAppApiAppUserNameDeleteUrl = (name: string, Object.entries(params || {}).forEach(([key, value]) => { if (value !== undefined) { - normalizedParams.append(key, value === null ? 'null' : value.toString()) + normalizedParams.append(key, value === null ? 'null' : String(value)) } }); @@ -9221,6 +9362,9 @@ export const getDeleteAppApiAppUserNameDeleteUrl = (name: string, return stringifiedParams.length > 0 ? `/api/app/user/${name}?${stringifiedParams}` : `/api/app/user/${name}` } +/** + * @summary Delete App + */ export const deleteAppApiAppUserNameDelete = async (name: string, params?: DeleteAppApiAppUserNameDeleteParams, options?: RequestInit): Promise => { @@ -9281,9 +9425,6 @@ export const useDeleteAppApiAppUserNameDelete = { const normalizedParams = new URLSearchParams(); @@ -9291,7 +9432,7 @@ export const getGetAppContentApiAppUserNameSpecGetUrl = (name: string, Object.entries(params || {}).forEach(([key, value]) => { if (value !== undefined) { - normalizedParams.append(key, value === null ? 'null' : value.toString()) + normalizedParams.append(key, value === null ? 'null' : String(value)) } }); @@ -9300,6 +9441,9 @@ export const getGetAppContentApiAppUserNameSpecGetUrl = (name: string, return stringifiedParams.length > 0 ? `/api/app/user/${name}/spec?${stringifiedParams}` : `/api/app/user/${name}/spec` } +/** + * @summary Get App Content + */ export const getAppContentApiAppUserNameSpecGet = async (name: string, params?: GetAppContentApiAppUserNameSpecGetParams, options?: RequestInit): Promise => { @@ -9340,7 +9484,7 @@ const {query: queryOptions, request: requestOptions} = options ?? {}; - return { queryKey, queryFn, enabled: !!(name), ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } + return { queryKey, queryFn, enabled: name !== null && name !== undefined, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } } export type GetAppContentApiAppUserNameSpecGetQueryResult = NonNullable>> @@ -9407,9 +9551,8 @@ export const invalidateGetAppContentApiAppUserNameSpecGet = async ( -/** - * @summary Rename App - */ + + export const getRenameAppApiAppUserNameRenamePostUrl = (name: string,) => { @@ -9418,6 +9561,9 @@ export const getRenameAppApiAppUserNameRenamePostUrl = (name: string,) => { return `/api/app/user/${name}/rename` } +/** + * @summary Rename App + */ export const renameAppApiAppUserNameRenamePost = async (name: string, renameAppApiAppUserNameRenamePostBody: string, options?: RequestInit): Promise => { @@ -9426,8 +9572,7 @@ export const renameAppApiAppUserNameRenamePost = async (name: string, ...options, method: 'POST', headers: { 'Content-Type': 'application/json', ...options?.headers }, - body: JSON.stringify( - renameAppApiAppUserNameRenamePostBody,) + body: JSON.stringify(renameAppApiAppUserNameRenamePostBody) } );} @@ -9479,10 +9624,6 @@ export const useRenameAppApiAppUserNameRenamePost = { const normalizedParams = new URLSearchParams(); @@ -9490,7 +9631,7 @@ export const getCancelWorkflowApiWorkflowNameCancelPostUrl = (name: string, Object.entries(params || {}).forEach(([key, value]) => { if (value !== undefined) { - normalizedParams.append(key, value === null ? 'null' : value.toString()) + normalizedParams.append(key, value === null ? 'null' : String(value)) } }); @@ -9499,6 +9640,10 @@ export const getCancelWorkflowApiWorkflowNameCancelPostUrl = (name: string, return stringifiedParams.length > 0 ? `/api/workflow/${name}/cancel?${stringifiedParams}` : `/api/workflow/${name}/cancel` } +/** + * Cancels the workflow. + * @summary Cancel Workflow + */ export const cancelWorkflowApiWorkflowNameCancelPost = async (name: string, params?: CancelWorkflowApiWorkflowNameCancelPostParams, options?: RequestInit): Promise => { @@ -9559,24 +9704,21 @@ export const useCancelWorkflowApiWorkflowNameCancelPost = { const normalizedParams = new URLSearchParams(); Object.entries(params || {}).forEach(([key, value]) => { - const explodeParameters = ["users","statuses","pools","tags","priority"]; + const explodeParameters = ["users","statuses","pools","tags","priority","label","no_label"]; if (Array.isArray(value) && explodeParameters.includes(key)) { value.forEach((v) => { - normalizedParams.append(key, v === null ? 'null' : v.toString()); + normalizedParams.append(key, v === null ? 'null' : String(v)); }); return; } if (value !== undefined) { - normalizedParams.append(key, value === null ? 'null' : value.toString()) + normalizedParams.append(key, value === null ? 'null' : String(value)) } }); @@ -9585,6 +9727,9 @@ export const getListWorkflowApiWorkflowGetUrl = (params?: ListWorkflowApiWorkflo return stringifiedParams.length > 0 ? `/api/workflow?${stringifiedParams}` : `/api/workflow` } +/** + * @summary List Workflow + */ export const listWorkflowApiWorkflowGet = async (params?: ListWorkflowApiWorkflowGetParams, options?: RequestInit): Promise => { return customFetch(getListWorkflowApiWorkflowGetUrl(params), @@ -9684,10 +9829,8 @@ export const invalidateListWorkflowApiWorkflowGet = async ( -/** - * Returns the task (with the latest retry_id) with the given name in the workflow. - * @summary Get Workflow Task - */ + + export const getGetWorkflowTaskApiWorkflowNameTaskTaskNameGetUrl = (name: string, taskName: string,) => { @@ -9697,6 +9840,10 @@ export const getGetWorkflowTaskApiWorkflowNameTaskTaskNameGetUrl = (name: string return `/api/workflow/${name}/task/${taskName}` } +/** + * Returns the task (with the latest retry_id) with the given name in the workflow. + * @summary Get Workflow Task + */ export const getWorkflowTaskApiWorkflowNameTaskTaskNameGet = async (name: string, taskName: string, options?: RequestInit): Promise => { @@ -9737,7 +9884,7 @@ const {query: queryOptions, request: requestOptions} = options ?? {}; - return { queryKey, queryFn, enabled: !!(name && taskName), ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } + return { queryKey, queryFn, enabled: name !== null && name !== undefined && taskName !== null && taskName !== undefined, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } } export type GetWorkflowTaskApiWorkflowNameTaskTaskNameGetQueryResult = NonNullable>> @@ -9804,9 +9951,8 @@ export const invalidateGetWorkflowTaskApiWorkflowNameTaskTaskNameGet = async ( -/** - * @summary List Task - */ + + export const getListTaskApiTaskGetUrl = (params?: ListTaskApiTaskGetParams,) => { const normalizedParams = new URLSearchParams(); @@ -9815,13 +9961,13 @@ export const getListTaskApiTaskGetUrl = (params?: ListTaskApiTaskGetParams,) => if (Array.isArray(value) && explodeParameters.includes(key)) { value.forEach((v) => { - normalizedParams.append(key, v === null ? 'null' : v.toString()); + normalizedParams.append(key, v === null ? 'null' : String(v)); }); return; } if (value !== undefined) { - normalizedParams.append(key, value === null ? 'null' : value.toString()) + normalizedParams.append(key, value === null ? 'null' : String(value)) } }); @@ -9830,6 +9976,9 @@ export const getListTaskApiTaskGetUrl = (params?: ListTaskApiTaskGetParams,) => return stringifiedParams.length > 0 ? `/api/task?${stringifiedParams}` : `/api/task` } +/** + * @summary List Task + */ export const listTaskApiTaskGet = async (params?: ListTaskApiTaskGetParams, options?: RequestInit): Promise => { return customFetch(getListTaskApiTaskGetUrl(params), @@ -9929,10 +10078,8 @@ export const invalidateListTaskApiTaskGet = async ( -/** - * Returns the workflow with the given name in the database. - * @summary Get Workflow - */ + + export const getGetWorkflowApiWorkflowNameGetUrl = (name: string, params?: GetWorkflowApiWorkflowNameGetParams,) => { const normalizedParams = new URLSearchParams(); @@ -9940,7 +10087,7 @@ export const getGetWorkflowApiWorkflowNameGetUrl = (name: string, Object.entries(params || {}).forEach(([key, value]) => { if (value !== undefined) { - normalizedParams.append(key, value === null ? 'null' : value.toString()) + normalizedParams.append(key, value === null ? 'null' : String(value)) } }); @@ -9949,6 +10096,10 @@ export const getGetWorkflowApiWorkflowNameGetUrl = (name: string, return stringifiedParams.length > 0 ? `/api/workflow/${name}?${stringifiedParams}` : `/api/workflow/${name}` } +/** + * Returns the workflow with the given name in the database. + * @summary Get Workflow + */ export const getWorkflowApiWorkflowNameGet = async (name: string, params?: GetWorkflowApiWorkflowNameGetParams, options?: RequestInit): Promise => { @@ -9989,7 +10140,7 @@ const {query: queryOptions, request: requestOptions} = options ?? {}; - return { queryKey, queryFn, enabled: !!(name), ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } + return { queryKey, queryFn, enabled: name !== null && name !== undefined, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } } export type GetWorkflowApiWorkflowNameGetQueryResult = NonNullable>> @@ -10056,10 +10207,8 @@ export const invalidateGetWorkflowApiWorkflowNameGet = async ( -/** - * Returns the workflow logs. - * @summary Get Workflow Logs - */ + + export const getGetWorkflowLogsApiWorkflowNameLogsGetUrl = (name: string, params?: GetWorkflowLogsApiWorkflowNameLogsGetParams,) => { const normalizedParams = new URLSearchParams(); @@ -10067,7 +10216,7 @@ export const getGetWorkflowLogsApiWorkflowNameLogsGetUrl = (name: string, Object.entries(params || {}).forEach(([key, value]) => { if (value !== undefined) { - normalizedParams.append(key, value === null ? 'null' : value.toString()) + normalizedParams.append(key, value === null ? 'null' : String(value)) } }); @@ -10076,6 +10225,10 @@ export const getGetWorkflowLogsApiWorkflowNameLogsGetUrl = (name: string, return stringifiedParams.length > 0 ? `/api/workflow/${name}/logs?${stringifiedParams}` : `/api/workflow/${name}/logs` } +/** + * Returns the workflow logs. + * @summary Get Workflow Logs + */ export const getWorkflowLogsApiWorkflowNameLogsGet = async (name: string, params?: GetWorkflowLogsApiWorkflowNameLogsGetParams, options?: RequestInit): Promise => { @@ -10116,7 +10269,7 @@ const {query: queryOptions, request: requestOptions} = options ?? {}; - return { queryKey, queryFn, enabled: !!(name), ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } + return { queryKey, queryFn, enabled: name !== null && name !== undefined, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } } export type GetWorkflowLogsApiWorkflowNameLogsGetQueryResult = NonNullable>> @@ -10183,10 +10336,8 @@ export const invalidateGetWorkflowLogsApiWorkflowNameLogsGet = async ( -/** - * Returns the workflow pod conditions. - * @summary Get Workflow Pod Conditions - */ + + export const getGetWorkflowPodConditionsApiWorkflowNameEventsGetUrl = (name: string, params?: GetWorkflowPodConditionsApiWorkflowNameEventsGetParams,) => { const normalizedParams = new URLSearchParams(); @@ -10194,7 +10345,7 @@ export const getGetWorkflowPodConditionsApiWorkflowNameEventsGetUrl = (name: str Object.entries(params || {}).forEach(([key, value]) => { if (value !== undefined) { - normalizedParams.append(key, value === null ? 'null' : value.toString()) + normalizedParams.append(key, value === null ? 'null' : String(value)) } }); @@ -10203,6 +10354,10 @@ export const getGetWorkflowPodConditionsApiWorkflowNameEventsGetUrl = (name: str return stringifiedParams.length > 0 ? `/api/workflow/${name}/events?${stringifiedParams}` : `/api/workflow/${name}/events` } +/** + * Returns the workflow pod conditions. + * @summary Get Workflow Pod Conditions + */ export const getWorkflowPodConditionsApiWorkflowNameEventsGet = async (name: string, params?: GetWorkflowPodConditionsApiWorkflowNameEventsGetParams, options?: RequestInit): Promise => { @@ -10243,7 +10398,7 @@ const {query: queryOptions, request: requestOptions} = options ?? {}; - return { queryKey, queryFn, enabled: !!(name), ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } + return { queryKey, queryFn, enabled: name !== null && name !== undefined, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } } export type GetWorkflowPodConditionsApiWorkflowNameEventsGetQueryResult = NonNullable>> @@ -10310,10 +10465,8 @@ export const invalidateGetWorkflowPodConditionsApiWorkflowNameEventsGet = async -/** - * Returns the workflow error logs. - * @summary Get Workflow Error Logs - */ + + export const getGetWorkflowErrorLogsApiWorkflowNameErrorLogsGetUrl = (name: string, params?: GetWorkflowErrorLogsApiWorkflowNameErrorLogsGetParams,) => { const normalizedParams = new URLSearchParams(); @@ -10321,7 +10474,7 @@ export const getGetWorkflowErrorLogsApiWorkflowNameErrorLogsGetUrl = (name: stri Object.entries(params || {}).forEach(([key, value]) => { if (value !== undefined) { - normalizedParams.append(key, value === null ? 'null' : value.toString()) + normalizedParams.append(key, value === null ? 'null' : String(value)) } }); @@ -10330,6 +10483,10 @@ export const getGetWorkflowErrorLogsApiWorkflowNameErrorLogsGetUrl = (name: stri return stringifiedParams.length > 0 ? `/api/workflow/${name}/error_logs?${stringifiedParams}` : `/api/workflow/${name}/error_logs` } +/** + * Returns the workflow error logs. + * @summary Get Workflow Error Logs + */ export const getWorkflowErrorLogsApiWorkflowNameErrorLogsGet = async (name: string, params?: GetWorkflowErrorLogsApiWorkflowNameErrorLogsGetParams, options?: RequestInit): Promise => { @@ -10370,7 +10527,7 @@ const {query: queryOptions, request: requestOptions} = options ?? {}; - return { queryKey, queryFn, enabled: !!(name), ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } + return { queryKey, queryFn, enabled: name !== null && name !== undefined, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } } export type GetWorkflowErrorLogsApiWorkflowNameErrorLogsGetQueryResult = NonNullable>> @@ -10437,10 +10594,8 @@ export const invalidateGetWorkflowErrorLogsApiWorkflowNameErrorLogsGet = async ( -/** - * Returns the workflow spec. - * @summary Get Workflow Spec - */ + + export const getGetWorkflowSpecApiWorkflowNameSpecGetUrl = (name: string, params?: GetWorkflowSpecApiWorkflowNameSpecGetParams,) => { const normalizedParams = new URLSearchParams(); @@ -10448,7 +10603,7 @@ export const getGetWorkflowSpecApiWorkflowNameSpecGetUrl = (name: string, Object.entries(params || {}).forEach(([key, value]) => { if (value !== undefined) { - normalizedParams.append(key, value === null ? 'null' : value.toString()) + normalizedParams.append(key, value === null ? 'null' : String(value)) } }); @@ -10457,6 +10612,10 @@ export const getGetWorkflowSpecApiWorkflowNameSpecGetUrl = (name: string, return stringifiedParams.length > 0 ? `/api/workflow/${name}/spec?${stringifiedParams}` : `/api/workflow/${name}/spec` } +/** + * Returns the workflow spec. + * @summary Get Workflow Spec + */ export const getWorkflowSpecApiWorkflowNameSpecGet = async (name: string, params?: GetWorkflowSpecApiWorkflowNameSpecGetParams, options?: RequestInit): Promise => { @@ -10497,7 +10656,7 @@ const {query: queryOptions, request: requestOptions} = options ?? {}; - return { queryKey, queryFn, enabled: !!(name), ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } + return { queryKey, queryFn, enabled: name !== null && name !== undefined, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } } export type GetWorkflowSpecApiWorkflowNameSpecGetQueryResult = NonNullable>> @@ -10564,10 +10723,8 @@ export const invalidateGetWorkflowSpecApiWorkflowNameSpecGet = async ( -/** - * Returns the workflow spec. - * @summary Tag Workflow - */ + + export const getTagWorkflowApiWorkflowNameTagPostUrl = (name: string, params?: TagWorkflowApiWorkflowNameTagPostParams,) => { const normalizedParams = new URLSearchParams(); @@ -10577,13 +10734,13 @@ export const getTagWorkflowApiWorkflowNameTagPostUrl = (name: string, if (Array.isArray(value) && explodeParameters.includes(key)) { value.forEach((v) => { - normalizedParams.append(key, v === null ? 'null' : v.toString()); + normalizedParams.append(key, v === null ? 'null' : String(v)); }); return; } if (value !== undefined) { - normalizedParams.append(key, value === null ? 'null' : value.toString()) + normalizedParams.append(key, value === null ? 'null' : String(value)) } }); @@ -10592,6 +10749,10 @@ export const getTagWorkflowApiWorkflowNameTagPostUrl = (name: string, return stringifiedParams.length > 0 ? `/api/workflow/${name}/tag?${stringifiedParams}` : `/api/workflow/${name}/tag` } +/** + * Returns the workflow spec. + * @summary Tag Workflow + */ export const tagWorkflowApiWorkflowNameTagPost = async (name: string, params?: TagWorkflowApiWorkflowNameTagPostParams, options?: RequestInit): Promise => { @@ -10652,10 +10813,6 @@ export const useTagWorkflowApiWorkflowNameTagPost = { @@ -10664,7 +10821,7 @@ export const getExecIntoGroupApiWorkflowNameExecGroupGroupNamePostUrl = (name: s Object.entries(params || {}).forEach(([key, value]) => { if (value !== undefined) { - normalizedParams.append(key, value === null ? 'null' : value.toString()) + normalizedParams.append(key, value === null ? 'null' : String(value)) } }); @@ -10673,6 +10830,10 @@ export const getExecIntoGroupApiWorkflowNameExecGroupGroupNamePostUrl = (name: s return stringifiedParams.length > 0 ? `/api/workflow/${name}/exec/group/${groupName}?${stringifiedParams}` : `/api/workflow/${name}/exec/group/${groupName}` } +/** + * Send command to all tasks in a group. + * @summary Exec Into Group + */ export const execIntoGroupApiWorkflowNameExecGroupGroupNamePost = async (name: string, groupName: string, params: ExecIntoGroupApiWorkflowNameExecGroupGroupNamePostParams, options?: RequestInit): Promise => { @@ -10734,10 +10895,6 @@ export const useExecIntoGroupApiWorkflowNameExecGroupGroupNamePost = { @@ -10746,7 +10903,7 @@ export const getExecIntoTaskApiWorkflowNameExecTaskTaskNamePostUrl = (name: stri Object.entries(params || {}).forEach(([key, value]) => { if (value !== undefined) { - normalizedParams.append(key, value === null ? 'null' : value.toString()) + normalizedParams.append(key, value === null ? 'null' : String(value)) } }); @@ -10755,6 +10912,10 @@ export const getExecIntoTaskApiWorkflowNameExecTaskTaskNamePostUrl = (name: stri return stringifiedParams.length > 0 ? `/api/workflow/${name}/exec/task/${taskName}?${stringifiedParams}` : `/api/workflow/${name}/exec/task/${taskName}` } +/** + * Exec into a task container. + * @summary Exec Into Task + */ export const execIntoTaskApiWorkflowNameExecTaskTaskNamePost = async (name: string, taskName: string, params: ExecIntoTaskApiWorkflowNameExecTaskTaskNamePostParams, options?: RequestInit): Promise => { @@ -10816,10 +10977,6 @@ export const useExecIntoTaskApiWorkflowNameExecTaskTaskNamePost = { @@ -10830,13 +10987,13 @@ export const getPortForwardTaskApiWorkflowNamePortforwardTaskNamePostUrl = (name if (Array.isArray(value) && explodeParameters.includes(key)) { value.forEach((v) => { - normalizedParams.append(key, v === null ? 'null' : v.toString()); + normalizedParams.append(key, v === null ? 'null' : String(v)); }); return; } if (value !== undefined) { - normalizedParams.append(key, value === null ? 'null' : value.toString()) + normalizedParams.append(key, value === null ? 'null' : String(value)) } }); @@ -10845,6 +11002,10 @@ export const getPortForwardTaskApiWorkflowNamePortforwardTaskNamePostUrl = (name return stringifiedParams.length > 0 ? `/api/workflow/${name}/portforward/${taskName}?${stringifiedParams}` : `/api/workflow/${name}/portforward/${taskName}` } +/** + * Portforward into a task container. + * @summary Port Forward Task + */ export const portForwardTaskApiWorkflowNamePortforwardTaskNamePost = async (name: string, taskName: string, params?: PortForwardTaskApiWorkflowNamePortforwardTaskNamePostParams, options?: RequestInit): Promise => { @@ -10906,10 +11067,6 @@ export const usePortForwardTaskApiWorkflowNamePortforwardTaskNamePost = { @@ -10918,7 +11075,7 @@ export const getPortForwardWebserverApiWorkflowNameWebserverTaskNamePostUrl = (n Object.entries(params || {}).forEach(([key, value]) => { if (value !== undefined) { - normalizedParams.append(key, value === null ? 'null' : value.toString()) + normalizedParams.append(key, value === null ? 'null' : String(value)) } }); @@ -10927,6 +11084,10 @@ export const getPortForwardWebserverApiWorkflowNameWebserverTaskNamePostUrl = (n return stringifiedParams.length > 0 ? `/api/workflow/${name}/webserver/${taskName}?${stringifiedParams}` : `/api/workflow/${name}/webserver/${taskName}` } +/** + * Hold a webserver connection to a task container. + * @summary Port Forward Webserver + */ export const portForwardWebserverApiWorkflowNameWebserverTaskNamePost = async (name: string, taskName: string, params: PortForwardWebserverApiWorkflowNameWebserverTaskNamePostParams, options?: RequestInit): Promise => { @@ -10988,10 +11149,6 @@ export const usePortForwardWebserverApiWorkflowNameWebserverTaskNamePost = { @@ -11001,6 +11158,10 @@ export const getRsyncTaskApiWorkflowNameRsyncTaskTaskNamePostUrl = (name: string return `/api/workflow/${name}/rsync/task/${taskName}` } +/** + * Rsync into a task container. + * @summary Rsync Task + */ export const rsyncTaskApiWorkflowNameRsyncTaskTaskNamePost = async (name: string, taskName: string, options?: RequestInit): Promise => { @@ -11061,10 +11222,6 @@ export const useRsyncTaskApiWorkflowNameRsyncTaskTaskNamePost = { @@ -11073,6 +11230,10 @@ export const getGetUserCredentialApiCredentialsGetUrl = () => { return `/api/credentials` } +/** + * Get default/all user credentials + * @summary Get User Credential + */ export const getUserCredentialApiCredentialsGet = async ( options?: RequestInit): Promise => { return customFetch(getGetUserCredentialApiCredentialsGetUrl(), @@ -11172,10 +11333,8 @@ export const invalidateGetUserCredentialApiCredentialsGet = async ( -/** - * Post/Update user credentials - * @summary Set User Credential - */ + + export const getSetUserCredentialApiCredentialsCredNamePostUrl = (credName: string,) => { @@ -11184,6 +11343,10 @@ export const getSetUserCredentialApiCredentialsCredNamePostUrl = (credName: stri return `/api/credentials/${credName}` } +/** + * Post/Update user credentials + * @summary Set User Credential + */ export const setUserCredentialApiCredentialsCredNamePost = async (credName: string, credentialOptions: CredentialOptions, options?: RequestInit): Promise => { @@ -11192,8 +11355,7 @@ export const setUserCredentialApiCredentialsCredNamePost = async (credName: stri ...options, method: 'POST', headers: { 'Content-Type': 'application/json', ...options?.headers }, - body: JSON.stringify( - credentialOptions,) + body: JSON.stringify(credentialOptions) } );} @@ -11245,10 +11407,6 @@ export const useSetUserCredentialApiCredentialsCredNamePost = { @@ -11257,6 +11415,10 @@ export const getDeleteUsersCredentialApiCredentialsCredNameDeleteUrl = (credName return `/api/credentials/${credName}` } +/** + * Delete user credentials given the secret_id + * @summary Delete Users Credential + */ export const deleteUsersCredentialApiCredentialsCredNameDelete = async (credName: string, options?: RequestInit): Promise => { return customFetch(getDeleteUsersCredentialApiCredentialsCredNameDeleteUrl(credName), @@ -11316,10 +11478,6 @@ export const useDeleteUsersCredentialApiCredentialsCredNameDelete = { const normalizedParams = new URLSearchParams(); @@ -11328,13 +11486,13 @@ export const getGetResourcesApiResourcesGetUrl = (params?: GetResourcesApiResour if (Array.isArray(value) && explodeParameters.includes(key)) { value.forEach((v) => { - normalizedParams.append(key, v === null ? 'null' : v.toString()); + normalizedParams.append(key, v === null ? 'null' : String(v)); }); return; } if (value !== undefined) { - normalizedParams.append(key, value === null ? 'null' : value.toString()) + normalizedParams.append(key, value === null ? 'null' : String(value)) } }); @@ -11343,6 +11501,10 @@ export const getGetResourcesApiResourcesGetUrl = (params?: GetResourcesApiResour return stringifiedParams.length > 0 ? `/api/resources?${stringifiedParams}` : `/api/resources` } +/** + * Returns the information of resources available in different pools. + * @summary Get Resources + */ export const getResourcesApiResourcesGet = async (params?: GetResourcesApiResourcesGetParams, options?: RequestInit): Promise => { return customFetch(getGetResourcesApiResourcesGetUrl(params), @@ -11442,10 +11604,8 @@ export const invalidateGetResourcesApiResourcesGet = async ( -/** - * Returns the request resource's information. - * @summary Get One Resource - */ + + export const getGetOneResourceApiResourcesNameGetUrl = (name: string,) => { @@ -11454,6 +11614,10 @@ export const getGetOneResourceApiResourcesNameGetUrl = (name: string,) => { return `/api/resources/${name}` } +/** + * Returns the request resource's information. + * @summary Get One Resource + */ export const getOneResourceApiResourcesNameGet = async (name: string, options?: RequestInit): Promise => { return customFetch(getGetOneResourceApiResourcesNameGetUrl(name), @@ -11491,7 +11655,7 @@ const {query: queryOptions, request: requestOptions} = options ?? {}; - return { queryKey, queryFn, enabled: !!(name), ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } + return { queryKey, queryFn, enabled: name !== null && name !== undefined, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } } export type GetOneResourceApiResourcesNameGetQueryResult = NonNullable>> @@ -11553,14 +11717,8 @@ export const invalidateGetOneResourceApiResourcesNameGet = async ( -/** - * Returns information regarding pools to users. -If all_pools is set to true, all pools' information will be returned in API response. -Otherwise, only information from pools that the user has access to will be returned -in the response. - * @summary Get Pools - */ + export const getGetPoolsApiPoolGetUrl = (params?: GetPoolsApiPoolGetParams,) => { const normalizedParams = new URLSearchParams(); @@ -11569,13 +11727,13 @@ export const getGetPoolsApiPoolGetUrl = (params?: GetPoolsApiPoolGetParams,) => if (Array.isArray(value) && explodeParameters.includes(key)) { value.forEach((v) => { - normalizedParams.append(key, v === null ? 'null' : v.toString()); + normalizedParams.append(key, v === null ? 'null' : String(v)); }); return; } if (value !== undefined) { - normalizedParams.append(key, value === null ? 'null' : value.toString()) + normalizedParams.append(key, value === null ? 'null' : String(value)) } }); @@ -11584,6 +11742,14 @@ export const getGetPoolsApiPoolGetUrl = (params?: GetPoolsApiPoolGetParams,) => return stringifiedParams.length > 0 ? `/api/pool?${stringifiedParams}` : `/api/pool` } +/** + * Returns information regarding pools to users. + * + * If all_pools is set to true, all pools' information will be returned in API response. + * Otherwise, only information from pools that the user has access to will be returned + * in the response. + * @summary Get Pools + */ export const getPoolsApiPoolGet = async (params?: GetPoolsApiPoolGetParams, options?: RequestInit): Promise => { return customFetch(getGetPoolsApiPoolGetUrl(params), @@ -11683,9 +11849,8 @@ export const invalidateGetPoolsApiPoolGet = async ( -/** - * @summary Get Pool Quotas - */ + + export const getGetPoolQuotasApiPoolQuotaGetUrl = (params?: GetPoolQuotasApiPoolQuotaGetParams,) => { const normalizedParams = new URLSearchParams(); @@ -11694,13 +11859,13 @@ export const getGetPoolQuotasApiPoolQuotaGetUrl = (params?: GetPoolQuotasApiPool if (Array.isArray(value) && explodeParameters.includes(key)) { value.forEach((v) => { - normalizedParams.append(key, v === null ? 'null' : v.toString()); + normalizedParams.append(key, v === null ? 'null' : String(v)); }); return; } if (value !== undefined) { - normalizedParams.append(key, value === null ? 'null' : value.toString()) + normalizedParams.append(key, value === null ? 'null' : String(value)) } }); @@ -11709,6 +11874,9 @@ export const getGetPoolQuotasApiPoolQuotaGetUrl = (params?: GetPoolQuotasApiPool return stringifiedParams.length > 0 ? `/api/pool_quota?${stringifiedParams}` : `/api/pool_quota` } +/** + * @summary Get Pool Quotas + */ export const getPoolQuotasApiPoolQuotaGet = async (params?: GetPoolQuotasApiPoolQuotaGetParams, options?: RequestInit): Promise => { return customFetch(getGetPoolQuotasApiPoolQuotaGetUrl(params), @@ -11808,26 +11976,24 @@ export const invalidateGetPoolQuotasApiPoolQuotaGet = async ( -/** - * This api validates that a workflow is well formed and valid and then submits it. - * @summary Submit Workflow - */ + + export const getSubmitWorkflowApiPoolPoolNameWorkflowPostUrl = (poolName: string, params?: SubmitWorkflowApiPoolPoolNameWorkflowPostParams,) => { const normalizedParams = new URLSearchParams(); Object.entries(params || {}).forEach(([key, value]) => { - const explodeParameters = ["env_vars"]; + const explodeParameters = ["env_vars","label"]; if (Array.isArray(value) && explodeParameters.includes(key)) { value.forEach((v) => { - normalizedParams.append(key, v === null ? 'null' : v.toString()); + normalizedParams.append(key, v === null ? 'null' : String(v)); }); return; } if (value !== undefined) { - normalizedParams.append(key, value === null ? 'null' : value.toString()) + normalizedParams.append(key, value === null ? 'null' : String(value)) } }); @@ -11836,8 +12002,12 @@ export const getSubmitWorkflowApiPoolPoolNameWorkflowPostUrl = (poolName: string return stringifiedParams.length > 0 ? `/api/pool/${poolName}/workflow?${stringifiedParams}` : `/api/pool/${poolName}/workflow` } +/** + * This api validates that a workflow is well formed and valid and then submits it. + * @summary Submit Workflow + */ export const submitWorkflowApiPoolPoolNameWorkflowPost = async (poolName: string, - templateSpecNull: TemplateSpec | null, + templateSpecNull?: TemplateSpec | null, params?: SubmitWorkflowApiPoolPoolNameWorkflowPostParams, options?: RequestInit): Promise => { return customFetch(getSubmitWorkflowApiPoolPoolNameWorkflowPostUrl(poolName,params), @@ -11845,8 +12015,7 @@ export const submitWorkflowApiPoolPoolNameWorkflowPost = async (poolName: string ...options, method: 'POST', headers: { 'Content-Type': 'application/json', ...options?.headers }, - body: JSON.stringify( - templateSpecNull,) + body: JSON.stringify(templateSpecNull) } );} @@ -11854,8 +12023,8 @@ export const submitWorkflowApiPoolPoolNameWorkflowPost = async (poolName: string export const getSubmitWorkflowApiPoolPoolNameWorkflowPostMutationOptions = (options?: { mutation?:UseMutationOptions>, TError,{poolName: string;data: TemplateSpec | null;params?: SubmitWorkflowApiPoolPoolNameWorkflowPostParams}, TContext>, request?: SecondParameter} -): UseMutationOptions>, TError,{poolName: string;data: TemplateSpec | null;params?: SubmitWorkflowApiPoolPoolNameWorkflowPostParams}, TContext> => { + TContext = unknown>(options?: { mutation?:UseMutationOptions>, TError,{poolName: string;data?: TemplateSpec | null;params?: SubmitWorkflowApiPoolPoolNameWorkflowPostParams}, TContext>, request?: SecondParameter} +): UseMutationOptions>, TError,{poolName: string;data?: TemplateSpec | null;params?: SubmitWorkflowApiPoolPoolNameWorkflowPostParams}, TContext> => { const mutationKey = ['submitWorkflowApiPoolPoolNameWorkflowPost']; const {mutation: mutationOptions, request: requestOptions} = options ? @@ -11867,7 +12036,7 @@ const {mutation: mutationOptions, request: requestOptions} = options ? - const mutationFn: MutationFunction>, {poolName: string;data: TemplateSpec | null;params?: SubmitWorkflowApiPoolPoolNameWorkflowPostParams}> = (props) => { + const mutationFn: MutationFunction>, {poolName: string;data?: TemplateSpec | null;params?: SubmitWorkflowApiPoolPoolNameWorkflowPostParams}> = (props) => { const {poolName,data,params} = props ?? {}; return submitWorkflowApiPoolPoolNameWorkflowPost(poolName,data,params,requestOptions) @@ -11881,27 +12050,23 @@ const {mutation: mutationOptions, request: requestOptions} = options ? return { mutationFn, ...mutationOptions }} export type SubmitWorkflowApiPoolPoolNameWorkflowPostMutationResult = NonNullable>> - export type SubmitWorkflowApiPoolPoolNameWorkflowPostMutationBody = TemplateSpec | null + export type SubmitWorkflowApiPoolPoolNameWorkflowPostMutationBody = TemplateSpec | null | undefined export type SubmitWorkflowApiPoolPoolNameWorkflowPostMutationError = HTTPValidationError /** * @summary Submit Workflow */ export const useSubmitWorkflowApiPoolPoolNameWorkflowPost = (options?: { mutation?:UseMutationOptions>, TError,{poolName: string;data: TemplateSpec | null;params?: SubmitWorkflowApiPoolPoolNameWorkflowPostParams}, TContext>, request?: SecondParameter} + TContext = unknown>(options?: { mutation?:UseMutationOptions>, TError,{poolName: string;data?: TemplateSpec | null;params?: SubmitWorkflowApiPoolPoolNameWorkflowPostParams}, TContext>, request?: SecondParameter} , queryClient?: QueryClient): UseMutationResult< Awaited>, TError, - {poolName: string;data: TemplateSpec | null;params?: SubmitWorkflowApiPoolPoolNameWorkflowPostParams}, + {poolName: string;data?: TemplateSpec | null;params?: SubmitWorkflowApiPoolPoolNameWorkflowPostParams}, TContext > => { return useMutation(getSubmitWorkflowApiPoolPoolNameWorkflowPostMutationOptions(options), queryClient); } -/** - * This api restarts a failed workflow and then submits it. - * @summary Restart Workflow - */ export const getRestartWorkflowApiPoolPoolNameWorkflowWorkflowIdRestartPostUrl = (poolName: string, workflowId: string,) => { @@ -11911,6 +12076,10 @@ export const getRestartWorkflowApiPoolPoolNameWorkflowWorkflowIdRestartPostUrl = return `/api/pool/${poolName}/workflow/${workflowId}/restart` } +/** + * This api restarts a failed workflow and then submits it. + * @summary Restart Workflow + */ export const restartWorkflowApiPoolPoolNameWorkflowWorkflowIdRestartPost = async (poolName: string, workflowId: string, options?: RequestInit): Promise => { @@ -11971,9 +12140,6 @@ export const useRestartWorkflowApiPoolPoolNameWorkflowWorkflowIdRestartPost = { @@ -11982,6 +12148,9 @@ export const getGetNotificationSettingsApiProfileSettingsGetUrl = () => { return `/api/profile/settings` } +/** + * @summary Get Notification Settings + */ export const getNotificationSettingsApiProfileSettingsGet = async ( options?: RequestInit): Promise => { return customFetch(getGetNotificationSettingsApiProfileSettingsGetUrl(), @@ -12081,16 +12250,15 @@ export const invalidateGetNotificationSettingsApiProfileSettingsGet = async ( -/** - * @summary Set Notification Settings - */ + + export const getSetNotificationSettingsApiProfileSettingsPostUrl = (params?: SetNotificationSettingsApiProfileSettingsPostParams,) => { const normalizedParams = new URLSearchParams(); Object.entries(params || {}).forEach(([key, value]) => { if (value !== undefined) { - normalizedParams.append(key, value === null ? 'null' : value.toString()) + normalizedParams.append(key, value === null ? 'null' : String(value)) } }); @@ -12099,6 +12267,9 @@ export const getSetNotificationSettingsApiProfileSettingsPostUrl = (params?: Set return stringifiedParams.length > 0 ? `/api/profile/settings?${stringifiedParams}` : `/api/profile/settings` } +/** + * @summary Set Notification Settings + */ export const setNotificationSettingsApiProfileSettingsPost = async (userProfile: UserProfile, params?: SetNotificationSettingsApiProfileSettingsPostParams, options?: RequestInit): Promise => { @@ -12107,8 +12278,7 @@ export const setNotificationSettingsApiProfileSettingsPost = async (userProfile: ...options, method: 'POST', headers: { 'Content-Type': 'application/json', ...options?.headers }, - body: JSON.stringify( - userProfile,) + body: JSON.stringify(userProfile) } );} @@ -12160,9 +12330,6 @@ export const useSetNotificationSettingsApiProfileSettingsPost = { @@ -12171,6 +12338,9 @@ export const getGetOsmoClientVersionClientVersionGetUrl = () => { return `/client/version` } +/** + * @summary Get Osmo Client Version + */ export const getOsmoClientVersionClientVersionGet = async ( options?: RequestInit): Promise => { return customFetch(getGetOsmoClientVersionClientVersionGetUrl(), @@ -12270,11 +12440,8 @@ export const invalidateGetOsmoClientVersionClientVersionGet = async ( -/** - * To be used for the readiness probe, but not liveness probe. That way, if this method is -slow, no new traffic gets routed, instead of killing the service. - * @summary Health - */ + + export const getHealthHealthGetUrl = () => { @@ -12283,6 +12450,11 @@ export const getHealthHealthGetUrl = () => { return `/health` } +/** + * To be used for the readiness probe, but not liveness probe. That way, if this method is + * slow, no new traffic gets routed, instead of killing the service. + * @summary Health + */ export const healthHealthGet = async ( options?: RequestInit): Promise => { return customFetch(getHealthHealthGetUrl(), @@ -12382,9 +12554,8 @@ export const invalidateHealthHealthGet = async ( -/** - * @summary Get Version - */ + + export const getGetVersionApiVersionGetUrl = () => { @@ -12393,6 +12564,9 @@ export const getGetVersionApiVersionGetUrl = () => { return `/api/version` } +/** + * @summary Get Version + */ export const getVersionApiVersionGet = async ( options?: RequestInit): Promise => { return customFetch(getGetVersionApiVersionGetUrl(), @@ -12492,10 +12666,8 @@ export const invalidateGetVersionApiVersionGet = async ( -/** - * Returns the values of all users who have submitted a workflow. - * @summary Get Users - */ + + export const getGetUsersApiUsersGetUrl = () => { @@ -12504,6 +12676,10 @@ export const getGetUsersApiUsersGetUrl = () => { return `/api/users` } +/** + * Returns the values of all users who have submitted a workflow. + * @summary Get Users + */ export const getUsersApiUsersGet = async ( options?: RequestInit): Promise => { return customFetch(getGetUsersApiUsersGetUrl(), @@ -12603,10 +12779,8 @@ export const invalidateGetUsersApiUsersGet = async ( -/** - * Returns all workflow tags. - * @summary Get Available Workflow Tags - */ + + export const getGetAvailableWorkflowTagsApiTagGetUrl = () => { @@ -12615,6 +12789,10 @@ export const getGetAvailableWorkflowTagsApiTagGetUrl = () => { return `/api/tag` } +/** + * Returns all workflow tags. + * @summary Get Available Workflow Tags + */ export const getAvailableWorkflowTagsApiTagGet = async ( options?: RequestInit): Promise => { return customFetch(getGetAvailableWorkflowTagsApiTagGetUrl(), @@ -12714,10 +12892,8 @@ export const invalidateGetAvailableWorkflowTagsApiTagGet = async ( -/** - * Get all the workflow plugins configurations - * @summary Get Workflow Plugins Configs - */ + + export const getGetWorkflowPluginsConfigsApiPluginsConfigsGetUrl = () => { @@ -12726,6 +12902,10 @@ export const getGetWorkflowPluginsConfigsApiPluginsConfigsGetUrl = () => { return `/api/plugins/configs` } +/** + * Get all the workflow plugins configurations + * @summary Get Workflow Plugins Configs + */ export const getWorkflowPluginsConfigsApiPluginsConfigsGet = async ( options?: RequestInit): Promise => { return customFetch(getGetWorkflowPluginsConfigsApiPluginsConfigsGetUrl(), diff --git a/src/ui/src/lib/workflow-labels.test.ts b/src/ui/src/lib/workflow-labels.test.ts index 6af99b56b2..5a8f11c00d 100644 --- a/src/ui/src/lib/workflow-labels.test.ts +++ b/src/ui/src/lib/workflow-labels.test.ts @@ -29,8 +29,8 @@ const draft = (key: string, value: string): WorkflowLabelDraft => ({ key, value describe("workflow label drafts", () => { it("sends only labels changed from a resubmitted workflow", () => { expect( - getChangedWorkflowLabelAssignments([draft("PPP", "robotics"), draft("team", "simulation"), draft("run", "42")], { - PPP: "robotics", + getChangedWorkflowLabelAssignments([draft("project", "robotics"), draft("team", "simulation"), draft("run", "42")], { + project: "robotics", team: "robotics", }), ).toEqual(["team=simulation", "run=42"]); diff --git a/src/ui/src/mocks/generated-mocks.ts b/src/ui/src/mocks/generated-mocks.ts index f6a720d66b..7575e3c376 100644 --- a/src/ui/src/mocks/generated-mocks.ts +++ b/src/ui/src/mocks/generated-mocks.ts @@ -1,5 +1,5 @@ /** - * Generated by orval v8.5.3 🍺 + * Generated by orval v8.17.0 🍺 * Do not edit manually. * FastAPI * OpenAPI spec version: 0.1.0 @@ -217,10 +217,10 @@ export interface ConfigDiffResponse { /** * Type of configs supported by config history */ -export type SrcLibUtilsConfigHistoryConfigHistoryType = - (typeof SrcLibUtilsConfigHistoryConfigHistoryType)[keyof typeof SrcLibUtilsConfigHistoryConfigHistoryType]; +export type ConfigHistoryType = (typeof ConfigHistoryType)[keyof typeof ConfigHistoryType]; -export const SrcLibUtilsConfigHistoryConfigHistoryType = { +export const ConfigHistoryType = { + DATASET: "DATASET", SERVICE: "SERVICE", WORKFLOW: "WORKFLOW", BACKEND: "BACKEND", @@ -236,7 +236,7 @@ export const SrcLibUtilsConfigHistoryConfigHistoryType = { * Object storing config history. */ export interface ConfigHistory { - config_type: SrcLibUtilsConfigHistoryConfigHistoryType; + config_type: ConfigHistoryType; name: string; revision: number; username: string; @@ -291,6 +291,19 @@ export interface UserRegistryCredential { auth: string; } +/** + * S3 request addressing style for S3-compatible providers. Use 'virtual' for providers such as CoreWeave CAIOS that reject path-style requests, or 'path' for localstack/MinIO-style endpoints. + */ +export type UserDataCredentialAddressingStyle = + | (typeof UserDataCredentialAddressingStyle)[keyof typeof UserDataCredentialAddressingStyle] + | null; + +export const UserDataCredentialAddressingStyle = { + virtual: "virtual", + path: "path", + auto: "auto", +} as const; + /** * Authentication information for a data service. */ @@ -302,7 +315,7 @@ export interface UserDataCredential { /** HTTP service URL for S3-compatible providers (e.g., http://minio:9000, https://s3-compat.example.com). Leave unset for native AWS S3, GCS, Azure, etc. */ override_url?: string | null; /** S3 request addressing style for S3-compatible providers. Use 'virtual' for providers such as CoreWeave CAIOS that reject path-style requests, or 'path' for localstack/MinIO-style endpoints. */ - addressing_style?: "virtual" | "path" | "auto" | null; + addressing_style?: UserDataCredentialAddressingStyle; /** The authentication key for a data backend */ access_key_id: string; /** The authentication secret for a data backend */ @@ -334,6 +347,19 @@ export interface CredentialOptions { generic_credential?: UserCredential | null; } +/** + * S3 request addressing style for S3-compatible providers. Use 'virtual' for providers such as CoreWeave CAIOS that reject path-style requests, or 'path' for localstack/MinIO-style endpoints. + */ +export type StaticDataCredentialAddressingStyle = + | (typeof StaticDataCredentialAddressingStyle)[keyof typeof StaticDataCredentialAddressingStyle] + | null; + +export const StaticDataCredentialAddressingStyle = { + virtual: "virtual", + path: "path", + auto: "auto", +} as const; + /** * Static data credentials (i.e. credentials with access_key_id and access_key) for a data backend. */ @@ -345,7 +371,7 @@ export interface StaticDataCredential { /** HTTP service URL for S3-compatible providers (e.g., http://minio:9000, https://s3-compat.example.com). Leave unset for native AWS S3, GCS, Azure, etc. */ override_url?: string | null; /** S3 request addressing style for S3-compatible providers. Use 'virtual' for providers such as CoreWeave CAIOS that reject path-style requests, or 'path' for localstack/MinIO-style endpoints. */ - addressing_style?: "virtual" | "path" | "auto" | null; + addressing_style?: StaticDataCredentialAddressingStyle; /** The authentication key for a data backend */ access_key_id: string; /** The encrypted authentication secret for a data backend */ @@ -353,13 +379,26 @@ export interface StaticDataCredential { } /** - * Data credential that delegates resolution to the underlying SDK. + * S3 request addressing style for S3-compatible providers. Use 'virtual' for providers such as CoreWeave CAIOS that reject path-style requests, or 'path' for localstack/MinIO-style endpoints. + */ +export type DefaultDataCredentialAddressingStyle = + | (typeof DefaultDataCredentialAddressingStyle)[keyof typeof DefaultDataCredentialAddressingStyle] + | null; -Uses the SDK's default credential chain (e.g., Azure's DefaultAzureCredential, -boto3's credential resolution) which may include environment variables, -workload identity, instance metadata, and other provider-specific methods. +export const DefaultDataCredentialAddressingStyle = { + virtual: "virtual", + path: "path", + auto: "auto", +} as const; -Intentionally left empty as all credential resolution is handled by the SDK. +/** + * Data credential that delegates resolution to the underlying SDK. + * + * Uses the SDK's default credential chain (e.g., Azure's DefaultAzureCredential, + * boto3's credential resolution) which may include environment variables, + * workload identity, instance metadata, and other provider-specific methods. + * + * Intentionally left empty as all credential resolution is handled by the SDK. */ export interface DefaultDataCredential { /** The OSMO storage URI for the data service (e.g., s3://bucket). For S3-compatible services with HTTP endpoints, set 'override_url' separately rather than pasting the full HTTPS URL here. */ @@ -369,7 +408,7 @@ export interface DefaultDataCredential { /** HTTP service URL for S3-compatible providers (e.g., http://minio:9000, https://s3-compat.example.com). Leave unset for native AWS S3, GCS, Azure, etc. */ override_url?: string | null; /** S3 request addressing style for S3-compatible providers. Use 'virtual' for providers such as CoreWeave CAIOS that reject path-style requests, or 'path' for localstack/MinIO-style endpoints. */ - addressing_style?: "virtual" | "path" | "auto" | null; + addressing_style?: DefaultDataCredentialAddressingStyle; } /** @@ -422,7 +461,7 @@ export const PoolStatus = { /** * Resources like GPU or CPU that have a discrete number. For guarantee and maximum, a value of -1 -indicates that there is no limit. + * indicates that there is no limit. */ export interface PoolResourceCountable { guarantee?: number; @@ -593,10 +632,14 @@ export interface GroupQueryResponse { tasks?: TaskQueryResponse[]; } +export type ValidationErrorCtx = { [key: string]: unknown }; + export interface ValidationError { loc: (string | number)[]; msg: string; type: string; + input?: unknown; + ctx?: ValidationErrorCtx; } export interface HTTPValidationError { @@ -612,6 +655,47 @@ export interface JwtTokenResponse { error?: string | null; } +/** + * Per-key policy strictness: 'off' skips checking, 'warn' surfaces + * missing or unlisted values as submission warnings, and 'enforce' + * rejects them. + */ +export type LabelEnforcement = (typeof LabelEnforcement)[keyof typeof LabelEnforcement]; + +export const LabelEnforcement = { + off: "off", + warn: "warn", + enforce: "enforce", +} as const; + +/** + * Configuration for one admin-designated workflow label key. + * + * An empty allow_list accepts any well-formed value; enforcement then + * applies only to the key being present. + */ +export interface LabelPolicy { + key: string; + allow_list?: string[]; + enforcement?: LabelEnforcement; +} + +/** + * Curated workflow label policy; empty by default, so no policy + * applies until configured. + */ +export interface LabelsConfigInput { + policy?: LabelPolicy[]; +} + +/** + * Curated workflow label policy; empty by default, so no policy + * applies until configured. + */ +export interface LabelsConfigOutput { + policy?: LabelPolicy[]; +} + /** * Object storing info for all backends. */ @@ -755,6 +839,23 @@ export interface NotificationConfig { smtp_settings?: SMTPConfig; } +/** + * Type of configs supported by config history mutations. + */ +export type OperableConfigHistoryType = (typeof OperableConfigHistoryType)[keyof typeof OperableConfigHistoryType]; + +export const OperableConfigHistoryType = { + SERVICE: "SERVICE", + WORKFLOW: "WORKFLOW", + BACKEND: "BACKEND", + POOL: "POOL", + POD_TEMPLATE: "POD_TEMPLATE", + GROUP_TEMPLATE: "GROUP_TEMPLATE", + RESOURCE_VALIDATION: "RESOURCE_VALIDATION", + BACKEND_TEST: "BACKEND_TEST", + ROLE: "ROLE", +} as const; + export type OperatorType = (typeof OperatorType)[keyof typeof OperatorType]; export const OperatorType = { @@ -779,7 +880,7 @@ export interface RegistryCredential { /** * Dynamic Config for storing the image URLs for service images and the credentials needed -to pull them. + * to pull them. */ export interface OsmoImageConfig { init?: string; @@ -1156,7 +1257,7 @@ export interface TokenIdentity { /** * Profile and identity info. When token header is set, roles/pools are the -token's; otherwise they are the user's. JSON is self-explanatory for CLI. + * token's; otherwise they are the user's. JSON is self-explanatory for CLI. */ export interface ProfileResponse { profile: UserProfile; @@ -1286,13 +1387,13 @@ export interface PutResourceValidationsRequest { /** * Single Role Policy Entry. - -Contains a list of actions (semantic format "resource:Action") and optional -resources the policy applies to. If effect is Deny and the policy matches, -access is denied even if another policy allows it. - -Actions are validated via regex; API/DB still use [{"action": "..."}] for -compatibility with the Go authz_sidecar. + * + * Contains a list of actions (semantic format "resource:Action") and optional + * resources the policy applies to. If effect is Deny and the policy matches, + * access is denied even if another policy allows it. + * + * Actions are validated via regex; API/DB still use [{"action": "..."}] for + * compatibility with the Go authz_sidecar. */ export interface RolePolicy { effect?: PolicyEffect; @@ -1302,10 +1403,10 @@ export interface RolePolicy { /** * Sync mode for role assignments. - -- FORCE: Always apply this role to all users (e.g., for system roles) -- IMPORT: Role is imported from IDP claims or user_roles table (default) -- IGNORE: Ignore this role in IDP sync (role is managed manually) + * + * - FORCE: Always apply this role to all users (e.g., for system roles) + * - IMPORT: Role is imported from IDP claims or user_roles table (default) + * - IGNORE: Ignore this role in IDP sync (role is managed manually) */ export type SyncMode = (typeof SyncMode)[keyof typeof SyncMode]; @@ -1317,9 +1418,9 @@ export const SyncMode = { /** * Single Role Entry. - -Note: Authorization checking is now handled by the authz_sidecar (Go service). -This Python class is only used for role CRUD operations. + * + * Note: Authorization checking is now handled by the authz_sidecar (Go service). + * This Python class is only used for role CRUD operations. */ export interface RoleInput { name: string; @@ -1378,7 +1479,7 @@ export interface WorkflowInfo { /** * Stores workflow limits per user. Default is None, which means no limit. -If a limit is set, it must be greater than 0. + * If a limit is set, it must be greater than 0. */ export interface UserWorkflowLimitConfig { max_num_workflows?: number | null; @@ -1401,6 +1502,7 @@ export interface WorkflowConfigInput { credential_config?: CredentialConfig; user_workflow_limits?: UserWorkflowLimitConfig; plugins_config?: PluginsConfigInput; + labels_config?: LabelsConfigInput; max_num_tasks?: number; max_num_ports_per_task?: number; max_retry_per_task?: number; @@ -1497,9 +1599,9 @@ export interface ResourcesResponse { /** * Single Role Entry. - -Note: Authorization checking is now handled by the authz_sidecar (Go service). -This Python class is only used for role CRUD operations. + * + * Note: Authorization checking is now handled by the authz_sidecar (Go service). + * This Python class is only used for role CRUD operations. */ export interface RoleOutput { name: string; @@ -1526,7 +1628,7 @@ export interface RoleUsersResponse { export interface RollbackConfigRequest { description?: string | null; tags?: string[] | null; - config_type: SrcLibUtilsConfigHistoryConfigHistoryType; + config_type: OperableConfigHistoryType; /** * Revision to roll back to * @exclusiveMinimum 0 @@ -1563,6 +1665,7 @@ export interface SubmitResponse { logs?: string | null; spec?: string | null; dashboard_url?: string | null; + warnings?: string[]; } /** @@ -1696,6 +1799,7 @@ export interface WorkflowConfigOutput { credential_config?: CredentialConfig; user_workflow_limits?: UserWorkflowLimitConfig; plugins_config?: PluginsConfigOutput; + labels_config?: LabelsConfigOutput; max_num_tasks?: number; max_num_ports_per_task?: number; max_retry_per_task?: number; @@ -1731,6 +1835,8 @@ export const WorkflowPriority = { LOW: "LOW", } as const; +export type WorkflowQueryResponseLabels = { [key: string]: string }; + /** * Represents the status of a workflow. */ @@ -1790,6 +1896,8 @@ export interface WorkflowQueryResponse { app_version?: number | null; plugins: WorkflowPlugins; priority: string; + labels?: WorkflowQueryResponseLabels; + warnings?: string[]; } export interface SrcServiceCoreAppObjectsListEntry { @@ -1806,6 +1914,8 @@ export interface SrcServiceCoreAppObjectsListResponse { more_entries: boolean; } +export type SrcServiceCoreWorkflowObjectsListEntryLabels = { [key: string]: string }; + /** * Entry for list API results. */ @@ -1829,6 +1939,7 @@ export interface SrcServiceCoreWorkflowObjectsListEntry { app_name?: string | null; app_version?: number | null; priority: string; + labels?: SrcServiceCoreWorkflowObjectsListEntryLabels; } export interface SrcServiceCoreWorkflowObjectsListResponse { @@ -1893,7 +2004,7 @@ export type GetConfigsHistoryApiConfigsHistoryGetParams = { /** * Filter by config types */ - config_types?: SrcLibUtilsConfigHistoryConfigHistoryType[] | null; + config_types?: ConfigHistoryType[] | null; /** * Filter by config name */ @@ -1925,7 +2036,7 @@ export type GetConfigsHistoryApiConfigsHistoryGetParams = { }; export type GetConfigDiffApiConfigsDiffGetParams = { - config_type: SrcLibUtilsConfigHistoryConfigHistoryType; + config_type: ConfigHistoryType; /** * First revision to compare * @exclusiveMinimum 0 @@ -2026,6 +2137,14 @@ export type ListWorkflowApiWorkflowGetParams = { tags?: string[] | null; app?: string | null; priority?: WorkflowPriority[] | null; + /** + * Workflow label selector: key=value with optional * wildcards and (a|b) alternatives, for example key=(team_*|osmo_*) or key=team_(a|b). Repeat for AND semantics. + */ + label?: string[] | null; + /** + * Label key that must be absent from the workflow; workflows without any labels match. Repeat for AND semantics. + */ + no_label?: string[] | null; }; export type ListTaskApiTaskGetParams = { @@ -2123,6 +2242,7 @@ export type SubmitWorkflowApiPoolPoolNameWorkflowPostParams = { validation_only?: boolean; priority?: WorkflowPriority; env_vars?: string[]; + label?: string[]; }; export type SetNotificationSettingsApiProfileSettingsPostParams = { @@ -2133,10 +2253,6 @@ export type HealthHealthGet200 = { [key: string]: string }; export type GetAvailableWorkflowTagsApiTagGet200 = { [key: string]: string[] }; -/** - * Read all the service configurations - * @summary Read Service Configs - */ export type readServiceConfigsApiConfigsServiceGetResponse200 = { data: ServiceConfigOutput; status: 200; @@ -2152,6 +2268,10 @@ export const getReadServiceConfigsApiConfigsServiceGetUrl = () => { return `/api/configs/service`; }; +/** + * Read all the service configurations + * @summary Read Service Configs + */ export const readServiceConfigsApiConfigsServiceGet = async ( options?: RequestInit, ): Promise => { @@ -2166,10 +2286,6 @@ export const readServiceConfigsApiConfigsServiceGet = async ( return { data, status: res.status, headers: res.headers } as readServiceConfigsApiConfigsServiceGetResponse; }; -/** - * Put service configurations - * @summary Put Service Configs - */ export type putServiceConfigsApiConfigsServicePutResponse200 = { data: PutServiceConfigsApiConfigsServicePut200; status: 200; @@ -2195,6 +2311,10 @@ export const getPutServiceConfigsApiConfigsServicePutUrl = () => { return `/api/configs/service`; }; +/** + * Put service configurations + * @summary Put Service Configs + */ export const putServiceConfigsApiConfigsServicePut = async ( putServiceRequest: PutServiceRequest, options?: RequestInit, @@ -2212,10 +2332,6 @@ export const putServiceConfigsApiConfigsServicePut = async ( return { data, status: res.status, headers: res.headers } as putServiceConfigsApiConfigsServicePutResponse; }; -/** - * Patch service configurations - * @summary Patch Service Configs - */ export type patchServiceConfigsApiConfigsServicePatchResponse200 = { data: PatchServiceConfigsApiConfigsServicePatch200; status: 200; @@ -2243,6 +2359,10 @@ export const getPatchServiceConfigsApiConfigsServicePatchUrl = () => { return `/api/configs/service`; }; +/** + * Patch service configurations + * @summary Patch Service Configs + */ export const patchServiceConfigsApiConfigsServicePatch = async ( patchConfigRequest: PatchConfigRequest, options?: RequestInit, @@ -2260,10 +2380,6 @@ export const patchServiceConfigsApiConfigsServicePatch = async ( return { data, status: res.status, headers: res.headers } as patchServiceConfigsApiConfigsServicePatchResponse; }; -/** - * Read all the workflow configurations - * @summary Read Workflow Configs - */ export type readWorkflowConfigsApiConfigsWorkflowGetResponse200 = { data: WorkflowConfigOutput; status: 200; @@ -2279,6 +2395,10 @@ export const getReadWorkflowConfigsApiConfigsWorkflowGetUrl = () => { return `/api/configs/workflow`; }; +/** + * Read all the workflow configurations + * @summary Read Workflow Configs + */ export const readWorkflowConfigsApiConfigsWorkflowGet = async ( options?: RequestInit, ): Promise => { @@ -2293,10 +2413,6 @@ export const readWorkflowConfigsApiConfigsWorkflowGet = async ( return { data, status: res.status, headers: res.headers } as readWorkflowConfigsApiConfigsWorkflowGetResponse; }; -/** - * Put workflow configurations - * @summary Put Workflow Configs - */ export type putWorkflowConfigsApiConfigsWorkflowPutResponse200 = { data: PutWorkflowConfigsApiConfigsWorkflowPut200; status: 200; @@ -2324,6 +2440,10 @@ export const getPutWorkflowConfigsApiConfigsWorkflowPutUrl = () => { return `/api/configs/workflow`; }; +/** + * Put workflow configurations + * @summary Put Workflow Configs + */ export const putWorkflowConfigsApiConfigsWorkflowPut = async ( putWorkflowRequest: PutWorkflowRequest, options?: RequestInit, @@ -2341,10 +2461,6 @@ export const putWorkflowConfigsApiConfigsWorkflowPut = async ( return { data, status: res.status, headers: res.headers } as putWorkflowConfigsApiConfigsWorkflowPutResponse; }; -/** - * Patch workflow configurations - * @summary Patch Workflow Configs - */ export type patchWorkflowConfigsApiConfigsWorkflowPatchResponse200 = { data: PatchWorkflowConfigsApiConfigsWorkflowPatch200; status: 200; @@ -2372,6 +2488,10 @@ export const getPatchWorkflowConfigsApiConfigsWorkflowPatchUrl = () => { return `/api/configs/workflow`; }; +/** + * Patch workflow configurations + * @summary Patch Workflow Configs + */ export const patchWorkflowConfigsApiConfigsWorkflowPatch = async ( patchConfigRequest: PatchConfigRequest, options?: RequestInit, @@ -2389,10 +2509,6 @@ export const patchWorkflowConfigsApiConfigsWorkflowPatch = async ( return { data, status: res.status, headers: res.headers } as patchWorkflowConfigsApiConfigsWorkflowPatchResponse; }; -/** - * List all backends. - * @summary List Backends - */ export type listBackendsApiConfigsBackendGetResponse200 = { data: ListBackendsResponse; status: 200; @@ -2407,6 +2523,10 @@ export const getListBackendsApiConfigsBackendGetUrl = () => { return `/api/configs/backend`; }; +/** + * List all backends. + * @summary List Backends + */ export const listBackendsApiConfigsBackendGet = async ( options?: RequestInit, ): Promise => { @@ -2421,10 +2541,6 @@ export const listBackendsApiConfigsBackendGet = async ( return { data, status: res.status, headers: res.headers } as listBackendsApiConfigsBackendGetResponse; }; -/** - * Override the config for a specific backend. - * @summary Update Backend - */ export type updateBackendApiConfigsBackendNamePostResponse200 = { data: unknown; status: 200; @@ -2451,6 +2567,10 @@ export const getUpdateBackendApiConfigsBackendNamePostUrl = (name: string) => { return `/api/configs/backend/${name}`; }; +/** + * Override the config for a specific backend. + * @summary Update Backend + */ export const updateBackendApiConfigsBackendNamePost = async ( name: string, postBackendRequest: PostBackendRequest, @@ -2469,10 +2589,6 @@ export const updateBackendApiConfigsBackendNamePost = async ( return { data, status: res.status, headers: res.headers } as updateBackendApiConfigsBackendNamePostResponse; }; -/** - * Get info for a specific backend. - * @summary Get Backend - */ export type getBackendApiConfigsBackendNameGetResponse200 = { data: Backend; status: 200; @@ -2498,6 +2614,10 @@ export const getGetBackendApiConfigsBackendNameGetUrl = (name: string) => { return `/api/configs/backend/${name}`; }; +/** + * Get info for a specific backend. + * @summary Get Backend + */ export const getBackendApiConfigsBackendNameGet = async ( name: string, options?: RequestInit, @@ -2513,10 +2633,6 @@ export const getBackendApiConfigsBackendNameGet = async ( return { data, status: res.status, headers: res.headers } as getBackendApiConfigsBackendNameGetResponse; }; -/** - * Remove a backend. - * @summary Delete Backend - */ export type deleteBackendApiConfigsBackendNameDeleteResponse200 = { data: unknown; status: 200; @@ -2544,6 +2660,10 @@ export const getDeleteBackendApiConfigsBackendNameDeleteUrl = (name: string) => return `/api/configs/backend/${name}`; }; +/** + * Remove a backend. + * @summary Delete Backend + */ export const deleteBackendApiConfigsBackendNameDelete = async ( name: string, deleteBackendRequest: DeleteBackendRequest, @@ -2562,10 +2682,6 @@ export const deleteBackendApiConfigsBackendNameDelete = async ( return { data, status: res.status, headers: res.headers } as deleteBackendApiConfigsBackendNameDeleteResponse; }; -/** - * List all Pools - * @summary List Pools - */ export type listPoolsApiConfigsPoolGetResponse200 = { data: VerbosePoolConfig | EditablePoolConfig; status: 200; @@ -2592,7 +2708,7 @@ export const getListPoolsApiConfigsPoolGetUrl = (params?: ListPoolsApiConfigsPoo Object.entries(params || {}).forEach(([key, value]) => { if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); + normalizedParams.append(key, value === null ? "null" : String(value)); } }); @@ -2601,6 +2717,10 @@ export const getListPoolsApiConfigsPoolGetUrl = (params?: ListPoolsApiConfigsPoo return stringifiedParams.length > 0 ? `/api/configs/pool?${stringifiedParams}` : `/api/configs/pool`; }; +/** + * List all Pools + * @summary List Pools + */ export const listPoolsApiConfigsPoolGet = async ( params?: ListPoolsApiConfigsPoolGetParams, options?: RequestInit, @@ -2616,10 +2736,6 @@ export const listPoolsApiConfigsPoolGet = async ( return { data, status: res.status, headers: res.headers } as listPoolsApiConfigsPoolGetResponse; }; -/** - * Put Pool configurations - * @summary Put Pools - */ export type putPoolsApiConfigsPoolPutResponse200 = { data: unknown; status: 200; @@ -2645,6 +2761,10 @@ export const getPutPoolsApiConfigsPoolPutUrl = () => { return `/api/configs/pool`; }; +/** + * Put Pool configurations + * @summary Put Pools + */ export const putPoolsApiConfigsPoolPut = async ( putPoolsRequest: PutPoolsRequest, options?: RequestInit, @@ -2662,13 +2782,6 @@ export const putPoolsApiConfigsPoolPut = async ( return { data, status: res.status, headers: res.headers } as putPoolsApiConfigsPoolPutResponse; }; -/** - * Read Pool configuration - -Return type Any to prevent unwanted artifacts between Pool and PoolEditable outputs -Should return Pool or PoolEditable objects - * @summary Read Pool - */ export type readPoolApiConfigsPoolNameGetResponse200 = { data: PoolOutput | PoolEditable; status: 200; @@ -2695,7 +2808,7 @@ export const getReadPoolApiConfigsPoolNameGetUrl = (name: string, params?: ReadP Object.entries(params || {}).forEach(([key, value]) => { if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); + normalizedParams.append(key, value === null ? "null" : String(value)); } }); @@ -2704,6 +2817,13 @@ export const getReadPoolApiConfigsPoolNameGetUrl = (name: string, params?: ReadP return stringifiedParams.length > 0 ? `/api/configs/pool/${name}?${stringifiedParams}` : `/api/configs/pool/${name}`; }; +/** + * Read Pool configuration + * + * Return type Any to prevent unwanted artifacts between Pool and PoolEditable outputs + * Should return Pool or PoolEditable objects + * @summary Read Pool + */ export const readPoolApiConfigsPoolNameGet = async ( name: string, params?: ReadPoolApiConfigsPoolNameGetParams, @@ -2720,10 +2840,6 @@ export const readPoolApiConfigsPoolNameGet = async ( return { data, status: res.status, headers: res.headers } as readPoolApiConfigsPoolNameGetResponse; }; -/** - * Put Pool configurations - * @summary Put Pool - */ export type putPoolApiConfigsPoolNamePutResponse200 = { data: unknown; status: 200; @@ -2749,6 +2865,10 @@ export const getPutPoolApiConfigsPoolNamePutUrl = (name: string) => { return `/api/configs/pool/${name}`; }; +/** + * Put Pool configurations + * @summary Put Pool + */ export const putPoolApiConfigsPoolNamePut = async ( name: string, putPoolRequest: PutPoolRequest, @@ -2767,10 +2887,6 @@ export const putPoolApiConfigsPoolNamePut = async ( return { data, status: res.status, headers: res.headers } as putPoolApiConfigsPoolNamePutResponse; }; -/** - * Patch Pool configurations - * @summary Patch Pool - */ export type patchPoolApiConfigsPoolNamePatchResponse200 = { data: unknown; status: 200; @@ -2796,6 +2912,10 @@ export const getPatchPoolApiConfigsPoolNamePatchUrl = (name: string) => { return `/api/configs/pool/${name}`; }; +/** + * Patch Pool configurations + * @summary Patch Pool + */ export const patchPoolApiConfigsPoolNamePatch = async ( name: string, patchPoolRequest: PatchPoolRequest, @@ -2814,10 +2934,6 @@ export const patchPoolApiConfigsPoolNamePatch = async ( return { data, status: res.status, headers: res.headers } as patchPoolApiConfigsPoolNamePatchResponse; }; -/** - * Delete Pool configurations - * @summary Delete Pool - */ export type deletePoolApiConfigsPoolNameDeleteResponse200 = { data: unknown; status: 200; @@ -2843,6 +2959,10 @@ export const getDeletePoolApiConfigsPoolNameDeleteUrl = (name: string) => { return `/api/configs/pool/${name}`; }; +/** + * Delete Pool configurations + * @summary Delete Pool + */ export const deletePoolApiConfigsPoolNameDelete = async ( name: string, configsRequest: ConfigsRequest, @@ -2861,10 +2981,6 @@ export const deletePoolApiConfigsPoolNameDelete = async ( return { data, status: res.status, headers: res.headers } as deletePoolApiConfigsPoolNameDeleteResponse; }; -/** - * Rename Pool - * @summary Rename Pool - */ export type renamePoolApiConfigsPoolNameRenamePutResponse200 = { data: unknown; status: 200; @@ -2890,6 +3006,10 @@ export const getRenamePoolApiConfigsPoolNameRenamePutUrl = (name: string) => { return `/api/configs/pool/${name}/rename`; }; +/** + * Rename Pool + * @summary Rename Pool + */ export const renamePoolApiConfigsPoolNameRenamePut = async ( name: string, renamePoolRequest: RenamePoolRequest, @@ -2908,10 +3028,6 @@ export const renamePoolApiConfigsPoolNameRenamePut = async ( return { data, status: res.status, headers: res.headers } as renamePoolApiConfigsPoolNameRenamePutResponse; }; -/** - * List all Platforms - * @summary List Platforms In Pool - */ export type listPlatformsInPoolApiConfigsPoolNamePlatformGetResponse200 = { data: ListPlatformsInPoolApiConfigsPoolNamePlatformGet200; status: 200; @@ -2943,7 +3059,7 @@ export const getListPlatformsInPoolApiConfigsPoolNamePlatformGetUrl = ( Object.entries(params || {}).forEach(([key, value]) => { if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); + normalizedParams.append(key, value === null ? "null" : String(value)); } }); @@ -2954,6 +3070,10 @@ export const getListPlatformsInPoolApiConfigsPoolNamePlatformGetUrl = ( : `/api/configs/pool/${name}/platform`; }; +/** + * List all Platforms + * @summary List Platforms In Pool + */ export const listPlatformsInPoolApiConfigsPoolNamePlatformGet = async ( name: string, params?: ListPlatformsInPoolApiConfigsPoolNamePlatformGetParams, @@ -2970,10 +3090,6 @@ export const listPlatformsInPoolApiConfigsPoolNamePlatformGet = async ( return { data, status: res.status, headers: res.headers } as listPlatformsInPoolApiConfigsPoolNamePlatformGetResponse; }; -/** - * Read Platform - * @summary Read Platform In Pool - */ export type readPlatformInPoolApiConfigsPoolNamePlatformPlatformNameGetResponse200 = { data: PlatformMinimal | PlatformEditable | PlatformOutput; status: 200; @@ -3006,7 +3122,7 @@ export const getReadPlatformInPoolApiConfigsPoolNamePlatformPlatformNameGetUrl = Object.entries(params || {}).forEach(([key, value]) => { if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); + normalizedParams.append(key, value === null ? "null" : String(value)); } }); @@ -3017,6 +3133,10 @@ export const getReadPlatformInPoolApiConfigsPoolNamePlatformPlatformNameGetUrl = : `/api/configs/pool/${name}/platform/${platformName}`; }; +/** + * Read Platform + * @summary Read Platform In Pool + */ export const readPlatformInPoolApiConfigsPoolNamePlatformPlatformNameGet = async ( name: string, platformName: string, @@ -3043,10 +3163,6 @@ export const readPlatformInPoolApiConfigsPoolNamePlatformPlatformNameGet = async } as readPlatformInPoolApiConfigsPoolNamePlatformPlatformNameGetResponse; }; -/** - * Put Platform configurations - * @summary Put Platform In Pool - */ export type putPlatformInPoolApiConfigsPoolNamePlatformPlatformNamePutResponse200 = { data: unknown; status: 200; @@ -3077,6 +3193,10 @@ export const getPutPlatformInPoolApiConfigsPoolNamePlatformPlatformNamePutUrl = return `/api/configs/pool/${name}/platform/${platformName}`; }; +/** + * Put Platform configurations + * @summary Put Platform In Pool + */ export const putPlatformInPoolApiConfigsPoolNamePlatformPlatformNamePut = async ( name: string, platformName: string, @@ -3100,10 +3220,6 @@ export const putPlatformInPoolApiConfigsPoolNamePlatformPlatformNamePut = async } as putPlatformInPoolApiConfigsPoolNamePlatformPlatformNamePutResponse; }; -/** - * Rename Platform - * @summary Rename Platform In Pool - */ export type renamePlatformInPoolApiConfigsPoolNamePlatformPlatformNameRenamePutResponse200 = { data: unknown; status: 200; @@ -3134,6 +3250,10 @@ export const getRenamePlatformInPoolApiConfigsPoolNamePlatformPlatformNameRename return `/api/configs/pool/${name}/platform/${platformName}/rename`; }; +/** + * Rename Platform + * @summary Rename Platform In Pool + */ export const renamePlatformInPoolApiConfigsPoolNamePlatformPlatformNameRenamePut = async ( name: string, platformName: string, @@ -3162,10 +3282,6 @@ export const renamePlatformInPoolApiConfigsPoolNamePlatformPlatformNameRenamePut } as renamePlatformInPoolApiConfigsPoolNamePlatformPlatformNameRenamePutResponse; }; -/** - * List all Pod Template configurations - * @summary List Pod Templates - */ export type listPodTemplatesApiConfigsPodTemplateGetResponse200 = { data: ListPodTemplatesApiConfigsPodTemplateGet200; status: 200; @@ -3181,6 +3297,10 @@ export const getListPodTemplatesApiConfigsPodTemplateGetUrl = () => { return `/api/configs/pod_template`; }; +/** + * List all Pod Template configurations + * @summary List Pod Templates + */ export const listPodTemplatesApiConfigsPodTemplateGet = async ( options?: RequestInit, ): Promise => { @@ -3195,10 +3315,6 @@ export const listPodTemplatesApiConfigsPodTemplateGet = async ( return { data, status: res.status, headers: res.headers } as listPodTemplatesApiConfigsPodTemplateGetResponse; }; -/** - * Set Dict of Pod Templates configurations - * @summary Put Pod Templates - */ export type putPodTemplatesApiConfigsPodTemplatePutResponse200 = { data: unknown; status: 200; @@ -3226,6 +3342,10 @@ export const getPutPodTemplatesApiConfigsPodTemplatePutUrl = () => { return `/api/configs/pod_template`; }; +/** + * Set Dict of Pod Templates configurations + * @summary Put Pod Templates + */ export const putPodTemplatesApiConfigsPodTemplatePut = async ( putPodTemplatesRequest: PutPodTemplatesRequest, options?: RequestInit, @@ -3243,10 +3363,6 @@ export const putPodTemplatesApiConfigsPodTemplatePut = async ( return { data, status: res.status, headers: res.headers } as putPodTemplatesApiConfigsPodTemplatePutResponse; }; -/** - * Read Pod Template configurations - * @summary Read Pod Template - */ export type readPodTemplateApiConfigsPodTemplateNameGetResponse200 = { data: ReadPodTemplateApiConfigsPodTemplateNameGet200; status: 200; @@ -3274,6 +3390,10 @@ export const getReadPodTemplateApiConfigsPodTemplateNameGetUrl = (name: string) return `/api/configs/pod_template/${name}`; }; +/** + * Read Pod Template configurations + * @summary Read Pod Template + */ export const readPodTemplateApiConfigsPodTemplateNameGet = async ( name: string, options?: RequestInit, @@ -3289,10 +3409,6 @@ export const readPodTemplateApiConfigsPodTemplateNameGet = async ( return { data, status: res.status, headers: res.headers } as readPodTemplateApiConfigsPodTemplateNameGetResponse; }; -/** - * Put Pod Template configurations - * @summary Put Pod Template - */ export type putPodTemplateApiConfigsPodTemplateNamePutResponse200 = { data: unknown; status: 200; @@ -3320,6 +3436,10 @@ export const getPutPodTemplateApiConfigsPodTemplateNamePutUrl = (name: string) = return `/api/configs/pod_template/${name}`; }; +/** + * Put Pod Template configurations + * @summary Put Pod Template + */ export const putPodTemplateApiConfigsPodTemplateNamePut = async ( name: string, putPodTemplateRequest: PutPodTemplateRequest, @@ -3338,10 +3458,6 @@ export const putPodTemplateApiConfigsPodTemplateNamePut = async ( return { data, status: res.status, headers: res.headers } as putPodTemplateApiConfigsPodTemplateNamePutResponse; }; -/** - * Delete Pod Template configurations - * @summary Delete Pod Template - */ export type deletePodTemplateApiConfigsPodTemplateNameDeleteResponse200 = { data: unknown; status: 200; @@ -3369,6 +3485,10 @@ export const getDeletePodTemplateApiConfigsPodTemplateNameDeleteUrl = (name: str return `/api/configs/pod_template/${name}`; }; +/** + * Delete Pod Template configurations + * @summary Delete Pod Template + */ export const deletePodTemplateApiConfigsPodTemplateNameDelete = async ( name: string, configsRequest: ConfigsRequest, @@ -3387,10 +3507,6 @@ export const deletePodTemplateApiConfigsPodTemplateNameDelete = async ( return { data, status: res.status, headers: res.headers } as deletePodTemplateApiConfigsPodTemplateNameDeleteResponse; }; -/** - * List all Group Template configurations - * @summary List Group Templates - */ export type listGroupTemplatesApiConfigsGroupTemplateGetResponse200 = { data: ListGroupTemplatesApiConfigsGroupTemplateGet200; status: 200; @@ -3407,6 +3523,10 @@ export const getListGroupTemplatesApiConfigsGroupTemplateGetUrl = () => { return `/api/configs/group_template`; }; +/** + * List all Group Template configurations + * @summary List Group Templates + */ export const listGroupTemplatesApiConfigsGroupTemplateGet = async ( options?: RequestInit, ): Promise => { @@ -3421,10 +3541,6 @@ export const listGroupTemplatesApiConfigsGroupTemplateGet = async ( return { data, status: res.status, headers: res.headers } as listGroupTemplatesApiConfigsGroupTemplateGetResponse; }; -/** - * Set Dict of Group Templates configurations - * @summary Put Group Templates - */ export type putGroupTemplatesApiConfigsGroupTemplatePutResponse200 = { data: unknown; status: 200; @@ -3452,6 +3568,10 @@ export const getPutGroupTemplatesApiConfigsGroupTemplatePutUrl = () => { return `/api/configs/group_template`; }; +/** + * Set Dict of Group Templates configurations + * @summary Put Group Templates + */ export const putGroupTemplatesApiConfigsGroupTemplatePut = async ( putGroupTemplatesRequest: PutGroupTemplatesRequest, options?: RequestInit, @@ -3469,10 +3589,6 @@ export const putGroupTemplatesApiConfigsGroupTemplatePut = async ( return { data, status: res.status, headers: res.headers } as putGroupTemplatesApiConfigsGroupTemplatePutResponse; }; -/** - * Read Group Template configurations - * @summary Read Group Template - */ export type readGroupTemplateApiConfigsGroupTemplateNameGetResponse200 = { data: ReadGroupTemplateApiConfigsGroupTemplateNameGet200; status: 200; @@ -3500,6 +3616,10 @@ export const getReadGroupTemplateApiConfigsGroupTemplateNameGetUrl = (name: stri return `/api/configs/group_template/${name}`; }; +/** + * Read Group Template configurations + * @summary Read Group Template + */ export const readGroupTemplateApiConfigsGroupTemplateNameGet = async ( name: string, options?: RequestInit, @@ -3515,10 +3635,6 @@ export const readGroupTemplateApiConfigsGroupTemplateNameGet = async ( return { data, status: res.status, headers: res.headers } as readGroupTemplateApiConfigsGroupTemplateNameGetResponse; }; -/** - * Put Group Template configurations - * @summary Put Group Template - */ export type putGroupTemplateApiConfigsGroupTemplateNamePutResponse200 = { data: unknown; status: 200; @@ -3546,6 +3662,10 @@ export const getPutGroupTemplateApiConfigsGroupTemplateNamePutUrl = (name: strin return `/api/configs/group_template/${name}`; }; +/** + * Put Group Template configurations + * @summary Put Group Template + */ export const putGroupTemplateApiConfigsGroupTemplateNamePut = async ( name: string, putGroupTemplateRequest: PutGroupTemplateRequest, @@ -3564,10 +3684,6 @@ export const putGroupTemplateApiConfigsGroupTemplateNamePut = async ( return { data, status: res.status, headers: res.headers } as putGroupTemplateApiConfigsGroupTemplateNamePutResponse; }; -/** - * Delete Group Template configurations - * @summary Delete Group Template - */ export type deleteGroupTemplateApiConfigsGroupTemplateNameDeleteResponse200 = { data: unknown; status: 200; @@ -3595,6 +3711,10 @@ export const getDeleteGroupTemplateApiConfigsGroupTemplateNameDeleteUrl = (name: return `/api/configs/group_template/${name}`; }; +/** + * Delete Group Template configurations + * @summary Delete Group Template + */ export const deleteGroupTemplateApiConfigsGroupTemplateNameDelete = async ( name: string, configsRequest: ConfigsRequest, @@ -3617,10 +3737,6 @@ export const deleteGroupTemplateApiConfigsGroupTemplateNameDelete = async ( } as deleteGroupTemplateApiConfigsGroupTemplateNameDeleteResponse; }; -/** - * List all Resource Validation configurations - * @summary List Resource Validations - */ export type listResourceValidationsApiConfigsResourceValidationGetResponse200 = { data: ListResourceValidationsApiConfigsResourceValidationGet200; status: 200; @@ -3637,6 +3753,10 @@ export const getListResourceValidationsApiConfigsResourceValidationGetUrl = () = return `/api/configs/resource_validation`; }; +/** + * List all Resource Validation configurations + * @summary List Resource Validations + */ export const listResourceValidationsApiConfigsResourceValidationGet = async ( options?: RequestInit, ): Promise => { @@ -3655,10 +3775,6 @@ export const listResourceValidationsApiConfigsResourceValidationGet = async ( } as listResourceValidationsApiConfigsResourceValidationGetResponse; }; -/** - * Put Resource Validation configurations - * @summary Put Resource Validations - */ export type putResourceValidationsApiConfigsResourceValidationPutResponse200 = { data: unknown; status: 200; @@ -3686,6 +3802,10 @@ export const getPutResourceValidationsApiConfigsResourceValidationPutUrl = () => return `/api/configs/resource_validation`; }; +/** + * Put Resource Validation configurations + * @summary Put Resource Validations + */ export const putResourceValidationsApiConfigsResourceValidationPut = async ( putResourceValidationsRequest: PutResourceValidationsRequest, options?: RequestInit, @@ -3707,10 +3827,6 @@ export const putResourceValidationsApiConfigsResourceValidationPut = async ( } as putResourceValidationsApiConfigsResourceValidationPutResponse; }; -/** - * Read Resource Validation configurations - * @summary Read Resource Validation - */ export type readResourceValidationApiConfigsResourceValidationNameGetResponse200 = { data: ResourceAssertion[]; status: 200; @@ -3738,6 +3854,10 @@ export const getReadResourceValidationApiConfigsResourceValidationNameGetUrl = ( return `/api/configs/resource_validation/${name}`; }; +/** + * Read Resource Validation configurations + * @summary Read Resource Validation + */ export const readResourceValidationApiConfigsResourceValidationNameGet = async ( name: string, options?: RequestInit, @@ -3757,10 +3877,6 @@ export const readResourceValidationApiConfigsResourceValidationNameGet = async ( } as readResourceValidationApiConfigsResourceValidationNameGetResponse; }; -/** - * Put Resource Validation configurations - * @summary Put Resource Validation - */ export type putResourceValidationApiConfigsResourceValidationNamePutResponse200 = { data: unknown; status: 200; @@ -3788,6 +3904,10 @@ export const getPutResourceValidationApiConfigsResourceValidationNamePutUrl = (n return `/api/configs/resource_validation/${name}`; }; +/** + * Put Resource Validation configurations + * @summary Put Resource Validation + */ export const putResourceValidationApiConfigsResourceValidationNamePut = async ( name: string, putResourceValidationRequest: PutResourceValidationRequest, @@ -3810,10 +3930,6 @@ export const putResourceValidationApiConfigsResourceValidationNamePut = async ( } as putResourceValidationApiConfigsResourceValidationNamePutResponse; }; -/** - * Delete Resource Validation configurations - * @summary Delete Resource Validation - */ export type deleteResourceValidationApiConfigsResourceValidationNameDeleteResponse200 = { data: unknown; status: 200; @@ -3841,6 +3957,10 @@ export const getDeleteResourceValidationApiConfigsResourceValidationNameDeleteUr return `/api/configs/resource_validation/${name}`; }; +/** + * Delete Resource Validation configurations + * @summary Delete Resource Validation + */ export const deleteResourceValidationApiConfigsResourceValidationNameDelete = async ( name: string, configsRequest: ConfigsRequest, @@ -3865,10 +3985,6 @@ export const deleteResourceValidationApiConfigsResourceValidationNameDelete = as } as deleteResourceValidationApiConfigsResourceValidationNameDeleteResponse; }; -/** - * List all Roles - * @summary List Roles - */ export type listRolesApiConfigsRoleGetResponse200 = { data: RoleOutput[]; status: 200; @@ -3883,6 +3999,10 @@ export const getListRolesApiConfigsRoleGetUrl = () => { return `/api/configs/role`; }; +/** + * List all Roles + * @summary List Roles + */ export const listRolesApiConfigsRoleGet = async ( options?: RequestInit, ): Promise => { @@ -3897,10 +4017,6 @@ export const listRolesApiConfigsRoleGet = async ( return { data, status: res.status, headers: res.headers } as listRolesApiConfigsRoleGetResponse; }; -/** - * Put Roles - * @summary Put Roles - */ export type putRolesApiConfigsRolePutResponse200 = { data: unknown; status: 200; @@ -3926,6 +4042,10 @@ export const getPutRolesApiConfigsRolePutUrl = () => { return `/api/configs/role`; }; +/** + * Put Roles + * @summary Put Roles + */ export const putRolesApiConfigsRolePut = async ( putRolesRequest: PutRolesRequest, options?: RequestInit, @@ -3943,10 +4063,6 @@ export const putRolesApiConfigsRolePut = async ( return { data, status: res.status, headers: res.headers } as putRolesApiConfigsRolePutResponse; }; -/** - * Read Role - * @summary Read Role - */ export type readRoleApiConfigsRoleNameGetResponse200 = { data: RoleOutput; status: 200; @@ -3972,6 +4088,10 @@ export const getReadRoleApiConfigsRoleNameGetUrl = (name: string) => { return `/api/configs/role/${name}`; }; +/** + * Read Role + * @summary Read Role + */ export const readRoleApiConfigsRoleNameGet = async ( name: string, options?: RequestInit, @@ -3987,10 +4107,6 @@ export const readRoleApiConfigsRoleNameGet = async ( return { data, status: res.status, headers: res.headers } as readRoleApiConfigsRoleNameGetResponse; }; -/** - * Patch Role configurations - * @summary Put Role - */ export type putRoleApiConfigsRoleNamePutResponse200 = { data: unknown; status: 200; @@ -4016,6 +4132,10 @@ export const getPutRoleApiConfigsRoleNamePutUrl = (name: string) => { return `/api/configs/role/${name}`; }; +/** + * Patch Role configurations + * @summary Put Role + */ export const putRoleApiConfigsRoleNamePut = async ( name: string, putRoleRequest: PutRoleRequest, @@ -4034,10 +4154,6 @@ export const putRoleApiConfigsRoleNamePut = async ( return { data, status: res.status, headers: res.headers } as putRoleApiConfigsRoleNamePutResponse; }; -/** - * Delete Role - * @summary Delete Role - */ export type deleteRoleApiConfigsRoleNameDeleteResponse200 = { data: unknown; status: 200; @@ -4063,6 +4179,10 @@ export const getDeleteRoleApiConfigsRoleNameDeleteUrl = (name: string) => { return `/api/configs/role/${name}`; }; +/** + * Delete Role + * @summary Delete Role + */ export const deleteRoleApiConfigsRoleNameDelete = async ( name: string, configsRequest: ConfigsRequest, @@ -4081,10 +4201,6 @@ export const deleteRoleApiConfigsRoleNameDelete = async ( return { data, status: res.status, headers: res.headers } as deleteRoleApiConfigsRoleNameDeleteResponse; }; -/** - * List all backend test configurations - * @summary List Backend Tests - */ export type listBackendTestsApiConfigsBackendTestGetResponse200 = { data: ListBackendTestsApiConfigsBackendTestGet200; status: 200; @@ -4100,6 +4216,10 @@ export const getListBackendTestsApiConfigsBackendTestGetUrl = () => { return `/api/configs/backend_test`; }; +/** + * List all backend test configurations + * @summary List Backend Tests + */ export const listBackendTestsApiConfigsBackendTestGet = async ( options?: RequestInit, ): Promise => { @@ -4114,10 +4234,6 @@ export const listBackendTestsApiConfigsBackendTestGet = async ( return { data, status: res.status, headers: res.headers } as listBackendTestsApiConfigsBackendTestGetResponse; }; -/** - * Put backend test configurations - * @summary Put Backend Tests - */ export type putBackendTestsApiConfigsBackendTestPutResponse200 = { data: unknown; status: 200; @@ -4145,6 +4261,10 @@ export const getPutBackendTestsApiConfigsBackendTestPutUrl = () => { return `/api/configs/backend_test`; }; +/** + * Put backend test configurations + * @summary Put Backend Tests + */ export const putBackendTestsApiConfigsBackendTestPut = async ( putBackendTestsRequest: PutBackendTestsRequest, options?: RequestInit, @@ -4162,10 +4282,6 @@ export const putBackendTestsApiConfigsBackendTestPut = async ( return { data, status: res.status, headers: res.headers } as putBackendTestsApiConfigsBackendTestPutResponse; }; -/** - * Read backend test configuration - * @summary Read Backend Test - */ export type readBackendTestApiConfigsBackendTestNameGetResponse200 = { data: BackendTests; status: 200; @@ -4193,6 +4309,10 @@ export const getReadBackendTestApiConfigsBackendTestNameGetUrl = (name: string) return `/api/configs/backend_test/${name}`; }; +/** + * Read backend test configuration + * @summary Read Backend Test + */ export const readBackendTestApiConfigsBackendTestNameGet = async ( name: string, options?: RequestInit, @@ -4208,10 +4328,6 @@ export const readBackendTestApiConfigsBackendTestNameGet = async ( return { data, status: res.status, headers: res.headers } as readBackendTestApiConfigsBackendTestNameGetResponse; }; -/** - * Put backend test configuration - * @summary Put Backend Test - */ export type putBackendTestApiConfigsBackendTestNamePutResponse200 = { data: unknown; status: 200; @@ -4239,6 +4355,10 @@ export const getPutBackendTestApiConfigsBackendTestNamePutUrl = (name: string) = return `/api/configs/backend_test/${name}`; }; +/** + * Put backend test configuration + * @summary Put Backend Test + */ export const putBackendTestApiConfigsBackendTestNamePut = async ( name: string, putBackendTestRequest: PutBackendTestRequest, @@ -4257,10 +4377,6 @@ export const putBackendTestApiConfigsBackendTestNamePut = async ( return { data, status: res.status, headers: res.headers } as putBackendTestApiConfigsBackendTestNamePutResponse; }; -/** - * Patch backend test configuration - * @summary Patch Backend Test - */ export type patchBackendTestApiConfigsBackendTestNamePatchResponse200 = { data: unknown; status: 200; @@ -4288,6 +4404,10 @@ export const getPatchBackendTestApiConfigsBackendTestNamePatchUrl = (name: strin return `/api/configs/backend_test/${name}`; }; +/** + * Patch backend test configuration + * @summary Patch Backend Test + */ export const patchBackendTestApiConfigsBackendTestNamePatch = async ( name: string, patchBackendTestRequest: PatchBackendTestRequest, @@ -4306,10 +4426,6 @@ export const patchBackendTestApiConfigsBackendTestNamePatch = async ( return { data, status: res.status, headers: res.headers } as patchBackendTestApiConfigsBackendTestNamePatchResponse; }; -/** - * Delete test configuration - * @summary Delete Backend Test - */ export type deleteBackendTestApiConfigsBackendTestNameDeleteResponse200 = { data: unknown; status: 200; @@ -4337,6 +4453,10 @@ export const getDeleteBackendTestApiConfigsBackendTestNameDeleteUrl = (name: str return `/api/configs/backend_test/${name}`; }; +/** + * Delete test configuration + * @summary Delete Backend Test + */ export const deleteBackendTestApiConfigsBackendTestNameDelete = async ( name: string, configsRequest: ConfigsRequest, @@ -4355,10 +4475,6 @@ export const deleteBackendTestApiConfigsBackendTestNameDelete = async ( return { data, status: res.status, headers: res.headers } as deleteBackendTestApiConfigsBackendTestNameDeleteResponse; }; -/** - * List history of all configs - * @summary Get Configs History - */ export type getConfigsHistoryApiConfigsHistoryGetResponse200 = { data: GetConfigsHistoryResponse; status: 200; @@ -4388,13 +4504,13 @@ export const getGetConfigsHistoryApiConfigsHistoryGetUrl = (params?: GetConfigsH if (Array.isArray(value) && explodeParameters.includes(key)) { value.forEach((v) => { - normalizedParams.append(key, v === null ? "null" : v.toString()); + normalizedParams.append(key, v === null ? "null" : String(v)); }); return; } if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); + normalizedParams.append(key, value === null ? "null" : String(value)); } }); @@ -4403,6 +4519,10 @@ export const getGetConfigsHistoryApiConfigsHistoryGetUrl = (params?: GetConfigsH return stringifiedParams.length > 0 ? `/api/configs/history?${stringifiedParams}` : `/api/configs/history`; }; +/** + * List history of all configs + * @summary Get Configs History + */ export const getConfigsHistoryApiConfigsHistoryGet = async ( params?: GetConfigsHistoryApiConfigsHistoryGetParams, options?: RequestInit, @@ -4418,10 +4538,6 @@ export const getConfigsHistoryApiConfigsHistoryGet = async ( return { data, status: res.status, headers: res.headers } as getConfigsHistoryApiConfigsHistoryGetResponse; }; -/** - * Roll back a config to a particular revision. - * @summary Rollback Config - */ export type rollbackConfigApiConfigsHistoryRollbackPostResponse200 = { data: unknown; status: 200; @@ -4449,6 +4565,10 @@ export const getRollbackConfigApiConfigsHistoryRollbackPostUrl = () => { return `/api/configs/history/rollback`; }; +/** + * Roll back a config to a particular revision. + * @summary Rollback Config + */ export const rollbackConfigApiConfigsHistoryRollbackPost = async ( rollbackConfigRequest: RollbackConfigRequest, options?: RequestInit, @@ -4466,18 +4586,6 @@ export const rollbackConfigApiConfigsHistoryRollbackPost = async ( return { data, status: res.status, headers: res.headers } as rollbackConfigApiConfigsHistoryRollbackPostResponse; }; -/** - * Delete a specific config history revision. This performs a soft delete of the revision. - -Args: - config_type: Type of config to delete - revision: Revision number to delete (must be greater than 0) - username: Username of the person performing the delete - -Raises: - OSMOUserError: If the revision doesn't exist or is the current revision - * @summary Delete Config History Revision - */ export type deleteConfigHistoryRevisionApiConfigsHistoryConfigTypeRevisionRevisionDeleteResponse200 = { data: unknown; status: 200; @@ -4508,6 +4616,18 @@ export const getDeleteConfigHistoryRevisionApiConfigsHistoryConfigTypeRevisionRe return `/api/configs/history/${configType}/revision/${revision}`; }; +/** + * Delete a specific config history revision. This performs a soft delete of the revision. + * + * Args: + * config_type: Type of config to delete + * revision: Revision number to delete (must be greater than 0) + * username: Username of the person performing the delete + * + * Raises: + * OSMOUserError: If the revision doesn't exist or is the current revision + * @summary Delete Config History Revision + */ export const deleteConfigHistoryRevisionApiConfigsHistoryConfigTypeRevisionRevisionDelete = async ( configType: string, revision: number, @@ -4533,19 +4653,6 @@ export const deleteConfigHistoryRevisionApiConfigsHistoryConfigTypeRevisionRevis } as deleteConfigHistoryRevisionApiConfigsHistoryConfigTypeRevisionRevisionDeleteResponse; }; -/** - * Update tags for a specific config history revision. - -Args: - config_type: Type of config to update - revision: Revision number to update (must be greater than 0) - request: Request containing tags to add and delete - username: Username of the person performing the update - -Raises: - OSMOUserError: If the revision doesn't exist or is invalid - * @summary Update Config History Tags - */ export type updateConfigHistoryTagsApiConfigsHistoryConfigTypeRevisionRevisionTagsPostResponse200 = { data: unknown; status: 200; @@ -4576,6 +4683,19 @@ export const getUpdateConfigHistoryTagsApiConfigsHistoryConfigTypeRevisionRevisi return `/api/configs/history/${configType}/revision/${revision}/tags`; }; +/** + * Update tags for a specific config history revision. + * + * Args: + * config_type: Type of config to update + * revision: Revision number to update (must be greater than 0) + * request: Request containing tags to add and delete + * username: Username of the person performing the update + * + * Raises: + * OSMOUserError: If the revision doesn't exist or is invalid + * @summary Update Config History Tags + */ export const updateConfigHistoryTagsApiConfigsHistoryConfigTypeRevisionRevisionTagsPost = async ( configType: string, revision: number, @@ -4604,21 +4724,6 @@ export const updateConfigHistoryTagsApiConfigsHistoryConfigTypeRevisionRevisionT } as updateConfigHistoryTagsApiConfigsHistoryConfigTypeRevisionRevisionTagsPostResponse; }; -/** - * Returns two config revisions, similar to -GET /api/configs/history/{config_type}/revision/{revision}, but with obfuscated secret strings -that say if a secret string is changed. Intended for use with the `diff` command. - -Args: - request: Request containing config type and revisions to compare - -Returns: - ConfigDiffResponse containing the two revisions - -Raises: - OSMOUserError: If either revision doesn't exist or is invalid - * @summary Get Config Diff - */ export type getConfigDiffApiConfigsDiffGetResponse200 = { data: ConfigDiffResponse; status: 200; @@ -4645,7 +4750,7 @@ export const getGetConfigDiffApiConfigsDiffGetUrl = (params: GetConfigDiffApiCon Object.entries(params || {}).forEach(([key, value]) => { if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); + normalizedParams.append(key, value === null ? "null" : String(value)); } }); @@ -4654,6 +4759,21 @@ export const getGetConfigDiffApiConfigsDiffGetUrl = (params: GetConfigDiffApiCon return stringifiedParams.length > 0 ? `/api/configs/diff?${stringifiedParams}` : `/api/configs/diff`; }; +/** + * Returns two config revisions, similar to + * GET /api/configs/history/{config_type}/revision/{revision}, but with obfuscated secret strings + * that say if a secret string is changed. Intended for use with the `diff` command. + * + * Args: + * request: Request containing config type and revisions to compare + * + * Returns: + * ConfigDiffResponse containing the two revisions + * + * Raises: + * OSMOUserError: If either revision doesn't exist or is invalid + * @summary Get Config Diff + */ export const getConfigDiffApiConfigsDiffGet = async ( params: GetConfigDiffApiConfigsDiffGetParams, options?: RequestInit, @@ -4669,12 +4789,6 @@ export const getConfigDiffApiConfigsDiffGet = async ( return { data, status: res.status, headers: res.headers } as getConfigDiffApiConfigsDiffGetResponse; }; -/** - * API to fetch for a new access token using a refresh token. - -Deprecated: Use POST /api/auth/jwt/refresh_token instead. - * @summary Get New Jwt Token - */ export type getNewJwtTokenApiAuthJwtRefreshTokenGetResponse200 = { data: JwtTokenResponse; status: 200; @@ -4705,7 +4819,7 @@ export const getGetNewJwtTokenApiAuthJwtRefreshTokenGetUrl = ( Object.entries(params || {}).forEach(([key, value]) => { if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); + normalizedParams.append(key, value === null ? "null" : String(value)); } }); @@ -4716,6 +4830,12 @@ export const getGetNewJwtTokenApiAuthJwtRefreshTokenGetUrl = ( : `/api/auth/jwt/refresh_token`; }; +/** + * API to fetch for a new access token using a refresh token. + * + * Deprecated: Use POST /api/auth/jwt/refresh_token instead. + * @summary Get New Jwt Token + */ export const getNewJwtTokenApiAuthJwtRefreshTokenGet = async ( params: GetNewJwtTokenApiAuthJwtRefreshTokenGetParams, options?: RequestInit, @@ -4731,10 +4851,6 @@ export const getNewJwtTokenApiAuthJwtRefreshTokenGet = async ( return { data, status: res.status, headers: res.headers } as getNewJwtTokenApiAuthJwtRefreshTokenGetResponse; }; -/** - * API to fetch for a new access token using a refresh token. - * @summary Post New Jwt Token - */ export type postNewJwtTokenApiAuthJwtRefreshTokenPostResponse200 = { data: JwtTokenResponse; status: 200; @@ -4765,7 +4881,7 @@ export const getPostNewJwtTokenApiAuthJwtRefreshTokenPostUrl = ( Object.entries(params || {}).forEach(([key, value]) => { if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); + normalizedParams.append(key, value === null ? "null" : String(value)); } }); @@ -4776,6 +4892,10 @@ export const getPostNewJwtTokenApiAuthJwtRefreshTokenPostUrl = ( : `/api/auth/jwt/refresh_token`; }; +/** + * API to fetch for a new access token using a refresh token. + * @summary Post New Jwt Token + */ export const postNewJwtTokenApiAuthJwtRefreshTokenPost = async ( tokenRequest: TokenRequest, params: PostNewJwtTokenApiAuthJwtRefreshTokenPostParams, @@ -4794,12 +4914,6 @@ export const postNewJwtTokenApiAuthJwtRefreshTokenPost = async ( return { data, status: res.status, headers: res.headers } as postNewJwtTokenApiAuthJwtRefreshTokenPostResponse; }; -/** - * API to create a new jwt token from an access token. - -Deprecated: Use POST /api/auth/jwt/access_token instead. - * @summary Get Jwt Token From Access Token - */ export type getJwtTokenFromAccessTokenApiAuthJwtAccessTokenGetResponse200 = { data: JwtTokenResponse; status: 200; @@ -4830,7 +4944,7 @@ export const getGetJwtTokenFromAccessTokenApiAuthJwtAccessTokenGetUrl = ( Object.entries(params || {}).forEach(([key, value]) => { if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); + normalizedParams.append(key, value === null ? "null" : String(value)); } }); @@ -4841,6 +4955,12 @@ export const getGetJwtTokenFromAccessTokenApiAuthJwtAccessTokenGetUrl = ( : `/api/auth/jwt/access_token`; }; +/** + * API to create a new jwt token from an access token. + * + * Deprecated: Use POST /api/auth/jwt/access_token instead. + * @summary Get Jwt Token From Access Token + */ export const getJwtTokenFromAccessTokenApiAuthJwtAccessTokenGet = async ( params: GetJwtTokenFromAccessTokenApiAuthJwtAccessTokenGetParams, options?: RequestInit, @@ -4860,10 +4980,6 @@ export const getJwtTokenFromAccessTokenApiAuthJwtAccessTokenGet = async ( } as getJwtTokenFromAccessTokenApiAuthJwtAccessTokenGetResponse; }; -/** - * API to create a new jwt token from an access token. - * @summary Post Jwt Token From Access Token - */ export type postJwtTokenFromAccessTokenApiAuthJwtAccessTokenPostResponse200 = { data: JwtTokenResponse; status: 200; @@ -4891,6 +5007,10 @@ export const getPostJwtTokenFromAccessTokenApiAuthJwtAccessTokenPostUrl = () => return `/api/auth/jwt/access_token`; }; +/** + * API to create a new jwt token from an access token. + * @summary Post Jwt Token From Access Token + */ export const postJwtTokenFromAccessTokenApiAuthJwtAccessTokenPost = async ( tokenRequest: TokenRequest, options?: RequestInit, @@ -4912,15 +5032,6 @@ export const postJwtTokenFromAccessTokenApiAuthJwtAccessTokenPost = async ( } as postJwtTokenFromAccessTokenApiAuthJwtAccessTokenPostResponse; }; -/** - * API to create a new access token. - -If roles are specified, all specified roles must be assigned to the user. -If any role is not assigned to the user, the request fails and no token -is created. If no roles are specified, the access token inherits all of the user's -current roles from the user_roles table. - * @summary Create Access Token - */ export type createAccessTokenApiAuthAccessTokenTokenNamePostResponse200 = { data: string; status: 200; @@ -4955,13 +5066,13 @@ export const getCreateAccessTokenApiAuthAccessTokenTokenNamePostUrl = ( if (Array.isArray(value) && explodeParameters.includes(key)) { value.forEach((v) => { - normalizedParams.append(key, v === null ? "null" : v.toString()); + normalizedParams.append(key, v === null ? "null" : String(v)); }); return; } if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); + normalizedParams.append(key, value === null ? "null" : String(value)); } }); @@ -4972,6 +5083,15 @@ export const getCreateAccessTokenApiAuthAccessTokenTokenNamePostUrl = ( : `/api/auth/access_token/${tokenName}`; }; +/** + * API to create a new access token. + * + * If roles are specified, all specified roles must be assigned to the user. + * If any role is not assigned to the user, the request fails and no token + * is created. If no roles are specified, the access token inherits all of the user's + * current roles from the user_roles table. + * @summary Create Access Token + */ export const createAccessTokenApiAuthAccessTokenTokenNamePost = async ( tokenName: string, params: CreateAccessTokenApiAuthAccessTokenTokenNamePostParams, @@ -4988,10 +5108,6 @@ export const createAccessTokenApiAuthAccessTokenTokenNamePost = async ( return { data, status: res.status, headers: res.headers } as createAccessTokenApiAuthAccessTokenTokenNamePostResponse; }; -/** - * API to delete an access token. - * @summary Delete Access Token - */ export type deleteAccessTokenApiAuthAccessTokenTokenNameDeleteResponse200 = { data: unknown; status: 200; @@ -5019,6 +5135,10 @@ export const getDeleteAccessTokenApiAuthAccessTokenTokenNameDeleteUrl = (tokenNa return `/api/auth/access_token/${tokenName}`; }; +/** + * API to delete an access token. + * @summary Delete Access Token + */ export const deleteAccessTokenApiAuthAccessTokenTokenNameDelete = async ( tokenName: string, options?: RequestInit, @@ -5038,17 +5158,6 @@ export const deleteAccessTokenApiAuthAccessTokenTokenNameDelete = async ( } as deleteAccessTokenApiAuthAccessTokenTokenNameDeleteResponse; }; -/** - * List all roles assigned to an access token. - -Args: - token_name: The token name - user_name: Authenticated user (owner of the token) - -Returns: - AccessTokenRolesResponse with list of role assignments - * @summary List Access Token Roles - */ export type listAccessTokenRolesApiAuthAccessTokenTokenNameRolesGetResponse200 = { data: AccessTokenRolesResponse; status: 200; @@ -5076,6 +5185,17 @@ export const getListAccessTokenRolesApiAuthAccessTokenTokenNameRolesGetUrl = (to return `/api/auth/access_token/${tokenName}/roles`; }; +/** + * List all roles assigned to an access token. + * + * Args: + * token_name: The token name + * user_name: Authenticated user (owner of the token) + * + * Returns: + * AccessTokenRolesResponse with list of role assignments + * @summary List Access Token Roles + */ export const listAccessTokenRolesApiAuthAccessTokenTokenNameRolesGet = async ( tokenName: string, options?: RequestInit, @@ -5095,10 +5215,6 @@ export const listAccessTokenRolesApiAuthAccessTokenTokenNameRolesGet = async ( } as listAccessTokenRolesApiAuthAccessTokenTokenNameRolesGetResponse; }; -/** - * API to list all access tokens for a user, including their assigned roles. - * @summary List Access Tokens - */ export type listAccessTokensApiAuthAccessTokenGetResponse200 = { data: AccessTokenWithRoles[]; status: 200; @@ -5124,6 +5240,10 @@ export const getListAccessTokensApiAuthAccessTokenGetUrl = () => { return `/api/auth/access_token`; }; +/** + * API to list all access tokens for a user, including their assigned roles. + * @summary List Access Tokens + */ export const listAccessTokensApiAuthAccessTokenGet = async ( options?: RequestInit, ): Promise => { @@ -5138,29 +5258,6 @@ export const listAccessTokensApiAuthAccessTokenGet = async ( return { data, status: res.status, headers: res.headers } as listAccessTokensApiAuthAccessTokenGetResponse; }; -/** - * Admin API to create an access token for a specific user. - -This endpoint allows administrators to create an access token -on behalf of any user in the system. - -If roles are specified, all specified roles must be assigned to the target -user. If any role is not assigned to the user, the request fails and no -token is created. If no roles are specified, the access token inherits all of the -target user's current roles from the user_roles table. - -Args: - user_id: The user ID to create the token for - token_name: Name for the access token - expires_at: Expiration date in YYYY-MM-DD format - description: Optional description for the token - roles: Optional list of roles to assign (must all be assigned to user) - admin_user: Authenticated admin user making the request - -Returns: - The generated access token string - * @summary Admin Create Access Token - */ export type adminCreateAccessTokenApiAuthUserUserIdAccessTokenTokenNamePostResponse200 = { data: string; status: 200; @@ -5196,13 +5293,13 @@ export const getAdminCreateAccessTokenApiAuthUserUserIdAccessTokenTokenNamePostU if (Array.isArray(value) && explodeParameters.includes(key)) { value.forEach((v) => { - normalizedParams.append(key, v === null ? "null" : v.toString()); + normalizedParams.append(key, v === null ? "null" : String(v)); }); return; } if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); + normalizedParams.append(key, value === null ? "null" : String(value)); } }); @@ -5213,6 +5310,29 @@ export const getAdminCreateAccessTokenApiAuthUserUserIdAccessTokenTokenNamePostU : `/api/auth/user/${userId}/access_token/${tokenName}`; }; +/** + * Admin API to create an access token for a specific user. + * + * This endpoint allows administrators to create an access token + * on behalf of any user in the system. + * + * If roles are specified, all specified roles must be assigned to the target + * user. If any role is not assigned to the user, the request fails and no + * token is created. If no roles are specified, the access token inherits all of the + * target user's current roles from the user_roles table. + * + * Args: + * user_id: The user ID to create the token for + * token_name: Name for the access token + * expires_at: Expiration date in YYYY-MM-DD format + * description: Optional description for the token + * roles: Optional list of roles to assign (must all be assigned to user) + * admin_user: Authenticated admin user making the request + * + * Returns: + * The generated access token string + * @summary Admin Create Access Token + */ export const adminCreateAccessTokenApiAuthUserUserIdAccessTokenTokenNamePost = async ( userId: string, tokenName: string, @@ -5239,14 +5359,6 @@ export const adminCreateAccessTokenApiAuthUserUserIdAccessTokenTokenNamePost = a } as adminCreateAccessTokenApiAuthUserUserIdAccessTokenTokenNamePostResponse; }; -/** - * Admin API to delete an access token for a specific user. - -Args: - user_id: The user ID who owns the token - token_name: Name of the token to delete - * @summary Admin Delete Access Token - */ export type adminDeleteAccessTokenApiAuthUserUserIdAccessTokenTokenNameDeleteResponse200 = { data: unknown; status: 200; @@ -5277,6 +5389,14 @@ export const getAdminDeleteAccessTokenApiAuthUserUserIdAccessTokenTokenNameDelet return `/api/auth/user/${userId}/access_token/${tokenName}`; }; +/** + * Admin API to delete an access token for a specific user. + * + * Args: + * user_id: The user ID who owns the token + * token_name: Name of the token to delete + * @summary Admin Delete Access Token + */ export const adminDeleteAccessTokenApiAuthUserUserIdAccessTokenTokenNameDelete = async ( userId: string, tokenName: string, @@ -5299,16 +5419,6 @@ export const adminDeleteAccessTokenApiAuthUserUserIdAccessTokenTokenNameDelete = } as adminDeleteAccessTokenApiAuthUserUserIdAccessTokenTokenNameDeleteResponse; }; -/** - * Admin API to list all access tokens for a specific user, including their assigned roles. - -Args: - user_id: The user ID to list tokens for - -Returns: - List of AccessTokenWithRoles objects - * @summary Admin List Access Tokens - */ export type adminListAccessTokensApiAuthUserUserIdAccessTokenGetResponse200 = { data: AccessTokenWithRoles[]; status: 200; @@ -5336,6 +5446,16 @@ export const getAdminListAccessTokensApiAuthUserUserIdAccessTokenGetUrl = (userI return `/api/auth/user/${userId}/access_token`; }; +/** + * Admin API to list all access tokens for a specific user, including their assigned roles. + * + * Args: + * user_id: The user ID to list tokens for + * + * Returns: + * List of AccessTokenWithRoles objects + * @summary Admin List Access Tokens + */ export const adminListAccessTokensApiAuthUserUserIdAccessTokenGet = async ( userId: string, options?: RequestInit, @@ -5355,20 +5475,6 @@ export const adminListAccessTokensApiAuthUserUserIdAccessTokenGet = async ( } as adminListAccessTokensApiAuthUserUserIdAccessTokenGetResponse; }; -/** - * List all users with optional filtering. - -Args: - start_index: Pagination start (1-based, default: 1) - count: Results per page (default: 100, max: 1000) - id_prefix: Filter users whose ID starts with this prefix - roles: List of role names. Returns users who have ANY of these roles. - Use multiple query params: ?roles=admin&roles=user - -Returns: - UserListResponse with paginated user list - * @summary List Users - */ export type listUsersApiAuthUserGetResponse200 = { data: UserListResponse; status: 200; @@ -5398,13 +5504,13 @@ export const getListUsersApiAuthUserGetUrl = (params?: ListUsersApiAuthUserGetPa if (Array.isArray(value) && explodeParameters.includes(key)) { value.forEach((v) => { - normalizedParams.append(key, v === null ? "null" : v.toString()); + normalizedParams.append(key, v === null ? "null" : String(v)); }); return; } if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); + normalizedParams.append(key, value === null ? "null" : String(value)); } }); @@ -5413,6 +5519,20 @@ export const getListUsersApiAuthUserGetUrl = (params?: ListUsersApiAuthUserGetPa return stringifiedParams.length > 0 ? `/api/auth/user?${stringifiedParams}` : `/api/auth/user`; }; +/** + * List all users with optional filtering. + * + * Args: + * start_index: Pagination start (1-based, default: 1) + * count: Results per page (default: 100, max: 1000) + * id_prefix: Filter users whose ID starts with this prefix + * roles: List of role names. Returns users who have ANY of these roles. + * Use multiple query params: ?roles=admin&roles=user + * + * Returns: + * UserListResponse with paginated user list + * @summary List Users + */ export const listUsersApiAuthUserGet = async ( params?: ListUsersApiAuthUserGetParams, options?: RequestInit, @@ -5428,17 +5548,6 @@ export const listUsersApiAuthUserGet = async ( return { data, status: res.status, headers: res.headers } as listUsersApiAuthUserGetResponse; }; -/** - * Create a new user. - -Args: - request: CreateUserRequest with user details - created_by: Authenticated user making the request - -Returns: - Created User object - * @summary Create User - */ export type createUserApiAuthUserPostResponse200 = { data: User; status: 200; @@ -5464,6 +5573,17 @@ export const getCreateUserApiAuthUserPostUrl = () => { return `/api/auth/user`; }; +/** + * Create a new user. + * + * Args: + * request: CreateUserRequest with user details + * created_by: Authenticated user making the request + * + * Returns: + * Created User object + * @summary Create User + */ export const createUserApiAuthUserPost = async ( createUserRequest: CreateUserRequest, options?: RequestInit, @@ -5481,16 +5601,6 @@ export const createUserApiAuthUserPost = async ( return { data, status: res.status, headers: res.headers } as createUserApiAuthUserPostResponse; }; -/** - * Get a specific user's details including their roles. - -Args: - user_id: The user ID to fetch - -Returns: - UserWithRoles object - * @summary Get User - */ export type getUserApiAuthUserUserIdGetResponse200 = { data: UserWithRoles; status: 200; @@ -5516,6 +5626,16 @@ export const getGetUserApiAuthUserUserIdGetUrl = (userId: string) => { return `/api/auth/user/${userId}`; }; +/** + * Get a specific user's details including their roles. + * + * Args: + * user_id: The user ID to fetch + * + * Returns: + * UserWithRoles object + * @summary Get User + */ export const getUserApiAuthUserUserIdGet = async ( userId: string, options?: RequestInit, @@ -5531,13 +5651,6 @@ export const getUserApiAuthUserUserIdGet = async ( return { data, status: res.status, headers: res.headers } as getUserApiAuthUserUserIdGetResponse; }; -/** - * Delete a user and all associated role assignments and PATs. - -Args: - user_id: The user ID to delete - * @summary Delete User - */ export type deleteUserApiAuthUserUserIdDeleteResponse200 = { data: unknown; status: 200; @@ -5563,6 +5676,13 @@ export const getDeleteUserApiAuthUserUserIdDeleteUrl = (userId: string) => { return `/api/auth/user/${userId}`; }; +/** + * Delete a user and all associated role assignments and PATs. + * + * Args: + * user_id: The user ID to delete + * @summary Delete User + */ export const deleteUserApiAuthUserUserIdDelete = async ( userId: string, options?: RequestInit, @@ -5578,16 +5698,6 @@ export const deleteUserApiAuthUserUserIdDelete = async ( return { data, status: res.status, headers: res.headers } as deleteUserApiAuthUserUserIdDeleteResponse; }; -/** - * List all roles assigned to a user. - -Args: - user_id: The user ID - -Returns: - UserRolesResponse with list of role assignments - * @summary List User Roles - */ export type listUserRolesApiAuthUserUserIdRolesGetResponse200 = { data: UserRolesResponse; status: 200; @@ -5614,6 +5724,16 @@ export const getListUserRolesApiAuthUserUserIdRolesGetUrl = (userId: string) => return `/api/auth/user/${userId}/roles`; }; +/** + * List all roles assigned to a user. + * + * Args: + * user_id: The user ID + * + * Returns: + * UserRolesResponse with list of role assignments + * @summary List User Roles + */ export const listUserRolesApiAuthUserUserIdRolesGet = async ( userId: string, options?: RequestInit, @@ -5629,18 +5749,6 @@ export const listUserRolesApiAuthUserUserIdRolesGet = async ( return { data, status: res.status, headers: res.headers } as listUserRolesApiAuthUserUserIdRolesGetResponse; }; -/** - * Assign a role to a user. - -Args: - user_id: The user ID - request: AssignRoleRequest with role_name - assigned_by: Authenticated user making the request - -Returns: - UserRoleAssignment with assignment details - * @summary Assign Role To User - */ export type assignRoleToUserApiAuthUserUserIdRolesPostResponse200 = { data: UserRoleAssignment; status: 200; @@ -5668,6 +5776,18 @@ export const getAssignRoleToUserApiAuthUserUserIdRolesPostUrl = (userId: string) return `/api/auth/user/${userId}/roles`; }; +/** + * Assign a role to a user. + * + * Args: + * user_id: The user ID + * request: AssignRoleRequest with role_name + * assigned_by: Authenticated user making the request + * + * Returns: + * UserRoleAssignment with assignment details + * @summary Assign Role To User + */ export const assignRoleToUserApiAuthUserUserIdRolesPost = async ( userId: string, assignRoleRequest: AssignRoleRequest, @@ -5686,17 +5806,6 @@ export const assignRoleToUserApiAuthUserUserIdRolesPost = async ( return { data, status: res.status, headers: res.headers } as assignRoleToUserApiAuthUserUserIdRolesPostResponse; }; -/** - * Remove a role from a user and all their PATs. - -When a role is removed from a user, it is automatically removed from all PATs -owned by that user via the FK cascade from access_token_roles to user_roles. - -Args: - user_id: The user ID - role_name: The role to remove - * @summary Remove Role From User - */ export type removeRoleFromUserApiAuthUserUserIdRolesRoleNameDeleteResponse200 = { data: unknown; status: 200; @@ -5724,6 +5833,17 @@ export const getRemoveRoleFromUserApiAuthUserUserIdRolesRoleNameDeleteUrl = (use return `/api/auth/user/${userId}/roles/${roleName}`; }; +/** + * Remove a role from a user and all their PATs. + * + * When a role is removed from a user, it is automatically removed from all PATs + * owned by that user via the FK cascade from access_token_roles to user_roles. + * + * Args: + * user_id: The user ID + * role_name: The role to remove + * @summary Remove Role From User + */ export const removeRoleFromUserApiAuthUserUserIdRolesRoleNameDelete = async ( userId: string, roleName: string, @@ -5744,16 +5864,6 @@ export const removeRoleFromUserApiAuthUserUserIdRolesRoleNameDelete = async ( } as removeRoleFromUserApiAuthUserUserIdRolesRoleNameDeleteResponse; }; -/** - * List all users who have a specific role. - -Args: - role_name: The role name - -Returns: - RoleUsersResponse with list of users - * @summary List Users With Role - */ export type listUsersWithRoleApiAuthRolesRoleNameUsersGetResponse200 = { data: RoleUsersResponse; status: 200; @@ -5781,6 +5891,16 @@ export const getListUsersWithRoleApiAuthRolesRoleNameUsersGetUrl = (roleName: st return `/api/auth/roles/${roleName}/users`; }; +/** + * List all users who have a specific role. + * + * Args: + * role_name: The role name + * + * Returns: + * RoleUsersResponse with list of users + * @summary List Users With Role + */ export const listUsersWithRoleApiAuthRolesRoleNameUsersGet = async ( roleName: string, options?: RequestInit, @@ -5796,18 +5916,6 @@ export const listUsersWithRoleApiAuthRolesRoleNameUsersGet = async ( return { data, status: res.status, headers: res.headers } as listUsersWithRoleApiAuthRolesRoleNameUsersGetResponse; }; -/** - * Bulk assign a role to multiple users. - -Args: - role_name: The role to assign - request: BulkAssignRequest with list of user_ids - assigned_by: Authenticated user making the request - -Returns: - BulkAssignResponse with results - * @summary Bulk Assign Role - */ export type bulkAssignRoleApiAuthRolesRoleNameUsersPostResponse200 = { data: BulkAssignResponse; status: 200; @@ -5835,6 +5943,18 @@ export const getBulkAssignRoleApiAuthRolesRoleNameUsersPostUrl = (roleName: stri return `/api/auth/roles/${roleName}/users`; }; +/** + * Bulk assign a role to multiple users. + * + * Args: + * role_name: The role to assign + * request: BulkAssignRequest with list of user_ids + * assigned_by: Authenticated user making the request + * + * Returns: + * BulkAssignResponse with results + * @summary Bulk Assign Role + */ export const bulkAssignRoleApiAuthRolesRoleNameUsersPost = async ( roleName: string, bulkAssignRequest: BulkAssignRequest, @@ -5853,9 +5973,6 @@ export const bulkAssignRoleApiAuthRolesRoleNameUsersPost = async ( return { data, status: res.status, headers: res.headers } as bulkAssignRoleApiAuthRolesRoleNameUsersPostResponse; }; -/** - * @summary List Apps - */ export type listAppsApiAppGetResponse200 = { data: SrcServiceCoreAppObjectsListResponse; status: 200; @@ -5883,13 +6000,13 @@ export const getListAppsApiAppGetUrl = (params?: ListAppsApiAppGetParams) => { if (Array.isArray(value) && explodeParameters.includes(key)) { value.forEach((v) => { - normalizedParams.append(key, v === null ? "null" : v.toString()); + normalizedParams.append(key, v === null ? "null" : String(v)); }); return; } if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); + normalizedParams.append(key, value === null ? "null" : String(value)); } }); @@ -5898,6 +6015,9 @@ export const getListAppsApiAppGetUrl = (params?: ListAppsApiAppGetParams) => { return stringifiedParams.length > 0 ? `/api/app?${stringifiedParams}` : `/api/app`; }; +/** + * @summary List Apps + */ export const listAppsApiAppGet = async ( params?: ListAppsApiAppGetParams, options?: RequestInit, @@ -5913,9 +6033,6 @@ export const listAppsApiAppGet = async ( return { data, status: res.status, headers: res.headers } as listAppsApiAppGetResponse; }; -/** - * @summary Get App - */ export type getAppApiAppUserNameGetResponse200 = { data: GetAppResponse; status: 200; @@ -5942,7 +6059,7 @@ export const getGetAppApiAppUserNameGetUrl = (name: string, params?: GetAppApiAp Object.entries(params || {}).forEach(([key, value]) => { if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); + normalizedParams.append(key, value === null ? "null" : String(value)); } }); @@ -5951,6 +6068,9 @@ export const getGetAppApiAppUserNameGetUrl = (name: string, params?: GetAppApiAp return stringifiedParams.length > 0 ? `/api/app/user/${name}?${stringifiedParams}` : `/api/app/user/${name}`; }; +/** + * @summary Get App + */ export const getAppApiAppUserNameGet = async ( name: string, params?: GetAppApiAppUserNameGetParams, @@ -5967,9 +6087,6 @@ export const getAppApiAppUserNameGet = async ( return { data, status: res.status, headers: res.headers } as getAppApiAppUserNameGetResponse; }; -/** - * @summary Create App - */ export type createAppApiAppUserNamePostResponse200 = { data: unknown; status: 200; @@ -5996,7 +6113,7 @@ export const getCreateAppApiAppUserNamePostUrl = (name: string, params: CreateAp Object.entries(params || {}).forEach(([key, value]) => { if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); + normalizedParams.append(key, value === null ? "null" : String(value)); } }); @@ -6005,6 +6122,9 @@ export const getCreateAppApiAppUserNamePostUrl = (name: string, params: CreateAp return stringifiedParams.length > 0 ? `/api/app/user/${name}?${stringifiedParams}` : `/api/app/user/${name}`; }; +/** + * @summary Create App + */ export const createAppApiAppUserNamePost = async ( name: string, createAppApiAppUserNamePostBody: string, @@ -6024,9 +6144,6 @@ export const createAppApiAppUserNamePost = async ( return { data, status: res.status, headers: res.headers } as createAppApiAppUserNamePostResponse; }; -/** - * @summary Update App - */ export type updateAppApiAppUserNamePatchResponse200 = { data: EditResponse; status: 200; @@ -6052,6 +6169,9 @@ export const getUpdateAppApiAppUserNamePatchUrl = (name: string) => { return `/api/app/user/${name}`; }; +/** + * @summary Update App + */ export const updateAppApiAppUserNamePatch = async ( name: string, updateAppApiAppUserNamePatchBody: string, @@ -6070,9 +6190,6 @@ export const updateAppApiAppUserNamePatch = async ( return { data, status: res.status, headers: res.headers } as updateAppApiAppUserNamePatchResponse; }; -/** - * @summary Delete App - */ export type deleteAppApiAppUserNameDeleteResponse200 = { data: DeleteAppApiAppUserNameDelete200; status: 200; @@ -6099,7 +6216,7 @@ export const getDeleteAppApiAppUserNameDeleteUrl = (name: string, params?: Delet Object.entries(params || {}).forEach(([key, value]) => { if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); + normalizedParams.append(key, value === null ? "null" : String(value)); } }); @@ -6108,6 +6225,9 @@ export const getDeleteAppApiAppUserNameDeleteUrl = (name: string, params?: Delet return stringifiedParams.length > 0 ? `/api/app/user/${name}?${stringifiedParams}` : `/api/app/user/${name}`; }; +/** + * @summary Delete App + */ export const deleteAppApiAppUserNameDelete = async ( name: string, params?: DeleteAppApiAppUserNameDeleteParams, @@ -6124,9 +6244,6 @@ export const deleteAppApiAppUserNameDelete = async ( return { data, status: res.status, headers: res.headers } as deleteAppApiAppUserNameDeleteResponse; }; -/** - * @summary Get App Content - */ export type getAppContentApiAppUserNameSpecGetResponse200 = { data: void; status: 200; @@ -6156,7 +6273,7 @@ export const getGetAppContentApiAppUserNameSpecGetUrl = ( Object.entries(params || {}).forEach(([key, value]) => { if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); + normalizedParams.append(key, value === null ? "null" : String(value)); } }); @@ -6167,6 +6284,9 @@ export const getGetAppContentApiAppUserNameSpecGetUrl = ( : `/api/app/user/${name}/spec`; }; +/** + * @summary Get App Content + */ export const getAppContentApiAppUserNameSpecGet = async ( name: string, params?: GetAppContentApiAppUserNameSpecGetParams, @@ -6179,13 +6299,10 @@ export const getAppContentApiAppUserNameSpecGet = async ( const body = [204, 205, 304].includes(res.status) ? null : await res.text(); - const data: getAppContentApiAppUserNameSpecGetResponse["data"] = body ? JSON.parse(body) : {}; + const data: getAppContentApiAppUserNameSpecGetResponse["data"] = body ? JSON.parse(body) : undefined; return { data, status: res.status, headers: res.headers } as getAppContentApiAppUserNameSpecGetResponse; }; -/** - * @summary Rename App - */ export type renameAppApiAppUserNameRenamePostResponse200 = { data: string; status: 200; @@ -6211,6 +6328,9 @@ export const getRenameAppApiAppUserNameRenamePostUrl = (name: string) => { return `/api/app/user/${name}/rename`; }; +/** + * @summary Rename App + */ export const renameAppApiAppUserNameRenamePost = async ( name: string, renameAppApiAppUserNameRenamePostBody: string, @@ -6229,10 +6349,6 @@ export const renameAppApiAppUserNameRenamePost = async ( return { data, status: res.status, headers: res.headers } as renameAppApiAppUserNameRenamePostResponse; }; -/** - * Cancels the workflow. - * @summary Cancel Workflow - */ export type cancelWorkflowApiWorkflowNameCancelPostResponse200 = { data: CancelResponse; status: 200; @@ -6264,7 +6380,7 @@ export const getCancelWorkflowApiWorkflowNameCancelPostUrl = ( Object.entries(params || {}).forEach(([key, value]) => { if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); + normalizedParams.append(key, value === null ? "null" : String(value)); } }); @@ -6275,6 +6391,10 @@ export const getCancelWorkflowApiWorkflowNameCancelPostUrl = ( : `/api/workflow/${name}/cancel`; }; +/** + * Cancels the workflow. + * @summary Cancel Workflow + */ export const cancelWorkflowApiWorkflowNameCancelPost = async ( name: string, params?: CancelWorkflowApiWorkflowNameCancelPostParams, @@ -6291,9 +6411,6 @@ export const cancelWorkflowApiWorkflowNameCancelPost = async ( return { data, status: res.status, headers: res.headers } as cancelWorkflowApiWorkflowNameCancelPostResponse; }; -/** - * @summary List Workflow - */ export type listWorkflowApiWorkflowGetResponse200 = { data: SrcServiceCoreWorkflowObjectsListResponse; status: 200; @@ -6319,17 +6436,17 @@ export const getListWorkflowApiWorkflowGetUrl = (params?: ListWorkflowApiWorkflo const normalizedParams = new URLSearchParams(); Object.entries(params || {}).forEach(([key, value]) => { - const explodeParameters = ["users", "statuses", "pools", "tags", "priority"]; + const explodeParameters = ["users", "statuses", "pools", "tags", "priority", "label", "no_label"]; if (Array.isArray(value) && explodeParameters.includes(key)) { value.forEach((v) => { - normalizedParams.append(key, v === null ? "null" : v.toString()); + normalizedParams.append(key, v === null ? "null" : String(v)); }); return; } if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); + normalizedParams.append(key, value === null ? "null" : String(value)); } }); @@ -6338,6 +6455,9 @@ export const getListWorkflowApiWorkflowGetUrl = (params?: ListWorkflowApiWorkflo return stringifiedParams.length > 0 ? `/api/workflow?${stringifiedParams}` : `/api/workflow`; }; +/** + * @summary List Workflow + */ export const listWorkflowApiWorkflowGet = async ( params?: ListWorkflowApiWorkflowGetParams, options?: RequestInit, @@ -6352,11 +6472,7 @@ export const listWorkflowApiWorkflowGet = async ( const data: listWorkflowApiWorkflowGetResponse["data"] = body ? JSON.parse(body) : {}; return { data, status: res.status, headers: res.headers } as listWorkflowApiWorkflowGetResponse; }; - -/** - * Returns the task (with the latest retry_id) with the given name in the workflow. - * @summary Get Workflow Task - */ + export type getWorkflowTaskApiWorkflowNameTaskTaskNameGetResponse200 = { data: TaskEntry; status: 200; @@ -6384,6 +6500,10 @@ export const getGetWorkflowTaskApiWorkflowNameTaskTaskNameGetUrl = (name: string return `/api/workflow/${name}/task/${taskName}`; }; +/** + * Returns the task (with the latest retry_id) with the given name in the workflow. + * @summary Get Workflow Task + */ export const getWorkflowTaskApiWorkflowNameTaskTaskNameGet = async ( name: string, taskName: string, @@ -6400,9 +6520,6 @@ export const getWorkflowTaskApiWorkflowNameTaskTaskNameGet = async ( return { data, status: res.status, headers: res.headers } as getWorkflowTaskApiWorkflowNameTaskTaskNameGetResponse; }; -/** - * @summary List Task - */ export type listTaskApiTaskGetResponse200 = { data: ListTaskSummaryResponse | ListTaskResponse | ListTaskAggregatedResponse; status: 200; @@ -6430,13 +6547,13 @@ export const getListTaskApiTaskGetUrl = (params?: ListTaskApiTaskGetParams) => { if (Array.isArray(value) && explodeParameters.includes(key)) { value.forEach((v) => { - normalizedParams.append(key, v === null ? "null" : v.toString()); + normalizedParams.append(key, v === null ? "null" : String(v)); }); return; } if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); + normalizedParams.append(key, value === null ? "null" : String(value)); } }); @@ -6445,6 +6562,9 @@ export const getListTaskApiTaskGetUrl = (params?: ListTaskApiTaskGetParams) => { return stringifiedParams.length > 0 ? `/api/task?${stringifiedParams}` : `/api/task`; }; +/** + * @summary List Task + */ export const listTaskApiTaskGet = async ( params?: ListTaskApiTaskGetParams, options?: RequestInit, @@ -6460,10 +6580,6 @@ export const listTaskApiTaskGet = async ( return { data, status: res.status, headers: res.headers } as listTaskApiTaskGetResponse; }; -/** - * Returns the workflow with the given name in the database. - * @summary Get Workflow - */ export type getWorkflowApiWorkflowNameGetResponse200 = { data: WorkflowQueryResponse; status: 200; @@ -6490,7 +6606,7 @@ export const getGetWorkflowApiWorkflowNameGetUrl = (name: string, params?: GetWo Object.entries(params || {}).forEach(([key, value]) => { if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); + normalizedParams.append(key, value === null ? "null" : String(value)); } }); @@ -6499,6 +6615,10 @@ export const getGetWorkflowApiWorkflowNameGetUrl = (name: string, params?: GetWo return stringifiedParams.length > 0 ? `/api/workflow/${name}?${stringifiedParams}` : `/api/workflow/${name}`; }; +/** + * Returns the workflow with the given name in the database. + * @summary Get Workflow + */ export const getWorkflowApiWorkflowNameGet = async ( name: string, params?: GetWorkflowApiWorkflowNameGetParams, @@ -6515,10 +6635,6 @@ export const getWorkflowApiWorkflowNameGet = async ( return { data, status: res.status, headers: res.headers } as getWorkflowApiWorkflowNameGetResponse; }; -/** - * Returns the workflow logs. - * @summary Get Workflow Logs - */ export type getWorkflowLogsApiWorkflowNameLogsGetResponse200 = { data: string; status: 200; @@ -6548,7 +6664,7 @@ export const getGetWorkflowLogsApiWorkflowNameLogsGetUrl = ( Object.entries(params || {}).forEach(([key, value]) => { if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); + normalizedParams.append(key, value === null ? "null" : String(value)); } }); @@ -6559,6 +6675,10 @@ export const getGetWorkflowLogsApiWorkflowNameLogsGetUrl = ( : `/api/workflow/${name}/logs`; }; +/** + * Returns the workflow logs. + * @summary Get Workflow Logs + */ export const getWorkflowLogsApiWorkflowNameLogsGet = async ( name: string, params?: GetWorkflowLogsApiWorkflowNameLogsGetParams, @@ -6569,16 +6689,17 @@ export const getWorkflowLogsApiWorkflowNameLogsGet = async ( method: "GET", }); + const contentType = (res.headers.get("content-type") ?? "").toLowerCase(); const body = [204, 205, 304].includes(res.status) ? null : await res.text(); - const data: getWorkflowLogsApiWorkflowNameLogsGetResponse["data"] = body ? JSON.parse(body) : {}; + const data: getWorkflowLogsApiWorkflowNameLogsGetResponse["data"] = body + ? contentType.includes("json") + ? JSON.parse(body) + : body + : {}; return { data, status: res.status, headers: res.headers } as getWorkflowLogsApiWorkflowNameLogsGetResponse; }; -/** - * Returns the workflow pod conditions. - * @summary Get Workflow Pod Conditions - */ export type getWorkflowPodConditionsApiWorkflowNameEventsGetResponse200 = { data: string; status: 200; @@ -6610,7 +6731,7 @@ export const getGetWorkflowPodConditionsApiWorkflowNameEventsGetUrl = ( Object.entries(params || {}).forEach(([key, value]) => { if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); + normalizedParams.append(key, value === null ? "null" : String(value)); } }); @@ -6621,6 +6742,10 @@ export const getGetWorkflowPodConditionsApiWorkflowNameEventsGetUrl = ( : `/api/workflow/${name}/events`; }; +/** + * Returns the workflow pod conditions. + * @summary Get Workflow Pod Conditions + */ export const getWorkflowPodConditionsApiWorkflowNameEventsGet = async ( name: string, params?: GetWorkflowPodConditionsApiWorkflowNameEventsGetParams, @@ -6631,16 +6756,17 @@ export const getWorkflowPodConditionsApiWorkflowNameEventsGet = async ( method: "GET", }); + const contentType = (res.headers.get("content-type") ?? "").toLowerCase(); const body = [204, 205, 304].includes(res.status) ? null : await res.text(); - const data: getWorkflowPodConditionsApiWorkflowNameEventsGetResponse["data"] = body ? JSON.parse(body) : {}; + const data: getWorkflowPodConditionsApiWorkflowNameEventsGetResponse["data"] = body + ? contentType.includes("json") + ? JSON.parse(body) + : body + : {}; return { data, status: res.status, headers: res.headers } as getWorkflowPodConditionsApiWorkflowNameEventsGetResponse; }; -/** - * Returns the workflow error logs. - * @summary Get Workflow Error Logs - */ export type getWorkflowErrorLogsApiWorkflowNameErrorLogsGetResponse200 = { data: string; status: 200; @@ -6672,7 +6798,7 @@ export const getGetWorkflowErrorLogsApiWorkflowNameErrorLogsGetUrl = ( Object.entries(params || {}).forEach(([key, value]) => { if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); + normalizedParams.append(key, value === null ? "null" : String(value)); } }); @@ -6683,6 +6809,10 @@ export const getGetWorkflowErrorLogsApiWorkflowNameErrorLogsGetUrl = ( : `/api/workflow/${name}/error_logs`; }; +/** + * Returns the workflow error logs. + * @summary Get Workflow Error Logs + */ export const getWorkflowErrorLogsApiWorkflowNameErrorLogsGet = async ( name: string, params?: GetWorkflowErrorLogsApiWorkflowNameErrorLogsGetParams, @@ -6693,16 +6823,17 @@ export const getWorkflowErrorLogsApiWorkflowNameErrorLogsGet = async ( method: "GET", }); + const contentType = (res.headers.get("content-type") ?? "").toLowerCase(); const body = [204, 205, 304].includes(res.status) ? null : await res.text(); - const data: getWorkflowErrorLogsApiWorkflowNameErrorLogsGetResponse["data"] = body ? JSON.parse(body) : {}; + const data: getWorkflowErrorLogsApiWorkflowNameErrorLogsGetResponse["data"] = body + ? contentType.includes("json") + ? JSON.parse(body) + : body + : {}; return { data, status: res.status, headers: res.headers } as getWorkflowErrorLogsApiWorkflowNameErrorLogsGetResponse; }; -/** - * Returns the workflow spec. - * @summary Get Workflow Spec - */ export type getWorkflowSpecApiWorkflowNameSpecGetResponse200 = { data: string; status: 200; @@ -6732,7 +6863,7 @@ export const getGetWorkflowSpecApiWorkflowNameSpecGetUrl = ( Object.entries(params || {}).forEach(([key, value]) => { if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); + normalizedParams.append(key, value === null ? "null" : String(value)); } }); @@ -6743,6 +6874,10 @@ export const getGetWorkflowSpecApiWorkflowNameSpecGetUrl = ( : `/api/workflow/${name}/spec`; }; +/** + * Returns the workflow spec. + * @summary Get Workflow Spec + */ export const getWorkflowSpecApiWorkflowNameSpecGet = async ( name: string, params?: GetWorkflowSpecApiWorkflowNameSpecGetParams, @@ -6753,16 +6888,17 @@ export const getWorkflowSpecApiWorkflowNameSpecGet = async ( method: "GET", }); + const contentType = (res.headers.get("content-type") ?? "").toLowerCase(); const body = [204, 205, 304].includes(res.status) ? null : await res.text(); - const data: getWorkflowSpecApiWorkflowNameSpecGetResponse["data"] = body ? JSON.parse(body) : {}; + const data: getWorkflowSpecApiWorkflowNameSpecGetResponse["data"] = body + ? contentType.includes("json") + ? JSON.parse(body) + : body + : {}; return { data, status: res.status, headers: res.headers } as getWorkflowSpecApiWorkflowNameSpecGetResponse; }; -/** - * Returns the workflow spec. - * @summary Tag Workflow - */ export type tagWorkflowApiWorkflowNameTagPostResponse200 = { data: unknown; status: 200; @@ -6795,13 +6931,13 @@ export const getTagWorkflowApiWorkflowNameTagPostUrl = ( if (Array.isArray(value) && explodeParameters.includes(key)) { value.forEach((v) => { - normalizedParams.append(key, v === null ? "null" : v.toString()); + normalizedParams.append(key, v === null ? "null" : String(v)); }); return; } if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); + normalizedParams.append(key, value === null ? "null" : String(value)); } }); @@ -6810,6 +6946,10 @@ export const getTagWorkflowApiWorkflowNameTagPostUrl = ( return stringifiedParams.length > 0 ? `/api/workflow/${name}/tag?${stringifiedParams}` : `/api/workflow/${name}/tag`; }; +/** + * Returns the workflow spec. + * @summary Tag Workflow + */ export const tagWorkflowApiWorkflowNameTagPost = async ( name: string, params?: TagWorkflowApiWorkflowNameTagPostParams, @@ -6826,10 +6966,6 @@ export const tagWorkflowApiWorkflowNameTagPost = async ( return { data, status: res.status, headers: res.headers } as tagWorkflowApiWorkflowNameTagPostResponse; }; -/** - * Send command to all tasks in a group. - * @summary Exec Into Group - */ export type execIntoGroupApiWorkflowNameExecGroupGroupNamePostResponse200 = { data: ExecIntoGroupApiWorkflowNameExecGroupGroupNamePost200; status: 200; @@ -6862,7 +6998,7 @@ export const getExecIntoGroupApiWorkflowNameExecGroupGroupNamePostUrl = ( Object.entries(params || {}).forEach(([key, value]) => { if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); + normalizedParams.append(key, value === null ? "null" : String(value)); } }); @@ -6873,6 +7009,10 @@ export const getExecIntoGroupApiWorkflowNameExecGroupGroupNamePostUrl = ( : `/api/workflow/${name}/exec/group/${groupName}`; }; +/** + * Send command to all tasks in a group. + * @summary Exec Into Group + */ export const execIntoGroupApiWorkflowNameExecGroupGroupNamePost = async ( name: string, groupName: string, @@ -6894,10 +7034,6 @@ export const execIntoGroupApiWorkflowNameExecGroupGroupNamePost = async ( } as execIntoGroupApiWorkflowNameExecGroupGroupNamePostResponse; }; -/** - * Exec into a task container. - * @summary Exec Into Task - */ export type execIntoTaskApiWorkflowNameExecTaskTaskNamePostResponse200 = { data: RouterResponse; status: 200; @@ -6930,7 +7066,7 @@ export const getExecIntoTaskApiWorkflowNameExecTaskTaskNamePostUrl = ( Object.entries(params || {}).forEach(([key, value]) => { if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); + normalizedParams.append(key, value === null ? "null" : String(value)); } }); @@ -6941,6 +7077,10 @@ export const getExecIntoTaskApiWorkflowNameExecTaskTaskNamePostUrl = ( : `/api/workflow/${name}/exec/task/${taskName}`; }; +/** + * Exec into a task container. + * @summary Exec Into Task + */ export const execIntoTaskApiWorkflowNameExecTaskTaskNamePost = async ( name: string, taskName: string, @@ -6958,10 +7098,6 @@ export const execIntoTaskApiWorkflowNameExecTaskTaskNamePost = async ( return { data, status: res.status, headers: res.headers } as execIntoTaskApiWorkflowNameExecTaskTaskNamePostResponse; }; -/** - * Portforward into a task container. - * @summary Port Forward Task - */ export type portForwardTaskApiWorkflowNamePortforwardTaskNamePostResponse200 = { data: RouterResponse[] | RouterResponse; status: 200; @@ -6997,13 +7133,13 @@ export const getPortForwardTaskApiWorkflowNamePortforwardTaskNamePostUrl = ( if (Array.isArray(value) && explodeParameters.includes(key)) { value.forEach((v) => { - normalizedParams.append(key, v === null ? "null" : v.toString()); + normalizedParams.append(key, v === null ? "null" : String(v)); }); return; } if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); + normalizedParams.append(key, value === null ? "null" : String(value)); } }); @@ -7014,6 +7150,10 @@ export const getPortForwardTaskApiWorkflowNamePortforwardTaskNamePostUrl = ( : `/api/workflow/${name}/portforward/${taskName}`; }; +/** + * Portforward into a task container. + * @summary Port Forward Task + */ export const portForwardTaskApiWorkflowNamePortforwardTaskNamePost = async ( name: string, taskName: string, @@ -7035,10 +7175,6 @@ export const portForwardTaskApiWorkflowNamePortforwardTaskNamePost = async ( } as portForwardTaskApiWorkflowNamePortforwardTaskNamePostResponse; }; -/** - * Hold a webserver connection to a task container. - * @summary Port Forward Webserver - */ export type portForwardWebserverApiWorkflowNameWebserverTaskNamePostResponse200 = { data: RouterResponse; status: 200; @@ -7071,7 +7207,7 @@ export const getPortForwardWebserverApiWorkflowNameWebserverTaskNamePostUrl = ( Object.entries(params || {}).forEach(([key, value]) => { if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); + normalizedParams.append(key, value === null ? "null" : String(value)); } }); @@ -7082,6 +7218,10 @@ export const getPortForwardWebserverApiWorkflowNameWebserverTaskNamePostUrl = ( : `/api/workflow/${name}/webserver/${taskName}`; }; +/** + * Hold a webserver connection to a task container. + * @summary Port Forward Webserver + */ export const portForwardWebserverApiWorkflowNameWebserverTaskNamePost = async ( name: string, taskName: string, @@ -7103,10 +7243,6 @@ export const portForwardWebserverApiWorkflowNameWebserverTaskNamePost = async ( } as portForwardWebserverApiWorkflowNameWebserverTaskNamePostResponse; }; -/** - * Rsync into a task container. - * @summary Rsync Task - */ export type rsyncTaskApiWorkflowNameRsyncTaskTaskNamePostResponse200 = { data: RouterResponse; status: 200; @@ -7134,6 +7270,10 @@ export const getRsyncTaskApiWorkflowNameRsyncTaskTaskNamePostUrl = (name: string return `/api/workflow/${name}/rsync/task/${taskName}`; }; +/** + * Rsync into a task container. + * @summary Rsync Task + */ export const rsyncTaskApiWorkflowNameRsyncTaskTaskNamePost = async ( name: string, taskName: string, @@ -7150,10 +7290,6 @@ export const rsyncTaskApiWorkflowNameRsyncTaskTaskNamePost = async ( return { data, status: res.status, headers: res.headers } as rsyncTaskApiWorkflowNameRsyncTaskTaskNamePostResponse; }; -/** - * Get default/all user credentials - * @summary Get User Credential - */ export type getUserCredentialApiCredentialsGetResponse200 = { data: CredentialGetResponse; status: 200; @@ -7179,6 +7315,10 @@ export const getGetUserCredentialApiCredentialsGetUrl = () => { return `/api/credentials`; }; +/** + * Get default/all user credentials + * @summary Get User Credential + */ export const getUserCredentialApiCredentialsGet = async ( options?: RequestInit, ): Promise => { @@ -7193,10 +7333,6 @@ export const getUserCredentialApiCredentialsGet = async ( return { data, status: res.status, headers: res.headers } as getUserCredentialApiCredentialsGetResponse; }; -/** - * Post/Update user credentials - * @summary Set User Credential - */ export type setUserCredentialApiCredentialsCredNamePostResponse200 = { data: unknown; status: 200; @@ -7224,6 +7360,10 @@ export const getSetUserCredentialApiCredentialsCredNamePostUrl = (credName: stri return `/api/credentials/${credName}`; }; +/** + * Post/Update user credentials + * @summary Set User Credential + */ export const setUserCredentialApiCredentialsCredNamePost = async ( credName: string, credentialOptions: CredentialOptions, @@ -7242,10 +7382,6 @@ export const setUserCredentialApiCredentialsCredNamePost = async ( return { data, status: res.status, headers: res.headers } as setUserCredentialApiCredentialsCredNamePostResponse; }; -/** - * Delete user credentials given the secret_id - * @summary Delete Users Credential - */ export type deleteUsersCredentialApiCredentialsCredNameDeleteResponse200 = { data: CredentialGetResponse; status: 200; @@ -7273,6 +7409,10 @@ export const getDeleteUsersCredentialApiCredentialsCredNameDeleteUrl = (credName return `/api/credentials/${credName}`; }; +/** + * Delete user credentials given the secret_id + * @summary Delete Users Credential + */ export const deleteUsersCredentialApiCredentialsCredNameDelete = async ( credName: string, options?: RequestInit, @@ -7292,10 +7432,6 @@ export const deleteUsersCredentialApiCredentialsCredNameDelete = async ( } as deleteUsersCredentialApiCredentialsCredNameDeleteResponse; }; -/** - * Returns the information of resources available in different pools. - * @summary Get Resources - */ export type getResourcesApiResourcesGetResponse200 = { data: ResourcesResponse | PoolResourcesResponse; status: 200; @@ -7325,13 +7461,13 @@ export const getGetResourcesApiResourcesGetUrl = (params?: GetResourcesApiResour if (Array.isArray(value) && explodeParameters.includes(key)) { value.forEach((v) => { - normalizedParams.append(key, v === null ? "null" : v.toString()); + normalizedParams.append(key, v === null ? "null" : String(v)); }); return; } if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); + normalizedParams.append(key, value === null ? "null" : String(value)); } }); @@ -7340,6 +7476,10 @@ export const getGetResourcesApiResourcesGetUrl = (params?: GetResourcesApiResour return stringifiedParams.length > 0 ? `/api/resources?${stringifiedParams}` : `/api/resources`; }; +/** + * Returns the information of resources available in different pools. + * @summary Get Resources + */ export const getResourcesApiResourcesGet = async ( params?: GetResourcesApiResourcesGetParams, options?: RequestInit, @@ -7355,10 +7495,6 @@ export const getResourcesApiResourcesGet = async ( return { data, status: res.status, headers: res.headers } as getResourcesApiResourcesGetResponse; }; -/** - * Returns the request resource's information. - * @summary Get One Resource - */ export type getOneResourceApiResourcesNameGetResponse200 = { data: ResourcesResponse; status: 200; @@ -7384,6 +7520,10 @@ export const getGetOneResourceApiResourcesNameGetUrl = (name: string) => { return `/api/resources/${name}`; }; +/** + * Returns the request resource's information. + * @summary Get One Resource + */ export const getOneResourceApiResourcesNameGet = async ( name: string, options?: RequestInit, @@ -7399,14 +7539,6 @@ export const getOneResourceApiResourcesNameGet = async ( return { data, status: res.status, headers: res.headers } as getOneResourceApiResourcesNameGetResponse; }; -/** - * Returns information regarding pools to users. - -If all_pools is set to true, all pools' information will be returned in API response. -Otherwise, only information from pools that the user has access to will be returned -in the response. - * @summary Get Pools - */ export type getPoolsApiPoolGetResponse200 = { data: MinimalPoolConfig; status: 200; @@ -7434,13 +7566,13 @@ export const getGetPoolsApiPoolGetUrl = (params?: GetPoolsApiPoolGetParams) => { if (Array.isArray(value) && explodeParameters.includes(key)) { value.forEach((v) => { - normalizedParams.append(key, v === null ? "null" : v.toString()); + normalizedParams.append(key, v === null ? "null" : String(v)); }); return; } if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); + normalizedParams.append(key, value === null ? "null" : String(value)); } }); @@ -7449,6 +7581,14 @@ export const getGetPoolsApiPoolGetUrl = (params?: GetPoolsApiPoolGetParams) => { return stringifiedParams.length > 0 ? `/api/pool?${stringifiedParams}` : `/api/pool`; }; +/** + * Returns information regarding pools to users. + * + * If all_pools is set to true, all pools' information will be returned in API response. + * Otherwise, only information from pools that the user has access to will be returned + * in the response. + * @summary Get Pools + */ export const getPoolsApiPoolGet = async ( params?: GetPoolsApiPoolGetParams, options?: RequestInit, @@ -7464,9 +7604,6 @@ export const getPoolsApiPoolGet = async ( return { data, status: res.status, headers: res.headers } as getPoolsApiPoolGetResponse; }; -/** - * @summary Get Pool Quotas - */ export type getPoolQuotasApiPoolQuotaGetResponse200 = { data: PoolResponse; status: 200; @@ -7496,13 +7633,13 @@ export const getGetPoolQuotasApiPoolQuotaGetUrl = (params?: GetPoolQuotasApiPool if (Array.isArray(value) && explodeParameters.includes(key)) { value.forEach((v) => { - normalizedParams.append(key, v === null ? "null" : v.toString()); + normalizedParams.append(key, v === null ? "null" : String(v)); }); return; } if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); + normalizedParams.append(key, value === null ? "null" : String(value)); } }); @@ -7511,6 +7648,9 @@ export const getGetPoolQuotasApiPoolQuotaGetUrl = (params?: GetPoolQuotasApiPool return stringifiedParams.length > 0 ? `/api/pool_quota?${stringifiedParams}` : `/api/pool_quota`; }; +/** + * @summary Get Pool Quotas + */ export const getPoolQuotasApiPoolQuotaGet = async ( params?: GetPoolQuotasApiPoolQuotaGetParams, options?: RequestInit, @@ -7526,10 +7666,6 @@ export const getPoolQuotasApiPoolQuotaGet = async ( return { data, status: res.status, headers: res.headers } as getPoolQuotasApiPoolQuotaGetResponse; }; -/** - * This api validates that a workflow is well formed and valid and then submits it. - * @summary Submit Workflow - */ export type submitWorkflowApiPoolPoolNameWorkflowPostResponse200 = { data: SubmitResponse; status: 200; @@ -7560,17 +7696,17 @@ export const getSubmitWorkflowApiPoolPoolNameWorkflowPostUrl = ( const normalizedParams = new URLSearchParams(); Object.entries(params || {}).forEach(([key, value]) => { - const explodeParameters = ["env_vars"]; + const explodeParameters = ["env_vars", "label"]; if (Array.isArray(value) && explodeParameters.includes(key)) { value.forEach((v) => { - normalizedParams.append(key, v === null ? "null" : v.toString()); + normalizedParams.append(key, v === null ? "null" : String(v)); }); return; } if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); + normalizedParams.append(key, value === null ? "null" : String(value)); } }); @@ -7581,9 +7717,13 @@ export const getSubmitWorkflowApiPoolPoolNameWorkflowPostUrl = ( : `/api/pool/${poolName}/workflow`; }; +/** + * This api validates that a workflow is well formed and valid and then submits it. + * @summary Submit Workflow + */ export const submitWorkflowApiPoolPoolNameWorkflowPost = async ( poolName: string, - templateSpecNull: TemplateSpec | null, + templateSpecNull?: TemplateSpec | null, params?: SubmitWorkflowApiPoolPoolNameWorkflowPostParams, options?: RequestInit, ): Promise => { @@ -7600,10 +7740,6 @@ export const submitWorkflowApiPoolPoolNameWorkflowPost = async ( return { data, status: res.status, headers: res.headers } as submitWorkflowApiPoolPoolNameWorkflowPostResponse; }; -/** - * This api restarts a failed workflow and then submits it. - * @summary Restart Workflow - */ export type restartWorkflowApiPoolPoolNameWorkflowWorkflowIdRestartPostResponse200 = { data: SubmitResponse; status: 200; @@ -7634,6 +7770,10 @@ export const getRestartWorkflowApiPoolPoolNameWorkflowWorkflowIdRestartPostUrl = return `/api/pool/${poolName}/workflow/${workflowId}/restart`; }; +/** + * This api restarts a failed workflow and then submits it. + * @summary Restart Workflow + */ export const restartWorkflowApiPoolPoolNameWorkflowWorkflowIdRestartPost = async ( poolName: string, workflowId: string, @@ -7656,9 +7796,6 @@ export const restartWorkflowApiPoolPoolNameWorkflowWorkflowIdRestartPost = async } as restartWorkflowApiPoolPoolNameWorkflowWorkflowIdRestartPostResponse; }; -/** - * @summary Get Notification Settings - */ export type getNotificationSettingsApiProfileSettingsGetResponse200 = { data: ProfileResponse; status: 200; @@ -7686,6 +7823,9 @@ export const getGetNotificationSettingsApiProfileSettingsGetUrl = () => { return `/api/profile/settings`; }; +/** + * @summary Get Notification Settings + */ export const getNotificationSettingsApiProfileSettingsGet = async ( options?: RequestInit, ): Promise => { @@ -7700,9 +7840,6 @@ export const getNotificationSettingsApiProfileSettingsGet = async ( return { data, status: res.status, headers: res.headers } as getNotificationSettingsApiProfileSettingsGetResponse; }; -/** - * @summary Set Notification Settings - */ export type setNotificationSettingsApiProfileSettingsPostResponse200 = { data: unknown; status: 200; @@ -7733,7 +7870,7 @@ export const getSetNotificationSettingsApiProfileSettingsPostUrl = ( Object.entries(params || {}).forEach(([key, value]) => { if (value !== undefined) { - normalizedParams.append(key, value === null ? "null" : value.toString()); + normalizedParams.append(key, value === null ? "null" : String(value)); } }); @@ -7742,6 +7879,9 @@ export const getSetNotificationSettingsApiProfileSettingsPostUrl = ( return stringifiedParams.length > 0 ? `/api/profile/settings?${stringifiedParams}` : `/api/profile/settings`; }; +/** + * @summary Set Notification Settings + */ export const setNotificationSettingsApiProfileSettingsPost = async ( userProfile: UserProfile, params?: SetNotificationSettingsApiProfileSettingsPostParams, @@ -7760,9 +7900,6 @@ export const setNotificationSettingsApiProfileSettingsPost = async ( return { data, status: res.status, headers: res.headers } as setNotificationSettingsApiProfileSettingsPostResponse; }; -/** - * @summary Get Osmo Client Version - */ export type getOsmoClientVersionClientVersionGetResponse200 = { data: unknown; status: 200; @@ -7777,6 +7914,9 @@ export const getGetOsmoClientVersionClientVersionGetUrl = () => { return `/client/version`; }; +/** + * @summary Get Osmo Client Version + */ export const getOsmoClientVersionClientVersionGet = async ( options?: RequestInit, ): Promise => { @@ -7791,11 +7931,6 @@ export const getOsmoClientVersionClientVersionGet = async ( return { data, status: res.status, headers: res.headers } as getOsmoClientVersionClientVersionGetResponse; }; -/** - * To be used for the readiness probe, but not liveness probe. That way, if this method is -slow, no new traffic gets routed, instead of killing the service. - * @summary Health - */ export type healthHealthGetResponse200 = { data: HealthHealthGet200; status: 200; @@ -7810,6 +7945,11 @@ export const getHealthHealthGetUrl = () => { return `/health`; }; +/** + * To be used for the readiness probe, but not liveness probe. That way, if this method is + * slow, no new traffic gets routed, instead of killing the service. + * @summary Health + */ export const healthHealthGet = async (options?: RequestInit): Promise => { const res = await fetch(getHealthHealthGetUrl(), { ...options, @@ -7822,9 +7962,6 @@ export const healthHealthGet = async (options?: RequestInit): Promise { return `/api/version`; }; +/** + * @summary Get Version + */ export const getVersionApiVersionGet = async (options?: RequestInit): Promise => { const res = await fetch(getGetVersionApiVersionGetUrl(), { ...options, @@ -7851,10 +7991,6 @@ export const getVersionApiVersionGet = async (options?: RequestInit): Promise { return `/api/users`; }; +/** + * Returns the values of all users who have submitted a workflow. + * @summary Get Users + */ export const getUsersApiUsersGet = async (options?: RequestInit): Promise => { const res = await fetch(getGetUsersApiUsersGetUrl(), { ...options, @@ -7881,10 +8021,6 @@ export const getUsersApiUsersGet = async (options?: RequestInit): Promise { return `/api/tag`; }; +/** + * Returns all workflow tags. + * @summary Get Available Workflow Tags + */ export const getAvailableWorkflowTagsApiTagGet = async ( options?: RequestInit, ): Promise => { @@ -7913,10 +8053,6 @@ export const getAvailableWorkflowTagsApiTagGet = async ( return { data, status: res.status, headers: res.headers } as getAvailableWorkflowTagsApiTagGetResponse; }; -/** - * Get all the workflow plugins configurations - * @summary Get Workflow Plugins Configs - */ export type getWorkflowPluginsConfigsApiPluginsConfigsGetResponse200 = { data: PluginsConfigOutput; status: 200; @@ -7933,6 +8069,10 @@ export const getGetWorkflowPluginsConfigsApiPluginsConfigsGetUrl = () => { return `/api/plugins/configs`; }; +/** + * Get all the workflow plugins configurations + * @summary Get Workflow Plugins Configs + */ export const getWorkflowPluginsConfigsApiPluginsConfigsGet = async ( options?: RequestInit, ): Promise => { @@ -8212,6 +8352,15 @@ export const getReadWorkflowConfigsApiConfigsWorkflowGetResponseMock = ( client_upload_rate_limit: faker.number.int({ min: 0 }), }, }, + labels_config: { + policy: Array.from({ length: faker.number.int({ min: 1, max: 10 }) }, (_, i) => i + 1).map(() => ({ + key: faker.string.alpha({ length: { min: 10, max: 20 } }), + allow_list: Array.from({ length: faker.number.int({ min: 1, max: 10 }) }, (_, i) => i + 1).map(() => + faker.string.alpha({ length: { min: 10, max: 20 } }), + ), + enforcement: faker.helpers.arrayElement(Object.values(LabelEnforcement)), + })), + }, max_num_tasks: faker.number.int(), max_num_ports_per_task: faker.number.int(), max_retry_per_task: faker.number.int(), @@ -8966,7 +9115,7 @@ export const getGetConfigsHistoryApiConfigsHistoryGetResponseMock = ( overrideResponse: Partial> = {}, ): GetConfigsHistoryResponse => ({ configs: Array.from({ length: faker.number.int({ min: 1, max: 10 }) }, (_, i) => i + 1).map(() => ({ - config_type: faker.helpers.arrayElement(Object.values(SrcLibUtilsConfigHistoryConfigHistoryType)), + config_type: faker.helpers.arrayElement(Object.values(ConfigHistoryType)), name: faker.string.alpha({ length: { min: 10, max: 20 } }), revision: faker.number.int(), username: faker.string.alpha({ length: { min: 10, max: 20 } }), @@ -9300,6 +9449,12 @@ export const getListWorkflowApiWorkflowGetResponseMock = ( ]), app_version: faker.helpers.arrayElement([faker.helpers.arrayElement([faker.number.int(), null]), undefined]), priority: faker.string.alpha({ length: { min: 10, max: 20 } }), + labels: faker.helpers.arrayElement([ + { + [faker.string.alphanumeric(5)]: faker.string.alpha({ length: { min: 10, max: 20 } }), + }, + undefined, + ]), })), more_entries: faker.datatype.boolean(), ...overrideResponse, @@ -9598,6 +9753,18 @@ export const getGetWorkflowApiWorkflowNameGetResponseMock = ( app_version: faker.helpers.arrayElement([faker.helpers.arrayElement([faker.number.int(), null]), undefined]), plugins: { rsync: faker.datatype.boolean() }, priority: faker.string.alpha({ length: { min: 10, max: 20 } }), + labels: faker.helpers.arrayElement([ + { + [faker.string.alphanumeric(5)]: faker.string.alpha({ length: { min: 10, max: 20 } }), + }, + undefined, + ]), + warnings: faker.helpers.arrayElement([ + Array.from({ length: faker.number.int({ min: 1, max: 10 }) }, (_, i) => i + 1).map(() => + faker.string.alpha({ length: { min: 10, max: 20 } }), + ), + undefined, + ]), ...overrideResponse, }); @@ -9924,6 +10091,12 @@ export const getSubmitWorkflowApiPoolPoolNameWorkflowPostResponseMock = ( faker.helpers.arrayElement([faker.string.alpha({ length: { min: 10, max: 20 } }), null]), undefined, ]), + warnings: faker.helpers.arrayElement([ + Array.from({ length: faker.number.int({ min: 1, max: 10 }) }, (_, i) => i + 1).map(() => + faker.string.alpha({ length: { min: 10, max: 20 } }), + ), + undefined, + ]), ...overrideResponse, }); @@ -9947,6 +10120,12 @@ export const getRestartWorkflowApiPoolPoolNameWorkflowWorkflowIdRestartPostRespo faker.helpers.arrayElement([faker.string.alpha({ length: { min: 10, max: 20 } }), null]), undefined, ]), + warnings: faker.helpers.arrayElement([ + Array.from({ length: faker.number.int({ min: 1, max: 10 }) }, (_, i) => i + 1).map(() => + faker.string.alpha({ length: { min: 10, max: 20 } }), + ), + undefined, + ]), ...overrideResponse, }); From 57e80681582cd0cd0da804fea14ece8e14271250 Mon Sep 17 00:00:00 2001 From: Jiaen Ren Date: Tue, 28 Jul 2026 13:45:00 -0700 Subject: [PATCH 09/12] Reformat label UI tests after the generic-key rename Run Prettier on the files touched by the PPP->project rename so format:check (part of ui-build's validate:coverage) passes. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/ui/e2e/journeys/submit-workflow-form.spec.ts | 3 ++- src/ui/e2e/journeys/workflow-detail-overview.spec.ts | 3 ++- src/ui/src/lib/workflow-labels.test.ts | 11 +++++++---- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/ui/e2e/journeys/submit-workflow-form.spec.ts b/src/ui/e2e/journeys/submit-workflow-form.spec.ts index 221eb90ade..009ebeecdb 100644 --- a/src/ui/e2e/journeys/submit-workflow-form.spec.ts +++ b/src/ui/e2e/journeys/submit-workflow-form.spec.ts @@ -275,7 +275,8 @@ test.describe("Submit Workflow Form Validation", () => { }); test("shows workflow policy warnings returned by validation", async ({ page }) => { - const warning = "Workflow is missing label 'project'; add it now to avoid rejected submissions once it is required."; + const warning = + "Workflow is missing label 'project'; add it now to avoid rejected submissions once it is required."; let submittedLabels: string[] | null = null; await page.route("**/api/pool/test-pool/workflow*", (route) => { const url = new URL(route.request().url()); diff --git a/src/ui/e2e/journeys/workflow-detail-overview.spec.ts b/src/ui/e2e/journeys/workflow-detail-overview.spec.ts index d985eb57e6..1f962b452c 100644 --- a/src/ui/e2e/journeys/workflow-detail-overview.spec.ts +++ b/src/ui/e2e/journeys/workflow-detail-overview.spec.ts @@ -371,7 +371,8 @@ test.describe("Workflow Detail Overview — Details Section", () => { // The backend recomputes warnings from the current policy for every // status, including COMPLETED, so users see violations on finished runs. const wfName = "warnings-wf"; - const warning = "Workflow is missing label 'project'; add it now to avoid rejected submissions once it is required."; + const warning = + "Workflow is missing label 'project'; add it now to avoid rejected submissions once it is required."; await setupWorkflowDetail( page, wfName, diff --git a/src/ui/src/lib/workflow-labels.test.ts b/src/ui/src/lib/workflow-labels.test.ts index 5a8f11c00d..ec04d7c53f 100644 --- a/src/ui/src/lib/workflow-labels.test.ts +++ b/src/ui/src/lib/workflow-labels.test.ts @@ -29,10 +29,13 @@ const draft = (key: string, value: string): WorkflowLabelDraft => ({ key, value describe("workflow label drafts", () => { it("sends only labels changed from a resubmitted workflow", () => { expect( - getChangedWorkflowLabelAssignments([draft("project", "robotics"), draft("team", "simulation"), draft("run", "42")], { - project: "robotics", - team: "robotics", - }), + getChangedWorkflowLabelAssignments( + [draft("project", "robotics"), draft("team", "simulation"), draft("run", "42")], + { + project: "robotics", + team: "robotics", + }, + ), ).toEqual(["team=simulation", "run=42"]); }); From 38556a568876bb7b34e600db1488fab962b5877a Mon Sep 17 00:00:00 2001 From: Jiaen Ren Date: Tue, 28 Jul 2026 15:14:29 -0700 Subject: [PATCH 10/12] Name the add-label button consistently for a11y and e2e MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The label editor's other controls use the 'workflow label' phrasing ('Workflow label key/value N', 'Remove workflow label N'), but the add button's accessible name was just 'Add label' — inconsistent, and the resubmit/submit e2e journeys query 'Add workflow label', so the resubmit test could never find the button. Align the button's aria-label and visible text with the convention. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/ui/src/components/workflow/workflow-label-editor.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ui/src/components/workflow/workflow-label-editor.tsx b/src/ui/src/components/workflow/workflow-label-editor.tsx index 8f4f50b7d0..28d4119f0f 100644 --- a/src/ui/src/components/workflow/workflow-label-editor.tsx +++ b/src/ui/src/components/workflow/workflow-label-editor.tsx @@ -95,7 +95,7 @@ export function WorkflowLabelEditor({ type="button" variant="outline" size="sm" - aria-label="Add label" + aria-label="Add workflow label" disabled={disabled || labels.length >= MAX_WORKFLOW_LABELS} onClick={() => onChange([...labels, { key: "", value: "" }])} > @@ -103,7 +103,7 @@ export function WorkflowLabelEditor({ className="size-4" aria-hidden="true" /> - Add label + Add workflow label {error && (

Date: Fri, 31 Jul 2026 15:07:30 -0700 Subject: [PATCH 11/12] Refresh OpenAPI contract against the merged server surface Regenerate openapi.json + generated client/mocks from the current server (B5 admission incl. the help_text->assert_message rename, B6 filters, all now merged). Picks up an assert_message schema field the previously-frozen contract was missing. Verified: type-check, lint, format, vitest (1027), build all pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/ui/openapi.json | 5 +++++ src/ui/src/lib/api/generated.ts | 1 + src/ui/src/mocks/generated-mocks.ts | 2 ++ 3 files changed, 8 insertions(+) diff --git a/src/ui/openapi.json b/src/ui/openapi.json index a287047d5a..ee300c29f9 100644 --- a/src/ui/openapi.json +++ b/src/ui/openapi.json @@ -8745,6 +8745,11 @@ "enforcement": { "$ref": "#/components/schemas/LabelEnforcement", "default": "off" + }, + "assert_message": { + "type": "string", + "title": "Assert Message", + "default": "" } }, "additionalProperties": false, diff --git a/src/ui/src/lib/api/generated.ts b/src/ui/src/lib/api/generated.ts index 051249514b..ef166ed036 100644 --- a/src/ui/src/lib/api/generated.ts +++ b/src/ui/src/lib/api/generated.ts @@ -698,6 +698,7 @@ export interface LabelPolicy { key: string; allow_list?: string[]; enforcement?: LabelEnforcement; + assert_message?: string; } /** diff --git a/src/ui/src/mocks/generated-mocks.ts b/src/ui/src/mocks/generated-mocks.ts index 7575e3c376..9b0619ed00 100644 --- a/src/ui/src/mocks/generated-mocks.ts +++ b/src/ui/src/mocks/generated-mocks.ts @@ -678,6 +678,7 @@ export interface LabelPolicy { key: string; allow_list?: string[]; enforcement?: LabelEnforcement; + assert_message?: string; } /** @@ -8359,6 +8360,7 @@ export const getReadWorkflowConfigsApiConfigsWorkflowGetResponseMock = ( faker.string.alpha({ length: { min: 10, max: 20 } }), ), enforcement: faker.helpers.arrayElement(Object.values(LabelEnforcement)), + assert_message: faker.string.alpha({ length: { min: 10, max: 20 } }), })), }, max_num_tasks: faker.number.int(), From f863dbbc9cec376609844269118fa042cd89242d Mon Sep 17 00:00:00 2001 From: Jiaen Ren Date: Mon, 3 Aug 2026 11:57:51 -0700 Subject: [PATCH 12/12] Address #1227 review: generic warnings display + cleaner submit type - Warnings panel is no longer framed as label-policy-specific: the `warnings` field is generic, so drop the 'Workflow label policy' sub-heading and rename the region to 'Workflow warnings' (cypres: there are other possible warnings than label policies). - Alias the orval-generated `SubmitWorkflowApiPoolPoolNameWorkflowPostParams` to `SubmitWorkflowParams` in the adapter (matching the existing `as WorkflowListEntry` pattern) and use it in actions.ts. Verified: type-check, lint, format, vitest (1027) pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/ui/e2e/journeys/submit-workflow-form.spec.ts | 2 +- src/ui/e2e/journeys/workflow-detail-overview.spec.ts | 5 ++--- .../components/panel/ui/workflow/workflow-details.tsx | 5 ++--- src/ui/src/features/workflows/list/lib/actions.ts | 6 +++--- src/ui/src/lib/api/adapter/types.ts | 2 +- 5 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/ui/e2e/journeys/submit-workflow-form.spec.ts b/src/ui/e2e/journeys/submit-workflow-form.spec.ts index 009ebeecdb..466f11cce8 100644 --- a/src/ui/e2e/journeys/submit-workflow-form.spec.ts +++ b/src/ui/e2e/journeys/submit-workflow-form.spec.ts @@ -274,7 +274,7 @@ test.describe("Submit Workflow Form Validation", () => { await expect(page.getByText("Workflow submitted as yaml-labels")).toBeVisible(); }); - test("shows workflow policy warnings returned by validation", async ({ page }) => { + test("shows workflow warnings returned by validation", async ({ page }) => { const warning = "Workflow is missing label 'project'; add it now to avoid rejected submissions once it is required."; let submittedLabels: string[] | null = null; diff --git a/src/ui/e2e/journeys/workflow-detail-overview.spec.ts b/src/ui/e2e/journeys/workflow-detail-overview.spec.ts index 1f962b452c..50751bc0d8 100644 --- a/src/ui/e2e/journeys/workflow-detail-overview.spec.ts +++ b/src/ui/e2e/journeys/workflow-detail-overview.spec.ts @@ -367,7 +367,7 @@ test.describe("Workflow Detail Overview — Details Section", () => { await expect(page.getByText("Tags", { exact: true })).toBeVisible(); }); - test("shows current workflow policy warnings on completed workflows", async ({ page }) => { + test("shows current workflow warnings on completed workflows", async ({ page }) => { // The backend recomputes warnings from the current policy for every // status, including COMPLETED, so users see violations on finished runs. const wfName = "warnings-wf"; @@ -385,9 +385,8 @@ test.describe("Workflow Detail Overview — Details Section", () => { await page.goto(`/workflows/${wfName}`); await page.waitForLoadState("networkidle"); - const warningRegion = page.getByRole("region", { name: "Workflow policy warnings" }); + const warningRegion = page.getByRole("region", { name: "Workflow warnings" }); await expect(warningRegion).toBeVisible(); - await expect(warningRegion.getByText("Workflow label policy")).toBeVisible(); await expect(warningRegion.getByText(warning)).toBeVisible(); }); diff --git a/src/ui/src/features/workflows/detail/components/panel/ui/workflow/workflow-details.tsx b/src/ui/src/features/workflows/detail/components/panel/ui/workflow/workflow-details.tsx index 62e3a003dd..b7d04a6599 100644 --- a/src/ui/src/features/workflows/detail/components/panel/ui/workflow/workflow-details.tsx +++ b/src/ui/src/features/workflows/detail/components/panel/ui/workflow/workflow-details.tsx @@ -188,12 +188,12 @@ const StatusDisplay = memo(function StatusDisplay({ ); }); -/** Current warn-mode violations recomputed by the API from stored labels and active policy. */ +/** Warnings the API returns for this workflow (currently warn-mode label-policy violations, recomputed from the stored labels and active policy). */ const WorkflowWarnings = memo(function WorkflowWarnings({ warnings }: { warnings: string[] | undefined }) { if (!warnings || warnings.length === 0) return null; return ( -

+

Warnings