diff --git a/.gitignore b/.gitignore index 5c4c97e10..99117912b 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,8 @@ **/npm-debug.log* /.idea/ **/package-lock.json + +# Cursor IDE +.cursor/ +.cursorrules +.cursorrules.json diff --git a/DSL/Liquibase/changelog/20250127000000_add_endpoints_to_user_step_preference.xml b/DSL/Liquibase/changelog/20250127000000_add_endpoints_to_user_step_preference.xml new file mode 100644 index 000000000..1d5121be2 --- /dev/null +++ b/DSL/Liquibase/changelog/20250127000000_add_endpoints_to_user_step_preference.xml @@ -0,0 +1,18 @@ + + + + + + + + + + \ No newline at end of file diff --git a/DSL/Liquibase/changelog/migrations/20250127000000_add_endpoints_to_user_step_preference.sql b/DSL/Liquibase/changelog/migrations/20250127000000_add_endpoints_to_user_step_preference.sql new file mode 100644 index 000000000..7cdb4bdbc --- /dev/null +++ b/DSL/Liquibase/changelog/migrations/20250127000000_add_endpoints_to_user_step_preference.sql @@ -0,0 +1,4 @@ +-- liquibase formatted sql +-- changeset IgorKrupenja:20250127000000 + +ALTER TABLE user_step_preference ADD COLUMN endpoints UUID[] DEFAULT '{}'; \ No newline at end of file diff --git a/DSL/Liquibase/changelog/migrations/rollback/20250127000000_rollback.sql b/DSL/Liquibase/changelog/migrations/rollback/20250127000000_rollback.sql new file mode 100644 index 000000000..6a5684a5f --- /dev/null +++ b/DSL/Liquibase/changelog/migrations/rollback/20250127000000_rollback.sql @@ -0,0 +1,4 @@ +-- liquibase formatted sql +-- rollback + +ALTER TABLE user_step_preference DROP COLUMN endpoints; \ No newline at end of file diff --git a/DSL/Resql/services/POST/endpoints/get_endpoints_by_service_id.sql b/DSL/Resql/services/POST/endpoints/get_endpoints_by_service_id.sql index ee2e8398c..2de449e67 100644 --- a/DSL/Resql/services/POST/endpoints/get_endpoints_by_service_id.sql +++ b/DSL/Resql/services/POST/endpoints/get_endpoints_by_service_id.sql @@ -3,13 +3,29 @@ WITH LatestEndpoints AS ( FROM endpoints AS e WHERE (e.service_id = :id::uuid OR e.is_common = true) ORDER BY e.endpoint_id, e.id DESC +), +UserPreferences AS ( + SELECT endpoints + FROM user_step_preference + WHERE user_id_code = :user_id_code + ORDER BY created_at DESC + LIMIT 1 ) SELECT - endpoint_id, - name, - type, - is_common, - definitions -FROM LatestEndpoints -WHERE deleted IS FALSE -ORDER BY id DESC; + le.endpoint_id, + le.name, + le.type, + le.is_common, + le.definitions +FROM LatestEndpoints AS le +CROSS JOIN UserPreferences AS up +WHERE le.deleted IS FALSE +ORDER BY + CASE + WHEN up.endpoints IS NULL OR array_length(up.endpoints, 1) = 0 THEN 1 + ELSE array_position(up.endpoints, le.endpoint_id) + END, + CASE + WHEN up.endpoints IS NULL OR array_length(up.endpoints, 1) = 0 THEN le.id + ELSE NULL + END DESC; \ No newline at end of file diff --git a/DSL/Resql/services/POST/endpoints/remove_endpoint_from_preferences.sql b/DSL/Resql/services/POST/endpoints/remove_endpoint_from_preferences.sql new file mode 100644 index 000000000..c68a375bd --- /dev/null +++ b/DSL/Resql/services/POST/endpoints/remove_endpoint_from_preferences.sql @@ -0,0 +1,15 @@ +-- Remove a specific endpoint from all user step preferences +-- This is called when an endpoint is deleted to clean up references + +INSERT INTO user_step_preference (steps, endpoints, user_id_code) +SELECT + steps, + array_remove(endpoints, :endpoint_id::uuid) AS endpoints, + user_id_code +FROM user_step_preference AS usp1 +WHERE endpoints @> ARRAY[:endpoint_id::uuid] + AND created_at = ( + SELECT MAX(created_at) + FROM user_step_preference AS usp2 + WHERE usp2.user_id_code = usp1.user_id_code + ); \ No newline at end of file diff --git a/DSL/Resql/services/POST/endpoints/remove_service_endpoints_from_preferences.sql b/DSL/Resql/services/POST/endpoints/remove_service_endpoints_from_preferences.sql new file mode 100644 index 000000000..019e80fb5 --- /dev/null +++ b/DSL/Resql/services/POST/endpoints/remove_service_endpoints_from_preferences.sql @@ -0,0 +1,23 @@ +-- Remove all non-common endpoints of a deleted service from all user step preferences +-- This is called when a service is deleted to clean up references to its endpoints + +INSERT INTO user_step_preference (steps, endpoints, user_id_code) +SELECT + steps, + array_remove(endpoints, le.endpoint_id) AS endpoints, + user_id_code +FROM user_step_preference AS usp1 +CROSS JOIN ( + SELECT DISTINCT ON (endpoint_id) endpoint_id + FROM endpoints + WHERE service_id = :serviceId::uuid + AND is_common = FALSE + AND deleted = FALSE + ORDER BY endpoint_id, id DESC +) AS le +WHERE usp1.endpoints @> ARRAY[le.endpoint_id] + AND usp1.created_at = ( + SELECT MAX(created_at) + FROM user_step_preference AS usp2 + WHERE usp2.user_id_code = usp1.user_id_code + ); \ No newline at end of file diff --git a/DSL/Resql/services/POST/get-user-step-preferences.sql b/DSL/Resql/services/POST/get-user-step-preferences.sql index 16fe588d9..c2f3650d6 100644 --- a/DSL/Resql/services/POST/get-user-step-preferences.sql +++ b/DSL/Resql/services/POST/get-user-step-preferences.sql @@ -1,4 +1,4 @@ -SELECT steps +SELECT steps, endpoints FROM user_step_preference WHERE user_id_code = :user_id_code ORDER BY created_at DESC diff --git a/DSL/Resql/services/POST/update-user-step-preferences.sql b/DSL/Resql/services/POST/update-user-step-preferences.sql index 98119c0db..b84c6441e 100644 --- a/DSL/Resql/services/POST/update-user-step-preferences.sql +++ b/DSL/Resql/services/POST/update-user-step-preferences.sql @@ -1,2 +1,2 @@ -INSERT INTO user_step_preference(steps, user_id_code) -VALUES(:steps::step_type[], :user_id_code); +INSERT INTO user_step_preference(steps, endpoints, user_id_code) +VALUES(:steps::step_type[], :endpoints::uuid[], :user_id_code); diff --git a/DSL/Ruuter/services/GET/service-by-id.yml b/DSL/Ruuter/services/GET/service-by-id.yml index 6f2827ec5..5509196bb 100644 --- a/DSL/Ruuter/services/GET/service-by-id.yml +++ b/DSL/Ruuter/services/GET/service-by-id.yml @@ -17,6 +17,26 @@ check_for_parameters: - condition: ${incoming.params == null || incoming.params.id == null} next: return_incorrect_request +get_user_info: + call: http.post + args: + url: "[#SERVICE_TIM]/jwt/custom-jwt-userinfo" + contentType: plaintext + headers: + cookie: ${incoming.headers.cookie} + plaintext: "customJwtCookie" + result: res + +check_user_info_response: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: assignIdCode + next: return_unauthorized + +assignIdCode: + assign: + idCode: ${res.response.body.idCode} + get_service_by_id: call: http.post args: @@ -31,6 +51,7 @@ get_endpoints_by_service_id: url: "[#SERVICE_RESQL]/endpoints/get_endpoints_by_service_id" body: id: ${incoming.params.id} + user_id_code: ${idCode} result: endpoints_results prepare_results: @@ -57,3 +78,8 @@ return_incorrect_request: status: 400 return: "Required parameter(s) missing" next: end + +return_unauthorized: + status: 401 + return: "unauthorized" + next: end diff --git a/DSL/Ruuter/services/GET/steps/preferences.yml b/DSL/Ruuter/services/GET/steps/preferences.yml index 05b281116..290f6bb5c 100644 --- a/DSL/Ruuter/services/GET/steps/preferences.yml +++ b/DSL/Ruuter/services/GET/steps/preferences.yml @@ -39,7 +39,7 @@ check_preferences_response: switch: - condition: ${preferences.response.body.length > 0} next: return_preferences - next: seed_default_user_preferences + next: seed_default_user_preferences seed_default_user_preferences: call: http.post @@ -58,11 +58,11 @@ refetch_user_step_preferences: result: refetched_preferences return_refetched_preferences: - return: ${refetched_preferences.response.body[0].steps} + return: ${refetched_preferences.response.body[0]} next: end return_preferences: - return: ${preferences.response.body[0].steps} + return: ${preferences.response.body[0]} next: end return_unauthorized: diff --git a/DSL/Ruuter/services/POST/services/create-endpoint.yml b/DSL/Ruuter/services/POST/services/create-endpoint.yml index 7917956ae..2c73fd454 100644 --- a/DSL/Ruuter/services/POST/services/create-endpoint.yml +++ b/DSL/Ruuter/services/POST/services/create-endpoint.yml @@ -24,7 +24,7 @@ declaration: type: string description: "Service UUID" - field: definitions - type: json + type: object description: "Endpoint definitions" create_endpoint: diff --git a/DSL/Ruuter/services/POST/services/delete-endpoint.yml b/DSL/Ruuter/services/POST/services/delete-endpoint.yml index bf5f6319c..f585495ea 100644 --- a/DSL/Ruuter/services/POST/services/delete-endpoint.yml +++ b/DSL/Ruuter/services/POST/services/delete-endpoint.yml @@ -20,6 +20,14 @@ delete_endpoint: id: ${incoming.body.id} result: res +remove_from_preferences: + call: http.post + args: + url: "[#SERVICE_RESQL]/endpoints/remove_endpoint_from_preferences" + body: + endpoint_id: ${incoming.body.id} + result: preferences_res + return_ok: status: 200 return: "Endpoint deleted" diff --git a/DSL/Ruuter/services/POST/services/delete.yml b/DSL/Ruuter/services/POST/services/delete.yml index 544e8aea4..455fc6828 100644 --- a/DSL/Ruuter/services/POST/services/delete.yml +++ b/DSL/Ruuter/services/POST/services/delete.yml @@ -113,6 +113,15 @@ delete_endpoints_by_service_id: body: serviceId: ${id} result: delete_endpoint_results + next: remove_service_endpoints_from_preferences + +remove_service_endpoints_from_preferences: + call: http.post + args: + url: "[#SERVICE_RESQL]/endpoints/remove_service_endpoints_from_preferences" + body: + serviceId: ${id} + result: remove_preferences_results next: delete_mcq_files delete_mcq_files: diff --git a/DSL/Ruuter/services/POST/services/update-endpoint.yml b/DSL/Ruuter/services/POST/services/update-endpoint.yml index 5b2304a3e..9228079d1 100644 --- a/DSL/Ruuter/services/POST/services/update-endpoint.yml +++ b/DSL/Ruuter/services/POST/services/update-endpoint.yml @@ -21,7 +21,7 @@ declaration: type: string description: "Service UUID" - field: definitions - type: json + type: object description: "Endpoint definitions" params: - field: id diff --git a/DSL/Ruuter/services/POST/steps/preferences.yml b/DSL/Ruuter/services/POST/steps/preferences.yml index 7cf177253..61f727229 100644 --- a/DSL/Ruuter/services/POST/steps/preferences.yml +++ b/DSL/Ruuter/services/POST/steps/preferences.yml @@ -11,10 +11,14 @@ declaration: - field: steps type: string description: "Body field 'steps'" + - field: endpoints + type: string + description: "Body field 'endpoints'" extractRequestData: assign: - steps: ${incoming.body.steps.join(",")} + steps: ${incoming.body.steps.join(",")} + endpoints: ${incoming.body.endpoints.join(",")} get_user_info: call: http.post @@ -42,6 +46,7 @@ update_user_step_preferences: url: "[#SERVICE_RESQL]/update-user-step-preferences" body: steps: "{${steps}}" + endpoints: "{${endpoints}}" user_id_code: ${idCode} result: update_preferences_res @@ -54,7 +59,7 @@ get_user_step_preferences: result: preferences return_preferences: - return: ${preferences.response.body[0].steps} + return: ${preferences.response.body[0]} next: end return_unauthorized: diff --git a/GUI/src/assets/images/api-icon-tag.svg b/GUI/src/assets/images/api-icon-tag.svg deleted file mode 100644 index e2ed74604..000000000 --- a/GUI/src/assets/images/api-icon-tag.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/GUI/src/components/ApiEndpoint/ApiEndpoint.module.scss b/GUI/src/components/ApiEndpoint/ApiEndpoint.module.scss index ebdc35fd6..b2b597661 100644 --- a/GUI/src/components/ApiEndpoint/ApiEndpoint.module.scss +++ b/GUI/src/components/ApiEndpoint/ApiEndpoint.module.scss @@ -17,13 +17,32 @@ align-items: center; width: 100%; min-width: 0; + gap: 8px; .label { - padding-left: 8px; overflow: hidden; text-overflow: ellipsis; min-width: 0; } + + .apiBadge, + .publicBadge { + color: white; + font-size: 10px; + font-weight: bold; + padding: 2px 6px; + border-radius: 2px; + white-space: nowrap; + text-transform: uppercase; + } + + .apiBadge { + background-color: #8bb4d5; + } + + .publicBadge { + background-color: #6b9bc5; + } } .loader { diff --git a/GUI/src/components/ApiEndpoint/index.tsx b/GUI/src/components/ApiEndpoint/index.tsx index 004db3811..0ee21799f 100644 --- a/GUI/src/components/ApiEndpoint/index.tsx +++ b/GUI/src/components/ApiEndpoint/index.tsx @@ -4,20 +4,22 @@ import Icon from "components/Icon"; import Track from "components/Track"; import { FC, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; -import { MdDeleteOutline, MdOutlineEdit } from "react-icons/md"; +import { MdDeleteOutline, MdOutlineEdit, MdDragIndicator } from "react-icons/md"; import { Link } from "react-router-dom"; import { deleteEndpoint } from "resources/api-constants"; import useServiceStore from "store/new-services.store"; import useToastStore from "store/toasts.store"; import { Step, StepType } from "types"; import { EndpointData } from "types/endpoint"; -import apiIconTag from "../../assets/images/api-icon-tag.svg"; + import styles from "./ApiEndpoint.module.scss"; import api from "../../services/api-dev"; import Modal from "components/Modal"; import ApiEndpointCard from "components/ApiEndpointCard"; import { saveEndpoints } from "services/service-builder"; import { removeTrailingUnderscores } from "utils/string-util"; +import { useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; interface RelatedService { serviceId: string; @@ -32,6 +34,14 @@ interface ApiEndpointProps { const ApiEndpoint: FC = ({ step, onClick }) => { const { t } = useTranslation(); + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: step.id }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + }; + const [relatedServices, setRelatedServices] = useState([]); const [showEditModal, setShowEditModal] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false); @@ -182,18 +192,24 @@ const ApiEndpoint: FC = ({ step, onClick }) => { )} onClick(step)} - draggable={false} > -
- {step.type === "user-defined" && } - {step.label} -
+ +
+ } size="small" /> +
+
+ {step.type === "user-defined" && API} + {step.data?.isCommon && {t("serviceFlow.apiElements.public")}} + {step.label} +
+ = ({ name={`${requestTab.tab}-raw-data`} label={""} defaultValue={tabRawData[requestTab.tab]} - onBlur={() => updateEndpointRawData(tabRawData, endpointData.id, parentEndpointId)} + onBlur={() => updateEndpointRawData(tabRawData, endpoint)} onChange={(event) => { setJsonError(undefined); tabRawData[requestTab.tab] = event.target.value; @@ -341,7 +367,7 @@ const RequestVariables: React.FC = ({ <> = ({ onNameChange, onCommonChange, }) => { - const { deleteEndpoint, changeServiceEndpointType, getAvailableRequestValues } = useServiceStore(); + const { changeServiceEndpointType, getAvailableRequestValues } = useServiceStore(); const [selectedTab, setSelectedTab] = useState(EndpointEnv.Live); const [endpointName, setEndpointName] = useState(endpoint.name); const [isCommonEndpoint, setIsCommonEndpoint] = useState(endpoint.isCommon ?? false); @@ -44,7 +43,7 @@ const ApiEndpointCard: FC = ({ const getTabTriggerClasses = (tab: EndpointEnv) => `tab-group__tab-btn ${selectedTab === tab ? "active" : ""}`; - const requestValues = useMemo(() => getAvailableRequestValues(endpoint.endpointId), []); + const requestValues = useMemo(() => getAvailableRequestValues(endpoint), []); return ( = ({ {t("newService.endpoint.single")} - {isDeletable && ( - - )} {[EndpointEnv.Live, EndpointEnv.Test].map((env) => { return ( @@ -83,7 +76,7 @@ const ApiEndpointCard: FC = ({ onSelectionChange={(selection) => { setOption(selection); endpoint.type = selection?.value as EndpointType; - changeServiceEndpointType(endpoint.endpointId, (selection?.value ?? "custom") as EndpointType); + changeServiceEndpointType(endpoint, (selection?.value ?? "custom") as EndpointType); }} defaultValue={option?.value} /> diff --git a/GUI/src/components/Dropdown/Dropdown.scss b/GUI/src/components/Dropdown/Dropdown.scss index d383e23fd..48aec92ae 100644 --- a/GUI/src/components/Dropdown/Dropdown.scss +++ b/GUI/src/components/Dropdown/Dropdown.scss @@ -1,36 +1,45 @@ -@import 'src/styles/tools/spacing'; -@import 'src/styles/tools/color'; -@import 'src/styles/settings/variables/other'; -@import 'src/styles/settings/variables/typography'; +@import "src/styles/tools/spacing"; +@import "src/styles/tools/color"; +@import "src/styles/settings/variables/other"; +@import "src/styles/settings/variables/typography"; .dropdown { - background-color: white; - border-radius: 6px; - box-shadow: 0px 10px 38px -10px rgba(22, 23, 24, 0.35); - z-index: 1000; - resize: both; - overflow: auto; - width: 400px; - min-width: 400px; - max-width: 600px; - min-height: 500px; - max-height: 550px; + background-color: white; + border-radius: 6px; + box-shadow: 0px 10px 38px -10px rgba(22, 23, 24, 0.35); + z-index: 1000; + resize: both; + overflow: auto; + width: 400px; + min-width: 400px; + max-width: 600px; + min-height: 235px; + max-height: 75vh; + height: auto; + display: flex; + flex-direction: column; - &__header { - padding: get-spacing(haapsalu); - margin-bottom: 12px; - display: flex; - align-items: center; - border-radius: $veera-radius-m $veera-radius-m 0 0; - background-color: get-color(black-coral-0); - border-bottom: 1px solid get-color(black-coral-2); - } - - &__title { - font-size: large; - } - - &__content { - padding: 10px; - } + &__header { + padding: get-spacing(haapsalu); + margin-bottom: 12px; + display: flex; + align-items: center; + border-radius: $veera-radius-m $veera-radius-m 0 0; + background-color: get-color(black-coral-0); + border-bottom: 1px solid get-color(black-coral-2); + flex-shrink: 0; } + + &__title { + font-size: large; + } + + &__content { + padding: 10px; + flex: 1; + overflow-y: auto; + min-height: 0; + display: flex; + flex-direction: column; + } +} diff --git a/GUI/src/components/Dropdown/index.tsx b/GUI/src/components/Dropdown/index.tsx index fb5d1a25b..e748925fa 100644 --- a/GUI/src/components/Dropdown/index.tsx +++ b/GUI/src/components/Dropdown/index.tsx @@ -18,14 +18,14 @@ const Dropdown: FC = ({ open, onOpenChange, trigger, title, child {trigger} - - - {title} - - -
{children}
+ + + {title} + + +
{children}
diff --git a/GUI/src/components/Flow/EdgeTypes/AddEndpointModal.tsx b/GUI/src/components/Flow/EdgeTypes/AddEndpointModal.tsx new file mode 100644 index 000000000..6839122fa --- /dev/null +++ b/GUI/src/components/Flow/EdgeTypes/AddEndpointModal.tsx @@ -0,0 +1,90 @@ +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { v4 as uuid } from "uuid"; +import { ApiEndpointCard, Button, Modal, Track } from "components"; +import { EndpointData } from "../../../types/endpoint/endpoint-data"; +import useToastStore from "store/toasts.store"; +import useServiceStore from "store/new-services.store"; +import { saveEndpoints } from "../../../services/service-builder"; + +interface AddEndpointModalProps { + onClose: () => void; + onUpdatePreferences: (endpointIds: string[]) => void; + currentEndpointIds: string[]; +} + +const AddEndpointModal: React.FC = ({ onClose, onUpdatePreferences, currentEndpointIds }) => { + const { t } = useTranslation(); + const [endpoint, setEndpoint] = useState({ + endpointId: uuid(), + name: "", + definitions: [], + isNew: true, + }); + const [endpointName, setEndpointName] = useState(""); + const [endpointNameExists, setEndpointNameExists] = useState(false); + const [isCommonEndpoint, setIsCommonEndpoint] = useState(false); + const [isCreatingEndpoint, setIsCreatingEndpoint] = useState(false); + + const handleClose = () => { + setEndpoint({ endpointId: uuid(), name: "", definitions: [], isNew: true }); + setEndpointName(""); + setIsCommonEndpoint(false); + setIsCreatingEndpoint(false); + onClose(); + }; + + const handleCreate = () => { + const passedEndpoint = endpoint; + passedEndpoint.name = endpointName; + passedEndpoint.isCommon = isCommonEndpoint; + setIsCreatingEndpoint(true); + + saveEndpoints( + [passedEndpoint], + () => { + useServiceStore.getState().addEndpoint(passedEndpoint); + // Add the new endpoint to user preferences + const newEndpointIds = [...currentEndpointIds, passedEndpoint.endpointId]; + onUpdatePreferences(newEndpointIds); + + handleClose(); + useToastStore.getState().success({ title: t("serviceFlow.apiElements.createSuccess") }); + setIsCreatingEndpoint(false); + }, + (error) => { + console.error(`Error creating API endpoint: ${error}`); + useToastStore.getState().error({ title: t("serviceFlow.apiElements.createError") }); + setIsCreatingEndpoint(false); + } + ); + }; + + return ( + + + + + + + + + + ); +}; + +export default AddEndpointModal; diff --git a/GUI/src/components/Flow/EdgeTypes/CustomEdge.tsx b/GUI/src/components/Flow/EdgeTypes/CustomEdge.tsx index 68eaf9970..7efd0340c 100644 --- a/GUI/src/components/Flow/EdgeTypes/CustomEdge.tsx +++ b/GUI/src/components/Flow/EdgeTypes/CustomEdge.tsx @@ -1,11 +1,11 @@ import { BaseEdge, EdgeLabelRenderer, EdgeProps, getBezierPath } from "@xyflow/react"; import { CSSProperties, memo, useEffect, useState } from "react"; -import { ApiEndpointCard, Button, Collapsible, Dropdown, Modal, StepElement, Track } from "components"; +import { Collapsible, Dropdown, StepElement, Track } from "components"; import useServiceStore from "store/new-services.store"; import ApiEndpoint from "components/ApiEndpoint"; +import AddEndpointModal from "./AddEndpointModal"; import { useTranslation } from "react-i18next"; import { Step, stepsLabels, StepType } from "types"; -import { v4 as uuid } from "uuid"; import { arrayMove, SortableContext, @@ -25,11 +25,19 @@ import { import { userStepPreferences } from "resources/api-constants"; import api from "services/api"; import useEdgeAdd from "hooks/flow/useEdgeAdd"; -import { EndpointData } from "types/endpoint"; -import { saveEndpoints } from "services/service-builder"; import useToastStore from "store/toasts.store"; import { useParams } from "react-router-dom"; +function reorderElements(elements: T[], activeId: string | number, overId: string | number): T[] { + const oldIndex = elements.findIndex((item: any) => item.id === activeId); + const newIndex = elements.findIndex((item: any) => item.id === overId); + return arrayMove(elements, oldIndex, newIndex); +} + +function getEndpointIds(elements: Step[]): string[] { + return elements.map((e) => e.data!.endpointId); +} + function CustomEdge({ id, label, @@ -42,7 +50,6 @@ function CustomEdge({ style, markerEnd, }: EdgeProps) { - const [edgePath, edgeCenterX, edgeCenterY] = getBezierPath({ sourceX, sourceY, @@ -54,53 +61,76 @@ function CustomEdge({ const { t } = useTranslation(); const [allElements, setAllElements] = useState([]); + const [apiElements, setApiElements] = useState([]); const [dropdownOpen, setDropdownOpen] = useState(false); - const steps = useServiceStore((state) => state.mapEndpointsToSteps()); - const contentStyle: CSSProperties = { overflowY: "auto", maxHeight: "245px" }; + const contentStyle: CSSProperties = { + overflowY: "auto", + maxHeight: "calc(30vh - 42px)", + minHeight: "80px", + }; const [isAddEndpointModalVisible, setIsAddEndpointModalVisible] = useState(false); - const [isCreatingEndpoint, setIsCreatingEndpoint] = useState(false); - const [endpointNameExists, setEndpointNameExists] = useState(false); - const [endpoint, setEndpoint] = useState({ - endpointId: uuid(), - name: "", - definitions: [], - isNew: true, - }); const { id: idParam } = useParams(); - const [endpointName, setEndpointName] = useState(endpoint.name ?? ""); - const [isCommonEndpoint, setIsCommonEndpoint] = useState(endpoint.isCommon ?? false); const { setHasUnsavedChanges } = useServiceStore(); const stepPreferences = useServiceStore((state) => state.stepPreferences); + const mapEndpointsToSteps = useServiceStore((state) => state.mapEndpointsToSteps); + const endpoints = useServiceStore((state) => state.endpoints); const onEdgeAdd = useEdgeAdd(id); useEffect(() => { const elements: Step[] = []; stepPreferences.forEach((preference, index) => { - elements.push({ - id: index, - label: t(`${stepsLabels[preference as StepType]}`), - type: preference as StepType, - }); + // Add more steps when they are ready + const allowedSteps = [ + StepType.Condition, + StepType.Assign, + StepType.MultiChoiceQuestion, + StepType.FinishingStepRedirect, + StepType.FinishingStepEnd, + ]; + + if (allowedSteps.includes(preference as StepType)) { + elements.push({ + id: index, + label: t(`${stepsLabels[preference as StepType]}`), + type: preference as StepType, + }); + } }); setAllElements(elements); }, [stepPreferences]); + useEffect(() => { + const steps = mapEndpointsToSteps(); + setApiElements(steps); + // endpoints in the dependency array below needed to re-run when new endpoints are added + }, [mapEndpointsToSteps, endpoints]); + function handleDragEnd(event: DragEndEvent) { const { active, over } = event; if (over && active.id !== over.id) { setAllElements((elements) => { - const oldIndex = elements.findIndex((item) => item.id === active.id); - const newIndex = elements.findIndex((item) => item.id === over.id); - const newElements = arrayMove(elements, oldIndex, newIndex); + const newElements = reorderElements(elements, active.id, over.id); updateStepPreference(newElements); return newElements; }); } } + function handleApiDragEnd(event: DragEndEvent) { + const { active, over } = event; + + if (over && active.id !== over.id) { + const currentElements = apiElements; + const newElements = reorderElements(currentElements, active.id, over.id); + setApiElements(newElements); + const endpointIds = getEndpointIds(newElements); + updateEndpointPreference(endpointIds); + } + } + const sensors = useSensors( useSensor(PointerSensor), useSensor(KeyboardSensor, { @@ -111,6 +141,14 @@ function CustomEdge({ function updateStepPreference(steps: Step[]) { api.post(userStepPreferences(), { steps: steps.map((e) => e.type), + endpoints: getEndpointIds(apiElements), + }); + } + + function updateEndpointPreference(endpointIds: string[]) { + api.post(userStepPreferences(), { + steps: stepPreferences, + endpoints: endpointIds, }); } @@ -135,41 +173,8 @@ function CustomEdge({ } > + {/* All elements */} - { - if (!idParam) { - useToastStore.getState().error({ - title: t("newService.toast.servieNotFound"), - message: t("newService.toast.serviceNotFoundEndpointsMessage"), - }); - } else { - setIsAddEndpointModalVisible(true); - } - }} - > - {steps.length > 0 && ( - - {steps.map((step) => ( - { - onEdgeAdd(step).then(() => { - useServiceStore.getState().loadEndpointsResponseVariables(); - }); - setDropdownOpen(false); - setHasUnsavedChanges(true); - }} - /> - ))} - - )} - - + + {/* API elements */} + + { + if (!idParam) { + useToastStore.getState().error({ + title: t("newService.toast.serviceNotFound"), + message: t("newService.toast.serviceNotFoundEndpointsMessage"), + }); + } else { + setIsAddEndpointModalVisible(true); + } + }} + > + {apiElements.length > 0 && ( + + + {apiElements.map((step) => ( + { + onEdgeAdd(step).then(() => { + useServiceStore.getState().loadEndpointsResponseVariables(); + }); + setDropdownOpen(false); + setHasUnsavedChanges(true); + }} + /> + ))} + + + )} + + + + {/* Add endpoint modal */} {isAddEndpointModalVisible && ( - { - setEndpoint({ endpointId: uuid(), name: "", definitions: [], isNew: true }); - setIsAddEndpointModalVisible(false); - }} - > - - - - - - - - + setIsAddEndpointModalVisible(false)} + onUpdatePreferences={updateEndpointPreference} + currentEndpointIds={getEndpointIds(apiElements)} + /> )} diff --git a/GUI/src/components/FlowElementsPopup/PreviousVariables.tsx b/GUI/src/components/FlowElementsPopup/PreviousVariables.tsx index 8e30315b4..d84494be0 100644 --- a/GUI/src/components/FlowElementsPopup/PreviousVariables.tsx +++ b/GUI/src/components/FlowElementsPopup/PreviousVariables.tsx @@ -12,7 +12,7 @@ import { getTypeColor, isObject } from "utils/object-util"; import Tooltip from "../Tooltip"; import { v4 } from "uuid"; import { getHelperTooltips } from "utils/constants"; -import { datesVariables, helperVariables } from "resources/variables-constants"; +import { datesVariables, environmentVariables, helperVariables } from "resources/variables-constants"; import { Node, Edge } from "@xyflow/react"; import { NodeDataProps } from "types/service-flow"; @@ -20,9 +20,9 @@ type PreviousVariablesProps = { readonly nodeId: string; }; -// Unique key for input element, used below to identify it +// Unique key for predefined elements, used below to identify it // All other assign element keys are UUIDs -const INPUT_ELEMENT_KEY = "-1"; +const predefinedInputKeys = ['-1', '-2']; const PreviousVariables: FC = ({ nodeId }) => { const { t } = useTranslation(); @@ -68,16 +68,26 @@ const PreviousVariables: FC = ({ nodeId }) => { // Get Assign variables const assignNodes: Node[] = previousNodes.filter((node) => node.data.stepType === StepType.Assign) as Node[] ?? []; const assignElements = assignNodes.map((node) => node.data.assignElements).flat(); - const inputElement: Assign = { - id: INPUT_ELEMENT_KEY, - key: "input", - value: stringToTemplate("incoming.body.input"), - // Can only be a string array, see trigger-service.yaml in Buerokratt-Chatbot - // Value is not known at this point, so passing a dummy to correctly infer type - data: [], - }; + const predefinedInputElements: Assign[] = [ + { + id: predefinedInputKeys[0], + key: "input", + value: stringToTemplate("incoming.body.input"), + // Can only be a string array, see trigger-service.yaml in Buerokratt-Chatbot + // Value is not known at this point, so passing a dummy to correctly infer type + data: [], + }, + { + id: predefinedInputKeys[1], + key: "Empty Content Type", + value: stringToTemplate(""), + // Can only be a string array, see trigger-service.yaml in Buerokratt-Chatbot + // Value is not known at this point, so passing a dummy to correctly infer type + data: [], + }, + ]; - setAssignedVariables([...assignElements, inputElement, ...newAssignElements]); + setAssignedVariables([...assignElements, ...predefinedInputElements, ...newAssignElements]); }, [endpointsVariables, newAssignElements]); function getCurrentBranchNodesUp(nodes: Node[], edges: Edge[], currentNode: Node) { @@ -121,6 +131,15 @@ const PreviousVariables: FC = ({ nodeId }) => { /> )} + + { const typeColor = getTypeColor(variable?.value); - return isObject(variable.data) && variable.id !== INPUT_ELEMENT_KEY ? ( + return isObject(variable.data) && !predefinedInputKeys.includes(variable.id) ? ( ) : ( - + = ({ isCommon = false }) => { .deleteSelectedService( () => setIsDeletePopupVisible(false), t("overview.service.toast.deleted"), - t("overview.service.toast.failed.delete") + t("overview.service.toast.failed.delete"), + pagination, + sorting ); }; diff --git a/GUI/src/i18n/en/common.json b/GUI/src/i18n/en/common.json index f95cc2474..8e555764b 100644 --- a/GUI/src/i18n/en/common.json +++ b/GUI/src/i18n/en/common.json @@ -232,7 +232,8 @@ "unsupported": "Sorry, we currently only support GET and POST requests.", "insertName": "Insert endpoint name...", "type": "Endpoint type", - "nameAlreadyExists": "API title must be unique" + "nameAlreadyExists": "API title must be unique", + "formatJson": "Format JSON" }, "trainingModuleSetup": "Training module setup", "serviceSetup": "Service setup", @@ -257,7 +258,7 @@ "saveConfigFailed": "Failed to save config", "testResultSuccess": "Test result- success", "testResultError": "Test result - error", - "servieNotFound": "Service not found", + "serviceNotFound": "Service not found", "serviceNotFoundEndpointsMessage": "Please save the service first to be able to add endpoints", "serviceNameAlreadyExists": "Service name already exists", "elementNameAlreadyExists": "Element name already exists", @@ -280,7 +281,7 @@ "openNewWebpage": "Open new webpage", "fileGeneration": "File generation", "fileSigning": "File signing", - "conversationEnd": "End conversation", + "serviceEnd": "End service", "redirectConversationToSupport": "Direct to Customer Support", "rules": "Rules", "rasaRules": "Rasa Rules", @@ -342,6 +343,9 @@ "customFormat": "Custom format" }, "noName": "", + "environmentVariables": { + "title": "Environment Variables" + }, "helpers": { "title": "Tools", "map": "Change items", @@ -387,7 +391,8 @@ "deleteSuccess": "API element deleted successfully", "deleteError": "Failed to delete API element", "editSuccess": "API element edited successfully", - "editError": "Failed to edit API element" + "editError": "Failed to edit API element", + "public": "Public" }, "multiChoiceQuestion": { "questionPlaceholder": "Enter a question", diff --git a/GUI/src/i18n/et/common.json b/GUI/src/i18n/et/common.json index fe5e4e355..5eb608e44 100644 --- a/GUI/src/i18n/et/common.json +++ b/GUI/src/i18n/et/common.json @@ -232,7 +232,8 @@ "unsupported": "Vabandust, praegu toetame ainult GET ja POST päringud", "insertName": "Sisesta otspunkti nimetus...", "type": "Otspunkti tüüp", - "nameAlreadyExists": "API pealkiri peab olema unikaalne" + "nameAlreadyExists": "API pealkiri peab olema unikaalne", + "formatJson": "Formateeri JSON" }, "trainingModuleSetup": "Avaleht", "serviceSetup": "Teenuse seadistamine", @@ -257,7 +258,7 @@ "saveConfigFailed": "Seadete salvestamine ebaõnnestus", "testResultSuccess": "Testi tulemus - edu", "testResultError": "Testi tulemus - viga", - "servieNotFound": "Teenust ei leitud", + "serviceNotFound": "Teenust ei leitud", "serviceNotFoundEndpointsMessage": "Salvesta teenus kõigepealt, et saaksid lisada otspunkte", "serviceNameAlreadyExists": "Teenuse nimi on juba olemas", "elementNameAlreadyExists": "Elementi nimi on juba olemas", @@ -280,7 +281,7 @@ "openNewWebpage": "Uue veebilehe avamine", "fileGeneration": "Faili genereerimine", "fileSigning": "Faili allkirjastamine", - "conversationEnd": "Vestluse lõpetamine", + "serviceEnd": "Teenuse lõpetamine", "redirectConversationToSupport": "Klienditeenindusse suunamine", "rules": "Reeglid", "rasaRules": "Rasa reeglid", @@ -330,6 +331,9 @@ "previousVariables": { "assignElements": "Määra elemendid", "noName": "", + "environmentVariables": { + "title": "Keskkonnamuutujad" + }, "dates": { "title": "Kuupäevad", "currentDate": "Praegune kuupäev", @@ -387,7 +391,8 @@ "deleteSuccess": "API element kustutatud", "deleteError": "API elementi kustutamine ebaõnnestus", "editSuccess": "API element edukalt muudetud", - "editError": "API elemendi muutmine ebaõnnestus" + "editError": "API elemendi muutmine ebaõnnestus", + "public": "Avalik" }, "multiChoiceQuestion": { "title": "Mitmevalikuline küsimus", diff --git a/GUI/src/resources/variables-constants.ts b/GUI/src/resources/variables-constants.ts index 8de7b4314..b7cde650a 100644 --- a/GUI/src/resources/variables-constants.ts +++ b/GUI/src/resources/variables-constants.ts @@ -3,10 +3,16 @@ import { DATE_CONSTANTS, HELPERS_CONSTANTS } from "utils/constants"; import { stringToTemplate } from "utils/string-util"; import { v4 } from "uuid"; -const createTemplate = (id: string, key: string, value: string, tooltip: string | undefined = undefined): Assign => ({ +const createTemplate = ( + id: string, + key: string, + value: string, + tooltip: string | undefined = undefined, + valueFormat: "plain" | "formatted" = "formatted" +): Assign => ({ id, key, - value: stringToTemplate(value), + value: valueFormat === 'formatted' ? stringToTemplate(value) : value, tooltip, }); @@ -37,3 +43,8 @@ export const helperVariables: Assign[] = [ createTemplate(v4(), `${helpersTrPath}.reduce`, HELPERS_CONSTANTS.REDUCE), createTemplate(v4(), `${helpersTrPath}.mapAndJoin`, HELPERS_CONSTANTS.MAP_AND_JOIN), ]; + +export const environmentVariables: Assign[] = [ + createTemplate(v4(), "XTR", "[#XTR]", undefined, 'plain'), + createTemplate(v4(), "Open Search", "[#OPENSEARCH]", undefined, 'plain'), +]; diff --git a/GUI/src/services/service-builder.ts b/GUI/src/services/service-builder.ts index c6ffeee5c..dd4588dd0 100644 --- a/GUI/src/services/service-builder.ts +++ b/GUI/src/services/service-builder.ts @@ -3,16 +3,10 @@ import { Group, Rule } from "components/FlowElementsPopup/RuleBuilder/types"; import i18next, { t } from "i18next"; import { NodeHtmlMarkdown } from "node-html-markdown"; import { Edge, Node } from "@xyflow/react"; -import { - createEndpoint, - createNewService, - editService, - testService, - updateEndpoint, -} from "resources/api-constants"; +import { createEndpoint, createNewService, editService, testService, updateEndpoint } from "resources/api-constants"; import useServiceStore from "store/new-services.store"; import useToastStore from "store/toasts.store"; -import { Step, StepType } from "types"; +import { StepType } from "types"; import { EndpointData, EndpointVariableData } from "types/endpoint"; import api from "../services/api-dev"; import { NodeDataProps } from "types/service-flow"; @@ -24,7 +18,6 @@ export async function saveEndpoints(endpoints: EndpointData[], onSuccess?: () => const tasks: Promise[] = []; const serviceId = useServiceStore.getState().serviceId; - for (const endpoint of endpoints) { const selectedEndpointType = endpoint.definitions.find((e) => e.isSelected); if (!selectedEndpointType) continue; @@ -60,7 +53,6 @@ async function createEndpointAndUpdateState(endpoint: EndpointData): Promise { }; export const saveFlow = async ({ - steps, name, edges, nodes, @@ -140,7 +131,7 @@ export const saveFlow = async ({ status = "ready", }: SaveFlowConfig) => { try { - let yamlContent = getYamlContent(nodes, edges, steps, name, description); + let yamlContent = getYamlContent(nodes, edges, name, description); const mcqNodes = nodes.filter( (node) => node.data?.stepType === StepType.MultiChoiceQuestion @@ -151,7 +142,7 @@ export const saveFlow = async ({ 0, nodes.findIndex((node) => node.data?.stepType === StepType.MultiChoiceQuestion) + 1 ); - yamlContent = getYamlContent(nodesUpToFirstMcq, edges, steps, name, description); + yamlContent = getYamlContent(nodesUpToFirstMcq, edges, name, description); } await saveService( @@ -179,7 +170,7 @@ export const saveFlow = async ({ ); await saveService( - getYamlContent(branchNodes, branchEdges, steps, serviceName, description), + getYamlContent(branchNodes, branchEdges, serviceName, description), { name: serviceName, serviceId, description, slot, isCommon, nodes, edges, isNewService } as SaveFlowConfig, false, status @@ -232,7 +223,7 @@ async function saveService( .catch(onError); } -function getYamlContent(nodes: Node[], edges: Edge[], steps: Step[], name: string, description: string): any { +function getYamlContent(nodes: Node[], edges: Edge[], name: string, description: string): any { const allRelations: any[] = []; nodes.forEach((node) => { @@ -292,8 +283,28 @@ function getYamlContent(nodes: Node[], edges: Edge[], steps: Step[], name: strin accepts: "json", returns: "json", namespace: "service", + allowList: { + body: [ + { + field: "chatId", + type: "string", + description: "The chat ID for the message", + }, + { + field: "authorId", + type: "string", + description: "The author ID for the message", + }, + { + field: "input", + type: "object", + description: "The Input from the user", + }, + ], + }, }); + const firstNode = nodes.find((node) => node.type === "custom"); finishedFlow.set("prepare", { assign: { chatId: "${incoming.body.chatId}", @@ -304,18 +315,9 @@ function getYamlContent(nodes: Node[], edges: Edge[], steps: Step[], name: strin result: "", }, }, - next: "get_secrets", - }); - - const firstNode = nodes.find((node) => node.type === "custom"); - finishedFlow.set("get_secrets", { - call: "http.get", - args: { - url: `${import.meta.env.REACT_APP_API_URL}/secrets-with-priority`, - }, - result: "secrets", next: firstNode ? toSnakeCase(firstNode.data.label?.toString() ?? "format_messages") : "format_messages", }); + try { allRelations.forEach((r) => { const [parentNodeId, childNodeId] = r.split(","); @@ -356,7 +358,7 @@ function getYamlContent(nodes: Node[], edges: Edge[], steps: Step[], name: strin } const nextStep = childNode ? toSnakeCase(childNode.data.label ?? "format_messages") : "format_messages"; - const template = getTemplate(steps, parentNode, parentStepName, nextStep); + const template = getTemplate(parentNode, parentStepName, nextStep); finishedFlow.set(parentStepName, template); }); @@ -432,7 +434,7 @@ function handleTextField( finishedFlow: Map, parentStepName: string, parentNode: Node, - childNode: Node | undefined, + childNode: Node | undefined ) { const htmlToMarkdown = new NodeHtmlMarkdown({ textReplace: [ @@ -493,7 +495,7 @@ function handleAssignStep( parentNode: Node, finishedFlow: Map, parentStepName: string, - childNode: Node | undefined, + childNode: Node | undefined ) { const invalidElementsExist = hasInvalidElements(parentNode.data.assignElements ?? []); const isInvalid = @@ -518,7 +520,7 @@ function handleEndpointStep( parentNode: Node, finishedFlow: Map, parentStepName: string, - childNode: Node | undefined, + childNode: Node | undefined ) { const endpointDefinition = parentNode.data.endpoint?.definitions[0]; const paramsVariables = endpointDefinition?.params?.variables; @@ -531,6 +533,7 @@ function handleEndpointStep( args: { url: endpointDefinition?.url?.split("?")[0] ?? "", }, + result: `${parentNode.data.endpoint?.name.replaceAll(" ", "_")}_res`, next: childNode ? toSnakeCase(childNode.data.label ?? "format_messages") : "format_messages", }; @@ -562,7 +565,7 @@ function handleMultiChoiceQuestion( finishedFlow: Map, parentStepName: string, parentNode: Node, - childNode: Node | undefined, + childNode: Node | undefined ) { return finishedFlow.set(parentStepName, { assign: { @@ -610,7 +613,7 @@ const getNestedPreDefinedEndpointVariables = (variable: EndpointVariableData, re } }; -const getTemplate = (steps: Step[], node: Node, stepName: string, nextStep?: string) => { +const getTemplate = (node: Node, stepName: string, nextStep?: string) => { const data = getTemplateDataFromNode(node); return { @@ -687,13 +690,11 @@ export const saveFlowClick = async (status: "draft" | "ready" = "ready") => { const description = useServiceStore.getState().description; const slot = useServiceStore.getState().slot; const isCommon = useServiceStore.getState().isCommon; - const steps = useServiceStore.getState().mapEndpointsToSteps(); const isNewService = useServiceStore.getState().isNewService; const edges = useServiceStore.getState().edges; const nodes = useServiceStore.getState().nodes; await saveFlow({ - steps, name: !name ? `${t("newService.defaultServiceName").toString()}_${format(new Date(), "dd_MM_yyyy_HH_mm_ss")}` : name, diff --git a/GUI/src/store/new-services.store.ts b/GUI/src/store/new-services.store.ts index 58ab34872..3c6c1f69f 100644 --- a/GUI/src/store/new-services.store.ts +++ b/GUI/src/store/new-services.store.ts @@ -1,6 +1,16 @@ import { create } from "zustand"; import { v4 as uuid } from "uuid"; -import { Edge, EdgeChange, Node, NodeChange, ReactFlowInstance, applyEdgeChanges, applyNodeChanges, getIncomers, getOutgoers } from "@xyflow/react"; +import { + Edge, + EdgeChange, + Node, + NodeChange, + ReactFlowInstance, + applyEdgeChanges, + applyNodeChanges, + getIncomers, + getOutgoers, +} from "@xyflow/react"; import { EndpointData, EndpointEnv, EndpointTab, PreDefinedEndpointEnvVariables } from "types/endpoint"; import { getCommonEndpoints, @@ -77,19 +87,15 @@ interface ServiceStoreState { loadService: (id?: string, resetState?: boolean) => Promise | undefined>; loadCommonEndpoints: () => Promise; loadStepPreferences: () => Promise; - getAvailableRequestValues: (endpointId: string) => PreDefinedEndpointEnvVariables; + getAvailableRequestValues: (endpoint: EndpointData) => PreDefinedEndpointEnvVariables; onNameChange: (endpointId: string, oldName: string, newName: string) => void; - changeServiceEndpointType: (id: string, type: EndpointType) => void; + changeServiceEndpointType: (endpoint: EndpointData, type: EndpointType) => void; mapEndpointsToSteps: () => Step[]; selectedTab: EndpointEnv; setSelectedTab: (tab: EndpointEnv) => void; isLive: () => boolean; - updateEndpointRawData: ( - rawData: RequestVariablesTabsRawData, - endpointDataId?: string, - parentEndpointId?: string - ) => void; - updateEndpointData: (data: RequestVariablesTabsRowsData, endpointDataId?: string, parentEndpointId?: string) => void; + updateEndpointRawData: (rawData: RequestVariablesTabsRawData, endpoint?: EndpointData) => void; + updateEndpointData: (data: RequestVariablesTabsRowsData, endpoint?: EndpointData) => void; resetState: () => void; resetAssign: () => void; resetRules: () => void; @@ -248,12 +254,14 @@ const useServiceStore = create((set, get, store) => ({ try { const instance = get().reactFlowInstance; if (!instance) return; - const endpointNodes = instance.getNodes().filter((node) => node.data.stepType === StepType.UserDefined) as Node[]; + const endpointNodes = instance + .getNodes() + .filter((node) => node.data.stepType === StepType.UserDefined) as Node[]; if (endpointNodes.length === 0) { set({ endpointsResponseVariables: [] }); return; - }; - + } + const endpointsFromNodes = endpointNodes.map((node) => node.data.endpoint); const requests = endpointsFromNodes.flatMap((e) => e?.definitions.map((endpoint) => ({ @@ -283,8 +291,14 @@ const useServiceStore = create((set, get, store) => ({ }); } + chips.push({ + name: "Status Code", + value: `${endpoint?.name.replaceAll(" ", "_")}_res.response.statusCodeValue`, + data: `${endpoint?.name.replaceAll(" ", "_")}_res.response.statusCodeValue`, + }); + const variable: EndpointResponseVariable = { - name: endpoint?.name ?? '', + name: endpoint?.name ?? "", chips: chips, }; @@ -472,8 +486,10 @@ const useServiceStore = create((set, get, store) => ({ }, loadStepPreferences: async () => { try { - const response = await api.get<{ response: string[] }>(userStepPreferences()); - set({ stepPreferences: response.data.response }); + const response = await api.get<{ response: { steps: string[]; endpoints: string[] } }>(userStepPreferences()); + set({ + stepPreferences: response.data.response.steps ?? [], + }); } catch (error) { console.error("Failed to load step preferences:", error); } @@ -499,15 +515,13 @@ const useServiceStore = create((set, get, store) => ({ const taraVariables = Object.keys(data).map((key) => `{{TARA.${key}}}`); get().addProductionVariables(taraVariables); }, - getAvailableRequestValues: (endpointId: string) => { - const variables = get() - .endpoints.filter((endpoint) => endpoint.endpointId !== endpointId) - .map((endpoint) => ({ - id: endpoint.endpointId, - name: endpoint.name, - response: endpoint.definitions.find((x) => x.isSelected)?.response ?? [], - })) - .flatMap(({ id, name, response }) => response?.map((x) => `{{${name === "" ? id : name}.${x.name}}}`)); + getAvailableRequestValues: (endpoint: EndpointData) => { + const selectedDefinition = endpoint.definitions.find((x) => x.isSelected); + const responseVariables = selectedDefinition?.response ?? []; + + const variables = responseVariables.map( + (x) => `{{${endpoint.name === "" ? endpoint.endpointId : endpoint.name}.${x.name}}}` + ); return { prod: [...variables, ...get().availableVariables.prod], @@ -539,17 +553,8 @@ const useServiceStore = create((set, get, store) => ({ }, })); }, - changeServiceEndpointType: (id: string, type: EndpointType) => { - const endpoints = get().endpoints.map((x) => { - if (x.endpointId !== id) return x; - return { - ...x, - type, - definitions: [], - }; - }); - - set({ endpoints }); + changeServiceEndpointType: (endpoint: EndpointData, type: EndpointType) => { + endpoint.type = type; }, mapEndpointsToSteps: (): Step[] => { return get() @@ -566,22 +571,15 @@ const useServiceStore = create((set, get, store) => ({ data: endpoint, })); }, - setEndpoints: (callback) => { - set((state) => ({ - endpoints: callback(state.endpoints), - })); - }, + setEndpoints: () => {}, selectedTab: EndpointEnv.Live, setSelectedTab: (tab: EndpointEnv) => set({ selectedTab: tab }), isLive: () => get().selectedTab === EndpointEnv.Live, - updateEndpointRawData: (data: RequestVariablesTabsRawData, endpointId?: string, parentEndpointId?: string) => { - if (!endpointId) return; - const live = get().isLive() ? "value" : "testValue"; + updateEndpointRawData: (data: RequestVariablesTabsRawData, endpoint?: EndpointData) => { + if (!endpoint) return; + const live = "value"; - const endpoints = JSON.parse(JSON.stringify(get().endpoints)) as EndpointData[]; - const defEndpoint = endpoints - .find((x) => x.endpointId === parentEndpointId) - ?.definitions.find((x) => x.id === endpointId); + const defEndpoint = endpoint.definitions[0]; for (const key in data) { if (defEndpoint?.[key as EndpointTab]) { @@ -589,18 +587,13 @@ const useServiceStore = create((set, get, store) => ({ } } - set({ - endpoints, - }); + endpoint.definitions[0] = defEndpoint; + return endpoint; }, - updateEndpointData: (data: RequestVariablesTabsRowsData, endpointId?: string, parentEndpointId?: string) => { - if (!endpointId) return; + updateEndpointData: (data: RequestVariablesTabsRowsData, endpoint?: EndpointData) => { + if (!endpoint) return; - const live = get().isLive() ? "value" : "testValue"; - const endpoints = JSON.parse(JSON.stringify(get().endpoints)) as EndpointData[]; - const defEndpoint = endpoints - .find((x) => x.endpointId === parentEndpointId) - ?.definitions.find((x) => x.id === endpointId); + const defEndpoint = endpoint.definitions[0]; if (!defEndpoint) return; @@ -617,21 +610,20 @@ const useServiceStore = create((set, get, store) => ({ name: row.variable, type: "custom", required: false, - [live]: row.value, + value: row.value, }); } } for (const variable of keyedDefEndpoint?.variables ?? []) { const updatedVariable = data[key as EndpointTab]!.find((updated) => updated.endpointVariableId === variable.id); - variable[live] = updatedVariable?.value; variable.name = updatedVariable?.variable ?? variable.name; + variable.value = updatedVariable?.value ?? variable.value; } } - set({ - endpoints, - }); + endpoint.definitions[0] = defEndpoint; + return endpoint; }, reactFlowInstance: null, setReactFlowInstance: (reactFlowInstance) => set({ reactFlowInstance }), diff --git a/GUI/src/store/services.store.ts b/GUI/src/store/services.store.ts index 795af56df..5d362524d 100644 --- a/GUI/src/store/services.store.ts +++ b/GUI/src/store/services.store.ts @@ -37,7 +37,13 @@ interface ServiceStoreState { sorting: SortingState ) => Promise; checkServiceIntentConnection: (onConnected: (response: Trigger) => void, onNotConnected: () => void) => Promise; - deleteSelectedService: (onEnd: () => void, successMessage: string, errorMessage: string) => Promise; + deleteSelectedService: ( + onEnd: () => void, + successMessage: string, + errorMessage: string, + pagination: PaginationState, + sorting: SortingState + ) => Promise; requestServiceIntentConnection: ( onEnd: () => void, successMessage: string, @@ -212,7 +218,7 @@ const useServiceListStore = create((set, get, store) => ({ onNotConnected(); } }, - deleteSelectedService: async (onEnd, successMessage, errorMessage) => { + deleteSelectedService: async (onEnd, successMessage, errorMessage, pagination, sorting) => { const selectedService = get().selectedService; if (!selectedService) return; @@ -222,8 +228,8 @@ const useServiceListStore = create((set, get, store) => ({ type: selectedService?.type, }); useToastStore.getState().success({ title: successMessage }); - await useServiceListStore.getState().loadServicesList({ pageIndex: 0, pageSize: 10 }, []); - await useServiceListStore.getState().loadCommonServicesList({ pageIndex: 0, pageSize: 10 }, []); + await useServiceListStore.getState().loadServicesList(pagination, sorting); + await useServiceListStore.getState().loadCommonServicesList(pagination, sorting); } catch (error) { console.error(error); useToastStore.getState().error({ title: errorMessage }); diff --git a/GUI/src/types/request-variables/request-variables-table-columns.ts b/GUI/src/types/request-variables/request-variables-table-columns.ts index 3f04629fe..1b41b7366 100644 --- a/GUI/src/types/request-variables/request-variables-table-columns.ts +++ b/GUI/src/types/request-variables/request-variables-table-columns.ts @@ -1,5 +1,12 @@ export type RequestVariablesTableColumns = { + id: string; + isNameEditable: boolean; required: boolean; - value: any; - variable: string; + nestedLevel: number; + arrayType?: string; + description?: string; + endpointVariableId?: string; + type?: string; + value?: string; + variable?: string; }; diff --git a/GUI/src/types/step.ts b/GUI/src/types/step.ts index b08adc129..e7c5504bc 100644 --- a/GUI/src/types/step.ts +++ b/GUI/src/types/step.ts @@ -22,10 +22,9 @@ export const stepsLabels: Record = { [StepType.FileSign]: "serviceFlow.element.fileSigning", [StepType.Step]: "serviceFlow.element.step", [StepType.Rule]: "serviceFlow.element.rule", - [StepType.FinishingStepEnd]: "serviceFlow.element.conversationEnd", + [StepType.FinishingStepEnd]: "serviceFlow.element.serviceEnd", [StepType.FinishingStepRedirect]: "serviceFlow.element.redirectConversationToSupport", [StepType.UserDefined]: "serviceFlow.element.userDefined", [StepType.RasaRules]: "serviceFlow.element.rasaRules", [StepType.SiGa]: "serviceFlow.element.siga", }; - diff --git a/GUI/src/utils/string-util.ts b/GUI/src/utils/string-util.ts index 4305f3c92..23e3a1098 100644 --- a/GUI/src/utils/string-util.ts +++ b/GUI/src/utils/string-util.ts @@ -3,7 +3,7 @@ export const isTemplate = (value: string | number) => { }; export const stringToTemplate = (value: string | number) => { - return "${" + value + "}"; + return value ? "${" + value + "}" : '${""}'; }; export const templateToString = (value: string | number) => {