diff --git a/backend/fluksio/api/routes/dashboards.py b/backend/fluksio/api/routes/dashboards.py index b4f9a2e..9f4b260 100644 --- a/backend/fluksio/api/routes/dashboards.py +++ b/backend/fluksio/api/routes/dashboards.py @@ -201,6 +201,18 @@ async def delete_dashboard( return Message(message=f"Deleted dashboard '{name}'") +@router.post("/{name}/duplicate", response_model=DashboardDef) +async def duplicate_dashboard(name: str, store: DashboardStoreDep) -> Any: + """Copy a dashboard into a new one that is only a draft, named after it. + + No history limits to apply, as for a new dashboard: nothing published moved. + """ + try: + return await run_in_threadpool(store.duplicate, name) + except DashboardNotFound: + raise HTTPException(status_code=404, detail=f"No dashboard named '{name}'") + + @router.post("/{name}/rename", response_model=DashboardDef) async def rename_dashboard( name: str, body: RenameRequest, store: DashboardStoreDep diff --git a/backend/fluksio/api/routes/flows.py b/backend/fluksio/api/routes/flows.py index 8e8457e..251d2b3 100644 --- a/backend/fluksio/api/routes/flows.py +++ b/backend/fluksio/api/routes/flows.py @@ -480,15 +480,12 @@ def _changes_code(node: NodeDef, old: NodeDef | None) -> bool: ) -def _refuse_code_changes( - controller: FlowController, name: str, definition: FlowDef -) -> None: +def _refuse_code_changes(definition: FlowDef, before: FlowDef | None) -> None: """Refuse a non-superuser a change to code this flow would run. - Wiring, titles, a function node's settings and every other node stay - anyone's. + *before* is the working copy being replaced, None for a new flow. Wiring, + titles, a function node's settings and every other node stay anyone's. """ - before = _read_flow(controller, name) if controller.store.exists(name) else None previous = {node.id: node for node in before.nodes} if before else {} for node in definition.nodes: if _changes_code(node, previous.get(node.id)): @@ -524,7 +521,8 @@ async def save_flow( _check_flavors(definition) _check_origin(definition) if not user.is_superuser: - _refuse_code_changes(controller, name, definition) + before = _read_flow(controller, name) if controller.store.exists(name) else None + _refuse_code_changes(definition, before) try: stored = await run_in_threadpool( @@ -648,6 +646,24 @@ async def rename_flow( return _detail(controller, renamed) +@router.post("/{name}/duplicate", response_model=FlowDetail) +async def duplicate_flow( + name: str, controller: FlowControllerDep, user: CurrentUser +) -> Any: + """Copy a flow into a new one that is only a draft, named after it. + + Nothing to reload: a draft does not run. Its code nodes are new code in + the copy, so copying them is a superuser's, like writing them. + """ + if not user.is_superuser: + _refuse_code_changes(_read_flow(controller, name), None) + try: + copy = await run_in_threadpool(controller.store.duplicate_flow, name) + except FlowNotFound: + raise HTTPException(status_code=404, detail=f"No flow named '{name}'") + return _detail(controller, copy) + + # ----------------------------------------------------------------------------- # Node source # ----------------------------------------------------------------------------- diff --git a/backend/fluksio/flow/dashboards.py b/backend/fluksio/flow/dashboards.py index 45888b8..9ea244c 100644 --- a/backend/fluksio/flow/dashboards.py +++ b/backend/fluksio/flow/dashboards.py @@ -824,6 +824,32 @@ class DashboardStore: self.flows._commit(f"Rename dashboard '{name}' to '{new_name}'") return renamed + def duplicate(self, name: str) -> DashboardDef: + """Copy the working copy into a new dashboard, as a draft only. + + Named ``{name}_copy``, then ``_copy_2`` and on. Unpublished like a new + dashboard, so no panel shows it until someone publishes it. + """ + with self._lock, self.flows._write_lock: + source = self.read(name, draft=True) + new_name, count = f"{name}_copy", 1 + while self.exists(new_name): + count += 1 + new_name = f"{name}_copy_{count}" + copy = source.model_copy( + update={ + "name": new_name, + "title": f"{source.title} (copy)" if source.title else "", + "version": 1, + "has_draft": True, + } + ) + path = self._draft_file(new_name) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(self._dump(copy)) + self.flows._commit(f"Duplicate dashboard '{name}' as '{new_name}'") + return copy + def bindings_for(self, flow: str) -> Bindings: """Every widget bound to a message of ``flow``. diff --git a/backend/fluksio/flow/store.py b/backend/fluksio/flow/store.py index 9edef65..0e4aaca 100644 --- a/backend/fluksio/flow/store.py +++ b/backend/fluksio/flow/store.py @@ -674,6 +674,46 @@ class FlowStore: self._commit(f"Rename flow '{name}' to '{new_name}'") return self.read_flow(new_name, draft=True) + def duplicate_flow(self, name: str) -> FlowDef: + """Copy a flow's working copy into a new flow that is only a draft. + + Like a new flow, the copy runs nothing until someone publishes it. It is + named ``{name}_copy``, then ``_copy_2`` and on. ``origin`` is dropped, + since the copy is a local flow ``fluksio sync`` must not claim, and + ``runtime.json`` stays behind with everything else about running. + """ + with self._write_lock: + source = self.read_flow(name, draft=True) + new_name, count = f"{name}_copy", 1 + # The directory rather than `exists`: anything left in it would + # otherwise end up in the copy. + while self._flow_dir(new_name).exists(): + count += 1 + new_name = f"{name}_copy_{count}" + + copy = source.model_copy( + update={ + "name": new_name, + "title": f"{source.title} (copy)" if source.title else "", + "version": 1, + "origin": None, + } + ) + path = self._draft_file(new_name) + path.parent.mkdir(parents=True) + path.write_text(copy.model_dump_json(indent=2) + "\n") + + # The bodies the source's editor shows: published, edits over them. + # As drafts, so publishing the copy promotes them like a new flow's. + target = self._draft_nodes_dir(new_name) + for folder in (self._flow_dir(name) / "nodes", self._draft_nodes_dir(name)): + for body in sorted(folder.glob("*.py")): + target.mkdir(exist_ok=True) + shutil.copy2(body, target / body.name) + + self._commit(f"Duplicate flow '{name}' as '{new_name}'") + return copy + @staticmethod def _retarget(flow: FlowDef, old_prefix: str, new_prefix: str) -> bool: """Point this flow's cross-flow message names at a renamed flow.""" diff --git a/backend/tests/api/routes/test_code_rights.py b/backend/tests/api/routes/test_code_rights.py index 04c8f91..84ed5fd 100644 --- a/backend/tests/api/routes/test_code_rights.py +++ b/backend/tests/api/routes/test_code_rights.py @@ -90,6 +90,24 @@ def test_a_user_keeps_everything_but_the_code( assert saved.status_code == 200, saved.text +@pytest.mark.usefixtures("save") +def test_duplicating_code_nodes_is_a_superusers( + client: TestClient, + normal_user_token_headers: dict[str, str], + superuser_token_headers: dict[str, str], +) -> None: + url = f"{PREFIX}/{FLOW}/duplicate" + + refused = client.post(url, headers=normal_user_token_headers) + assert refused.status_code == 403 + assert refused.json()["detail"].startswith(CODE_IS_SUPERUSERS) + + copied = client.post(url, headers=superuser_token_headers) + assert copied.status_code == 200, copied.text + name = copied.json()["definition"]["name"] + client.delete(f"{PREFIX}/{name}", headers=superuser_token_headers) + + @pytest.mark.usefixtures("save") def test_node_source_is_a_superusers_to_write( client: TestClient, diff --git a/backend/tests/flow/test_dashboards.py b/backend/tests/flow/test_dashboards.py index f528dcc..2957178 100644 --- a/backend/tests/flow/test_dashboards.py +++ b/backend/tests/flow/test_dashboards.py @@ -123,6 +123,22 @@ def test_an_unpublished_dashboard_can_be_renamed_and_deleted(store: DashboardSto assert not store.exists("home") +def test_a_duplicate_is_an_unpublished_copy_of_the_working_copy( + store: DashboardStore, +): + store.write(default_dashboard("house")) + edited = store.write_draft( + store.read("house", draft=True).model_copy(update={"title": "Edited"}) + ) + + copy = store.duplicate("house") + + assert (copy.name, copy.title, copy.version) == ("house_copy", "Edited (copy)", 1) + assert store.has_draft("house_copy") and not store.is_published("house_copy") + assert store.read("house", draft=True).version == edited.version + assert store.duplicate("house").name == "house_copy_2" + + def test_deleting_and_renaming(store: DashboardStore): store.write(default_dashboard("house")) diff --git a/backend/tests/flow/test_drafts.py b/backend/tests/flow/test_drafts.py index 5f54da5..182ef47 100644 --- a/backend/tests/flow/test_drafts.py +++ b/backend/tests/flow/test_drafts.py @@ -7,7 +7,7 @@ import pytest from fluksio.flow.controller import FlowController from fluksio.flow.messages import MessageSpec -from fluksio.flow.schemas import FlowDef, NodeDef +from fluksio.flow.schemas import FlowDef, FlowOrigin, NodeDef from fluksio.flow.store import FlowStore, StaleVersion SOURCE = "def process():\n return {}\n" @@ -181,3 +181,22 @@ def test_renaming_carries_the_draft_and_repoints_other_drafts(store: FlowStore): assert store.read_flow("display", draft=True).nodes[0].requires[0].name == ( "warmth.temp" ) + + +def test_a_duplicate_is_a_local_draft_of_what_the_editor_shows(store: FlowStore): + store.write_flow(a_flow(title="Warm").model_copy(update={"origin": FlowOrigin()})) + store.write_node_source("heating", "sensor", SOURCE) + store.write_node_source("heating", "sensor", EDITED, draft=True) + store.write_enabled("heating", False) + + copy = store.duplicate_flow("heating") + + assert (copy.name, copy.title, copy.version) == ("heating_copy", "Warm (copy)", 1) + assert copy.origin is None + assert store.has_draft("heating_copy") and not store.is_published("heating_copy") + assert store.read_node_source("heating_copy", "sensor", draft=True) == EDITED + assert not (store.root / "heating_copy" / "runtime.json").exists() + assert store.duplicate_flow("heating").name == "heating_copy_2" + + store.publish_flow("heating_copy", copy.version) + assert store.read_node_source("heating_copy", "sensor") == EDITED diff --git a/frontend/src/client/sdk.gen.ts b/frontend/src/client/sdk.gen.ts index e7a5bb3..63af7da 100644 --- a/frontend/src/client/sdk.gen.ts +++ b/frontend/src/client/sdk.gen.ts @@ -3,7 +3,7 @@ import type { CancelablePromise } from './core/CancelablePromise'; import { OpenAPI } from './core/OpenAPI'; import { request as __request } from './core/request'; -import type { AiReadAiConfigResponse, AiUpdateAiConfigData, AiUpdateAiConfigResponse, AiDeleteAiConfigResponse, AiListAiModelsData, AiListAiModelsResponse, AiReadAiStatusResponse, AiReadControlNodesResponse, AiReadChatResponse, AiChatData, AiChatResponse, AiClearChatResponse, AlertsReadAlertsConfigResponse, AlertsSaveAlertsConfigData, AlertsSaveAlertsConfigResponse, AlertsReadWebpushKeyResponse, AlertsAddWebpushSubscriptionData, AlertsAddWebpushSubscriptionResponse, AlertsRemoveWebpushSubscriptionData, AlertsRemoveWebpushSubscriptionResponse, AlertsTestChannelData, AlertsTestChannelResponse, ArtifactsPutArtifactData, ArtifactsPutArtifactResponse, ArtifactsGetArtifactData, ArtifactsGetArtifactResponse, ArtifactsDeleteArtifactData, ArtifactsDeleteArtifactResponse, CloudReadStatusResponse, CloudEnrollData, CloudEnrollResponse, CloudAddRemoteUserData, CloudAddRemoteUserResponse, CloudReadMembersResponse, CloudLinkMemberData, CloudLinkMemberResponse, CloudRevokeMemberData, CloudRevokeMemberResponse, CloudDisconnectResponse, ConnectorsReadConnectorStateData, ConnectorsReadConnectorStateResponse, ConnectorsClearConnectorStateData, ConnectorsClearConnectorStateResponse, ConnectorsDeleteConnectorStateData, ConnectorsDeleteConnectorStateResponse, DashboardsReadDashboardsResponse, DashboardsReadDashboardData, DashboardsReadDashboardResponse, DashboardsCreateDashboardData, DashboardsCreateDashboardResponse, DashboardsSaveDashboardData, DashboardsSaveDashboardResponse, DashboardsDeleteDashboardData, DashboardsDeleteDashboardResponse, DashboardsGenerateResultsDashboardData, DashboardsGenerateResultsDashboardResponse, DashboardsPublishDashboardData, DashboardsPublishDashboardResponse, DashboardsDiscardDashboardDraftData, DashboardsDiscardDashboardDraftResponse, DashboardsRenameDashboardData, DashboardsRenameDashboardResponse, FlavorsReadFlavorsResponse, FlavorsCreateFlavorData, FlavorsCreateFlavorResponse, FlavorsUpdateFlavorData, FlavorsUpdateFlavorResponse, FlavorsDeleteFlavorData, FlavorsDeleteFlavorResponse, FlowsReadFlowsResponse, FlowsReadNodeTypesResponse, FlowsReadGraphResponse, FlowsReadLibraryResponse, FlowsDeleteSharedNodeData, FlowsDeleteSharedNodeResponse, FlowsReadFlowData, FlowsReadFlowResponse, FlowsSaveFlowData, FlowsSaveFlowResponse, FlowsDeleteFlowData, FlowsDeleteFlowResponse, FlowsPublishFlowData, FlowsPublishFlowResponse, FlowsDiscardDraftData, FlowsDiscardDraftResponse, FlowsRenameFlowData, FlowsRenameFlowResponse, FlowsReadNodeSourceData, FlowsReadNodeSourceResponse, FlowsSaveNodeSourceData, FlowsSaveNodeSourceResponse, FlowsShareNodeData, FlowsShareNodeResponse, FlowsUnshareNodeData, FlowsUnshareNodeResponse, FlowsStartFlowData, FlowsStartFlowResponse, FlowsStopFlowData, FlowsStopFlowResponse, FlowsPauseFlowData, FlowsPauseFlowResponse, FlowsResumeFlowData, FlowsResumeFlowResponse, FlowsStepFlowData, FlowsStepFlowResponse, FlowsValidateFlowData, FlowsValidateFlowResponse, FlowsRunFlowData, FlowsRunFlowResponse, FlowsTriggerNodeData, FlowsTriggerNodeResponse, FlowsCancelNodeData, FlowsCancelNodeResponse, FlowsAcknowledgeNodeErrorData, FlowsAcknowledgeNodeErrorResponse, FlowsReadFlowStateData, FlowsReadFlowStateResponse, FlowsReadMessageHistoryData, FlowsReadMessageHistoryResponse, FlowsIssueSocketTicketResponse, LoginLoginAccessTokenData, LoginLoginAccessTokenResponse, LoginTestTokenResponse, LoginRecoverPasswordData, LoginRecoverPasswordResponse, LoginResetPasswordData, LoginResetPasswordResponse, LoginRecoverPasswordHtmlContentData, LoginRecoverPasswordHtmlContentResponse, MessagesReadMessagesResponse, MessagesPublishMessageData, MessagesPublishMessageResponse, MessagesReadMessageHistoryData, MessagesReadMessageHistoryResponse, ModulesReadModulesResponse, ModulesApplyModulesData, ModulesApplyModulesResponse, ModulesRefreshModulesResponse, OauthRegisterClientData, OauthRegisterClientResponse, OauthAuthorizeValidateData, OauthAuthorizeValidateResponse, OauthAuthorizeData, OauthAuthorizeResponse, OauthTokenData, OauthTokenResponse, OauthReadClientsResponse, OauthRevokeClientData, OauthRevokeClientResponse, ObservabilityReadSummaryResponse, ObservabilityReadTimeseriesData, ObservabilityReadTimeseriesResponse, ObservabilityReadFlowRollupsData, ObservabilityReadFlowRollupsResponse, ObservabilityReadNodeTypeTimingsData, ObservabilityReadNodeTypeTimingsResponse, ObservabilityReadRunsData, ObservabilityReadRunsResponse, ObservabilityReadEventsData, ObservabilityReadEventsResponse, ObservabilityReadDeadLettersData, ObservabilityReadDeadLettersResponse, ObservabilityFlushMetricsResponse, PanelsReadPanelsResponse, PanelsSavePanelsData, PanelsSavePanelsResponse, PanelsStartPairingResponse, PanelsPollPairingData, PanelsPollPairingResponse, PanelsPendingDeviceData, PanelsPendingDeviceResponse, PanelsApprovePairingData, PanelsApprovePairingResponse, PanelsUnpairPanelData, PanelsUnpairPanelResponse, PanelsReadPanelData, PanelsReadPanelResponse, PrivateCreateUserData, PrivateCreateUserResponse, RunsCreateRunData, RunsCreateRunResponse, RunsCreateSweepData, RunsCreateSweepResponse, RunsImportRunsData, RunsImportRunsResponse, RunsReadRunsData, RunsReadRunsResponse, RunsDeleteRunsData, RunsDeleteRunsResponse, RunsReadOverviewResponse, RunsExportMetricsData, RunsExportMetricsResponse, RunsExportRunsData, RunsExportRunsResponse, RunsReadMetricNamesData, RunsReadMetricNamesResponse, RunsReadRunData, RunsReadRunResponse, RunsDeleteRunData, RunsDeleteRunResponse, RunsReadNodeOutputsData, RunsReadNodeOutputsResponse, RunsCancelRunData, RunsCancelRunResponse, RunsRetryRunData, RunsRetryRunResponse, RunsReadMetricsData, RunsReadMetricsResponse, RunsCompareMetricData, RunsCompareMetricResponse, SearchReadSearchIndexResponse, SecretsReadSecretsResponse, SecretsSaveSecretData, SecretsSaveSecretResponse, SecretsDeleteSecretData, SecretsDeleteSecretResponse, UsersReadUsersData, UsersReadUsersResponse, UsersCreateUserData, UsersCreateUserResponse, UsersReadUserMeResponse, UsersDeleteUserMeResponse, UsersUpdateUserMeData, UsersUpdateUserMeResponse, UsersUpdatePasswordMeData, UsersUpdatePasswordMeResponse, UsersRegisterUserData, UsersRegisterUserResponse, UsersReadUserByIdData, UsersReadUserByIdResponse, UsersUpdateUserData, UsersUpdateUserResponse, UsersDeleteUserData, UsersDeleteUserResponse, UtilsTestEmailData, UtilsTestEmailResponse, UtilsReadConfigResponse, UtilsHealthCheckResponse, UtilsHealthResponse, WorkersReadWorkersResponse, WorkersReadResourcesResponse, WorkersReadTokensResponse, WorkersIssueTokenData, WorkersIssueTokenResponse, WorkersRevokeTokenData, WorkersRevokeTokenResponse, WorkersReadRuntimeResponse } from './types.gen'; +import type { AiReadAiConfigResponse, AiUpdateAiConfigData, AiUpdateAiConfigResponse, AiDeleteAiConfigResponse, AiListAiModelsData, AiListAiModelsResponse, AiReadAiStatusResponse, AiReadControlNodesResponse, AiReadChatResponse, AiChatData, AiChatResponse, AiClearChatResponse, AlertsReadAlertsConfigResponse, AlertsSaveAlertsConfigData, AlertsSaveAlertsConfigResponse, AlertsReadWebpushKeyResponse, AlertsAddWebpushSubscriptionData, AlertsAddWebpushSubscriptionResponse, AlertsRemoveWebpushSubscriptionData, AlertsRemoveWebpushSubscriptionResponse, AlertsTestChannelData, AlertsTestChannelResponse, ArtifactsPutArtifactData, ArtifactsPutArtifactResponse, ArtifactsGetArtifactData, ArtifactsGetArtifactResponse, ArtifactsDeleteArtifactData, ArtifactsDeleteArtifactResponse, CloudReadStatusResponse, CloudEnrollData, CloudEnrollResponse, CloudAddRemoteUserData, CloudAddRemoteUserResponse, CloudReadMembersResponse, CloudLinkMemberData, CloudLinkMemberResponse, CloudRevokeMemberData, CloudRevokeMemberResponse, CloudDisconnectResponse, ConnectorsReadConnectorStateData, ConnectorsReadConnectorStateResponse, ConnectorsClearConnectorStateData, ConnectorsClearConnectorStateResponse, ConnectorsDeleteConnectorStateData, ConnectorsDeleteConnectorStateResponse, DashboardsReadDashboardsResponse, DashboardsReadDashboardData, DashboardsReadDashboardResponse, DashboardsCreateDashboardData, DashboardsCreateDashboardResponse, DashboardsSaveDashboardData, DashboardsSaveDashboardResponse, DashboardsDeleteDashboardData, DashboardsDeleteDashboardResponse, DashboardsGenerateResultsDashboardData, DashboardsGenerateResultsDashboardResponse, DashboardsPublishDashboardData, DashboardsPublishDashboardResponse, DashboardsDiscardDashboardDraftData, DashboardsDiscardDashboardDraftResponse, DashboardsDuplicateDashboardData, DashboardsDuplicateDashboardResponse, DashboardsRenameDashboardData, DashboardsRenameDashboardResponse, FlavorsReadFlavorsResponse, FlavorsCreateFlavorData, FlavorsCreateFlavorResponse, FlavorsUpdateFlavorData, FlavorsUpdateFlavorResponse, FlavorsDeleteFlavorData, FlavorsDeleteFlavorResponse, FlowsReadFlowsResponse, FlowsReadNodeTypesResponse, FlowsReadGraphResponse, FlowsReadLibraryResponse, FlowsDeleteSharedNodeData, FlowsDeleteSharedNodeResponse, FlowsReadFlowData, FlowsReadFlowResponse, FlowsSaveFlowData, FlowsSaveFlowResponse, FlowsDeleteFlowData, FlowsDeleteFlowResponse, FlowsPublishFlowData, FlowsPublishFlowResponse, FlowsDiscardDraftData, FlowsDiscardDraftResponse, FlowsRenameFlowData, FlowsRenameFlowResponse, FlowsDuplicateFlowData, FlowsDuplicateFlowResponse, FlowsReadNodeSourceData, FlowsReadNodeSourceResponse, FlowsSaveNodeSourceData, FlowsSaveNodeSourceResponse, FlowsShareNodeData, FlowsShareNodeResponse, FlowsUnshareNodeData, FlowsUnshareNodeResponse, FlowsStartFlowData, FlowsStartFlowResponse, FlowsStopFlowData, FlowsStopFlowResponse, FlowsPauseFlowData, FlowsPauseFlowResponse, FlowsResumeFlowData, FlowsResumeFlowResponse, FlowsStepFlowData, FlowsStepFlowResponse, FlowsValidateFlowData, FlowsValidateFlowResponse, FlowsRunFlowData, FlowsRunFlowResponse, FlowsTriggerNodeData, FlowsTriggerNodeResponse, FlowsCancelNodeData, FlowsCancelNodeResponse, FlowsAcknowledgeNodeErrorData, FlowsAcknowledgeNodeErrorResponse, FlowsReadFlowStateData, FlowsReadFlowStateResponse, FlowsReadMessageHistoryData, FlowsReadMessageHistoryResponse, FlowsIssueSocketTicketResponse, LoginLoginAccessTokenData, LoginLoginAccessTokenResponse, LoginTestTokenResponse, LoginRecoverPasswordData, LoginRecoverPasswordResponse, LoginResetPasswordData, LoginResetPasswordResponse, LoginRecoverPasswordHtmlContentData, LoginRecoverPasswordHtmlContentResponse, MessagesReadMessagesResponse, MessagesPublishMessageData, MessagesPublishMessageResponse, MessagesReadMessageHistoryData, MessagesReadMessageHistoryResponse, ModulesReadModulesResponse, ModulesApplyModulesData, ModulesApplyModulesResponse, ModulesRefreshModulesResponse, OauthRegisterClientData, OauthRegisterClientResponse, OauthAuthorizeValidateData, OauthAuthorizeValidateResponse, OauthAuthorizeData, OauthAuthorizeResponse, OauthTokenData, OauthTokenResponse, OauthReadClientsResponse, OauthRevokeClientData, OauthRevokeClientResponse, ObservabilityReadSummaryResponse, ObservabilityReadTimeseriesData, ObservabilityReadTimeseriesResponse, ObservabilityReadFlowRollupsData, ObservabilityReadFlowRollupsResponse, ObservabilityReadNodeTypeTimingsData, ObservabilityReadNodeTypeTimingsResponse, ObservabilityReadRunsData, ObservabilityReadRunsResponse, ObservabilityReadEventsData, ObservabilityReadEventsResponse, ObservabilityReadDeadLettersData, ObservabilityReadDeadLettersResponse, ObservabilityFlushMetricsResponse, PanelsReadPanelsResponse, PanelsSavePanelsData, PanelsSavePanelsResponse, PanelsStartPairingResponse, PanelsPollPairingData, PanelsPollPairingResponse, PanelsPendingDeviceData, PanelsPendingDeviceResponse, PanelsApprovePairingData, PanelsApprovePairingResponse, PanelsUnpairPanelData, PanelsUnpairPanelResponse, PanelsReadPanelData, PanelsReadPanelResponse, PrivateCreateUserData, PrivateCreateUserResponse, RunsCreateRunData, RunsCreateRunResponse, RunsCreateSweepData, RunsCreateSweepResponse, RunsImportRunsData, RunsImportRunsResponse, RunsReadRunsData, RunsReadRunsResponse, RunsDeleteRunsData, RunsDeleteRunsResponse, RunsReadOverviewResponse, RunsExportMetricsData, RunsExportMetricsResponse, RunsExportRunsData, RunsExportRunsResponse, RunsReadMetricNamesData, RunsReadMetricNamesResponse, RunsReadRunData, RunsReadRunResponse, RunsDeleteRunData, RunsDeleteRunResponse, RunsReadNodeOutputsData, RunsReadNodeOutputsResponse, RunsCancelRunData, RunsCancelRunResponse, RunsRetryRunData, RunsRetryRunResponse, RunsReadMetricsData, RunsReadMetricsResponse, RunsCompareMetricData, RunsCompareMetricResponse, SearchReadSearchIndexResponse, SecretsReadSecretsResponse, SecretsSaveSecretData, SecretsSaveSecretResponse, SecretsDeleteSecretData, SecretsDeleteSecretResponse, UsersReadUsersData, UsersReadUsersResponse, UsersCreateUserData, UsersCreateUserResponse, UsersReadUserMeResponse, UsersDeleteUserMeResponse, UsersUpdateUserMeData, UsersUpdateUserMeResponse, UsersUpdatePasswordMeData, UsersUpdatePasswordMeResponse, UsersRegisterUserData, UsersRegisterUserResponse, UsersReadUserByIdData, UsersReadUserByIdResponse, UsersUpdateUserData, UsersUpdateUserResponse, UsersDeleteUserData, UsersDeleteUserResponse, UtilsTestEmailData, UtilsTestEmailResponse, UtilsReadConfigResponse, UtilsHealthCheckResponse, UtilsHealthResponse, WorkersReadWorkersResponse, WorkersReadResourcesResponse, WorkersReadTokensResponse, WorkersIssueTokenData, WorkersIssueTokenResponse, WorkersRevokeTokenData, WorkersRevokeTokenResponse, WorkersReadRuntimeResponse } from './types.gen'; export class AiService { /** @@ -757,6 +757,29 @@ export class DashboardsService { }); } + /** + * Duplicate Dashboard + * Copy a dashboard into a new one that is only a draft, named after it. + * + * No history limits to apply, as for a new dashboard: nothing published moved. + * @param data The data for the request. + * @param data.name + * @returns DashboardDef_Output Successful Response + * @throws ApiError + */ + public static duplicateDashboard(data: DashboardsDuplicateDashboardData): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/api/v1/dashboards/{name}/duplicate', + path: { + name: data.name + }, + errors: { + 422: 'Validation Error' + } + }); + } + /** * Rename Dashboard * @param data The data for the request. @@ -1081,6 +1104,30 @@ export class FlowsService { }); } + /** + * Duplicate Flow + * Copy a flow into a new one that is only a draft, named after it. + * + * Nothing to reload: a draft does not run. Its code nodes are new code in + * the copy, so copying them is a superuser's, like writing them. + * @param data The data for the request. + * @param data.name + * @returns FlowDetail Successful Response + * @throws ApiError + */ + public static duplicateFlow(data: FlowsDuplicateFlowData): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/api/v1/flows/{name}/duplicate', + path: { + name: data.name + }, + errors: { + 422: 'Validation Error' + } + }); + } + /** * Read Node Source * Read a node's Python source, including unpublished edits. diff --git a/frontend/src/client/types.gen.ts b/frontend/src/client/types.gen.ts index 4beda3a..1e625bc 100644 --- a/frontend/src/client/types.gen.ts +++ b/frontend/src/client/types.gen.ts @@ -1765,6 +1765,12 @@ export type DashboardsDiscardDashboardDraftData = { export type DashboardsDiscardDashboardDraftResponse = (DashboardDef_Output); +export type DashboardsDuplicateDashboardData = { + name: string; +}; + +export type DashboardsDuplicateDashboardResponse = (DashboardDef_Output); + export type DashboardsRenameDashboardData = { name: string; requestBody: fluksio__api__routes__dashboards__RenameRequest; @@ -1847,6 +1853,12 @@ export type FlowsRenameFlowData = { export type FlowsRenameFlowResponse = (FlowDetail); +export type FlowsDuplicateFlowData = { + name: string; +}; + +export type FlowsDuplicateFlowResponse = (FlowDetail); + export type FlowsReadNodeSourceData = { name: string; nodeId: string;