From 89c965dab5bf8ccf6dd6d89035f7fe097127b6f6 Mon Sep 17 00:00:00 2001 From: Michael Sekamanya Date: Tue, 9 Apr 2024 17:53:48 -0700 Subject: [PATCH] Allow update datasource --- nesis/api/core/controllers/__init__.py | 8 +- nesis/api/core/controllers/datasources.py | 95 +++-- nesis/api/core/services/datasources.py | 101 +++++- .../core/controllers/test_datasources.py | 94 ++++- .../controllers/test_datasources_roles.py | 338 ++++++++++++++++++ .../core/controllers/test_management_roles.py | 2 +- .../src/components/inputs/SquareButton.js | 4 + .../Datasources/DatasourcesDetailPage.js | 27 +- .../Settings/Datasources/DatasourcesPage.js | 23 +- .../src/pages/Settings/Roles/RolesPage.js | 21 +- .../client/src/pages/Settings/SettingPage.js | 5 + .../src/pages/Settings/Users/UsersPage.js | 5 +- nesis/frontend/server/api/datasources.js | 17 +- nesis/frontend/server/api/datasources.spec.js | 42 +++ nesis/frontend/server/util/test-util.js | 2 + nesis/rag/Dockerfile | 2 +- 16 files changed, 679 insertions(+), 107 deletions(-) create mode 100644 nesis/api/tests/core/controllers/test_datasources_roles.py diff --git a/nesis/api/core/controllers/__init__.py b/nesis/api/core/controllers/__init__.py index c2454f1..c207839 100644 --- a/nesis/api/core/controllers/__init__.py +++ b/nesis/api/core/controllers/__init__.py @@ -1,7 +1,7 @@ -GET = "GET" -POST = "POST" -DELETE = "DELETE" -PUT = "PUT" +GET: str = "GET" +POST: str = "POST" +DELETE: str = "DELETE" +PUT: str = "PUT" from .api import app diff --git a/nesis/api/core/controllers/datasources.py b/nesis/api/core/controllers/datasources.py index b78329c..2c7e0c4 100644 --- a/nesis/api/core/controllers/datasources.py +++ b/nesis/api/core/controllers/datasources.py @@ -1,6 +1,6 @@ from flask import request, jsonify -from . import GET, POST, DELETE +import nesis.api.core.controllers as controllers from .api import app, error_message import logging @@ -14,18 +14,21 @@ _LOG = logging.getLogger(__name__) -@app.route("/v1/datasources", methods=[POST, GET]) +@app.route("/v1/datasources", methods=[controllers.POST, controllers.GET]) def operate_datasources(): token = get_bearer_token(request.headers.get("Authorization")) try: - if request.method == POST: - result = services.datasource_service.create( - token=token, datasource=request.json - ) - return jsonify(result.to_dict()) - else: - results = services.datasource_service.get(token=token) - return jsonify({"items": [item.to_dict() for item in results]}) + match request.method: + case controllers.POST: + result = services.datasource_service.create( + token=token, datasource=request.json + ) + return jsonify(result.to_dict()) + case controllers.GET: + results = services.datasource_service.get(token=token) + return jsonify({"items": [item.to_dict() for item in results]}) + case _: + raise Exception("Should never be reached") except util.ServiceException as se: return jsonify(error_message(str(se))), 400 except util.UnauthorizedAccess: @@ -39,49 +42,43 @@ def operate_datasources(): return jsonify(error_message("Server error")), 500 -@app.route("/v1/datasources/", methods=[GET, DELETE]) +@app.route( + "/v1/datasources/", + methods=[controllers.GET, controllers.DELETE, controllers.PUT], +) def operate_datasource(datasource_id): token = get_bearer_token(request.headers.get("Authorization")) - try: - if request.method == GET: - results = services.datasource_service.get( - token=token, datasource_id=datasource_id - ) - if len(results) != 0: - return jsonify(results[0].to_dict()) - else: - return ( - jsonify( - error_message( - f"Datasource {datasource_id} not found", message_type="WARN" - ) - ), - 404, - ) - else: - services.datasource_service.delete(token=token, datasource_id=datasource_id) - return jsonify(success=True) - except util.ServiceException as se: - return jsonify(error_message(str(se))), 400 - except util.UnauthorizedAccess: - return jsonify(error_message("Unauthorized access")), 401 - except util.PermissionException: - return jsonify(error_message("Forbidden resource")), 403 - except: - _LOG.exception("Error getting user") - return jsonify(error_message("Server error")), 500 - -@app.route("/v1/datasources//dataobjects/", methods=[GET]) -def operate_dataobject(datasource, dataobject): - token = get_bearer_token(request.headers.get("Authorization")) try: - if request.method == GET: - results = services.dataobject_service.get( - token=token, datasource=datasource, dataobject=dataobject - ) - return jsonify(results.to_dict()) - + match request.method: + case controllers.GET: + results = services.datasource_service.get( + token=token, datasource_id=datasource_id + ) + if len(results) != 0: + return jsonify(results[0].to_dict()) + else: + return ( + jsonify( + error_message( + f"Datasource {datasource_id} not found", + message_type="WARN", + ) + ), + 404, + ) + case controllers.DELETE: + services.datasource_service.delete( + token=token, datasource_id=datasource_id + ) + return jsonify(success=True) + case controllers.PUT: + result = services.datasource_service.update( + token=token, datasource=request.json, datasource_id=datasource_id + ) + return jsonify(result.to_dict()) + case _: + raise Exception("Should never be reached really") except util.ServiceException as se: return jsonify(error_message(str(se))), 400 except util.UnauthorizedAccess: diff --git a/nesis/api/core/services/datasources.py b/nesis/api/core/services/datasources.py index e68dc3d..a878dd4 100644 --- a/nesis/api/core/services/datasources.py +++ b/nesis/api/core/services/datasources.py @@ -19,6 +19,7 @@ ServiceException, is_valid_resource_name, has_valid_keys, + PermissionException, ) _LOG = logging.getLogger(__name__) @@ -115,21 +116,11 @@ def get(self, **kwargs): session = DBSession() try: - self._authorized( - session=session, token=kwargs.get("token"), action=Action.READ - ) - # Get datasources this user is authorized to access - authorized_datasources: list[RoleAction] = services.authorized_resources( - self._session_service, - session=session, - token=kwargs.get("token"), - action=Action.READ, - resource_type=objects.ResourceType.DATASOURCES, + datasources = self._authorized_resources( + session=session, action=Action.READ, token=kwargs.get("token") ) - datasources = {ds.resource for ds in authorized_datasources} - session.expire_on_commit = False query = session.query(Datasource) if datasource_id: @@ -143,6 +134,20 @@ def get(self, **kwargs): if session: session.close() + def _authorized_resources(self, token, session, action, resource=None): + authorized_datasources: list[RoleAction] = services.authorized_resources( + self._session_service, + session=session, + token=token, + action=action, + resource_type=objects.ResourceType.DATASOURCES, + ) + datasources = {ds.resource for ds in authorized_datasources} + if resource and resource not in datasources: + raise PermissionException("Access to resource denied") + + return {ds.resource for ds in authorized_datasources} + @staticmethod def get_datasources(source_type: str = None) -> list[Datasource]: session = DBSession() @@ -165,12 +170,7 @@ def delete(self, **kwargs): session = DBSession() try: - self._authorized( - session=session, token=kwargs.get("token"), action=Action.DELETE - ) - session.expire_on_commit = False - datasource = ( session.query(Datasource) .filter(Datasource.uuid == datasource_id) @@ -178,6 +178,13 @@ def delete(self, **kwargs): ) if datasource: + self._authorized_resources( + session=session, + action=Action.DELETE, + token=kwargs.get("token"), + resource=datasource.name, + ) + session.delete(datasource) session.commit() except Exception as e: @@ -188,4 +195,62 @@ def delete(self, **kwargs): session.close() def update(self, **kwargs): - raise NotImplementedError("Invalid operation on datasource") + """ + Update the datasource. The payload only contains fields that we intend to update. Any missing fields will be + ignored. + :param kwargs: datasource the datasource object as a dict + :param kwargs: id The datasource id + :return: + """ + datasource = kwargs["datasource"] + datasource_id = kwargs["datasource_id"] + + session = DBSession() + + try: + datasource_record: Datasource = ( + session.query(Datasource) + .filter(Datasource.uuid == datasource_id) + .first() + ) + + if datasource is None: + raise ServiceException("Datasource not found") + + self._authorized_resources( + session=session, + action=Action.UPDATE, + token=kwargs.get("token"), + resource=datasource_record.name, + ) + + session.expire_on_commit = False + + source_type: str = datasource.get("type") + if source_type is not None: + try: + datasource_type = DatasourceType[source_type.upper()] + datasource_record.type = datasource_type + except Exception: + raise ServiceException("Invalid datasource type") + + if datasource.get("connection"): + try: + connection = validators.validate_datasource_connection(datasource) + datasource_record.connection = connection + except ValueError as ve: + raise ServiceException(ve) + + if not has_valid_keys(connection): + raise ServiceException("Missing connection details") + + session.merge(datasource_record) + session.commit() + return datasource_record + except Exception as e: + session.rollback() + self._LOG.exception(f"Error when creating setting") + raise + finally: + if session: + session.close() diff --git a/nesis/api/tests/core/controllers/test_datasources.py b/nesis/api/tests/core/controllers/test_datasources.py index 49e0806..9f60b7c 100644 --- a/nesis/api/tests/core/controllers/test_datasources.py +++ b/nesis/api/tests/core/controllers/test_datasources.py @@ -25,6 +25,11 @@ def client(): return cloud_app.test_client() +@pytest.fixture +def tc(): + return ut.TestCase() + + def get_admin_session(client): admin_data = { "name": "s3 documents", @@ -37,7 +42,7 @@ def get_admin_session(client): ).json -def test_datasources(client): +def test_create_datasource_invalid_input(client, tc): # Get the prediction payload = { "type": "minio", @@ -94,7 +99,23 @@ def test_datasources(client): ) assert 400 == response.status_code, response.json - assert 400 == response.status_code, response.json + +def test_create_datasource(client, tc): + # Get the prediction + payload = { + "type": "minio", + "name": "finance6", + "connection": { + "user": "caikuodda", + "password": "some.password", + "host": "localhost", + "port": "5432", + "database": "initdb", + }, + } + + admin_session = get_admin_session(client=client) + response = client.post( f"/v1/datasources", headers=tests.get_header(token=admin_session["token"]), @@ -134,3 +155,72 @@ def test_datasources(client): headers=tests.get_header(token=admin_session["token"]), ) assert 404 == response.status_code, response.json + + +def test_update_datasources(client, tc): + # Create a datasource + payload = { + "type": "minio", + "name": "finance6", + "connection": { + "user": "caikuodda", + "password": "some.password", + "host": "localhost", + "port": "5432", + "database": "initdb", + }, + } + + admin_session = get_admin_session(client=client) + + response = client.post( + f"/v1/datasources", + headers=tests.get_header(token=admin_session["token"]), + data=json.dumps(payload), + ) + assert 200 == response.status_code, response.json + assert response.json.get("connection") is not None + print(json.dumps(response.json["connection"])) + + response = client.get( + "/v1/datasources", headers=tests.get_header(token=admin_session["token"]) + ) + assert 200 == response.status_code, response.json + print(response.json) + assert 1 == len(response.json["items"]) + + datasource_id = response.json["items"][0]["id"] + + response = client.get( + f"/v1/datasources/{datasource_id}", + headers=tests.get_header(token=admin_session["token"]), + ) + assert 200 == response.status_code, response.json + + datasource = response.json + + datasource["connection"] = { + "user": "root", + "password": "some.password", + "host": "some.other.host.tld", + "port": "3360", + "database": "initdb", + } + + response = client.put( + f"/v1/datasources/{datasource_id}", + headers=tests.get_header(token=admin_session["token"]), + data=json.dumps(datasource), + ) + assert 200 == response.status_code, response.json + + response = client.get( + f"/v1/datasources/{datasource_id}", + headers=tests.get_header(token=admin_session["token"]), + ) + + # Datasource password is never emitted so we skip it + tc.assertDictEqual( + response.json["connection"], + {k: v for k, v in datasource["connection"].items() if k != "password"}, + ) diff --git a/nesis/api/tests/core/controllers/test_datasources_roles.py b/nesis/api/tests/core/controllers/test_datasources_roles.py new file mode 100644 index 0000000..1c72528 --- /dev/null +++ b/nesis/api/tests/core/controllers/test_datasources_roles.py @@ -0,0 +1,338 @@ +import json +import os +import random + +import pytest +from sqlalchemy.orm.session import Session + +import nesis.api.core.services as services +import nesis.api.tests as tests +from nesis.api.core.controllers import app as cloud_app +from nesis.api.core.models import initialize_engine, DBSession + +""" +This file contains all tests for datasource roles and permissions +""" + + +@pytest.fixture +def client(): + + pytest.config = tests.config + initialize_engine(tests.config) + session: Session = DBSession() + tests.clear_database(session) + + os.environ["OPENAI_API_KEY"] = "some.api.key" + + services.init_services(tests.config) + return cloud_app.test_client() + + +def test_read_permissions(client): + """ + This tests that a user given read access to a specific datasource, will only be able to + list/read from that since datasource + :param client: + :return: + """ + + # Create an admin user + admin_session = tests.get_admin_session(app=client) + + # Create a datasource + payload = { + "type": "minio", + "name": "finance6", + "connection": { + "user": "caikuodda", + "password": "some.password", + "host": "localhost", + "port": "5432", + "database": "initdb", + }, + } + + # Create finance6 datasource + response = client.post( + f"/v1/datasources", + headers=tests.get_header(token=admin_session["token"]), + data=json.dumps(payload), + ) + assert 200 == response.status_code, response.json + + # Create finance7 datasource + response = client.post( + f"/v1/datasources", + headers=tests.get_header(token=admin_session["token"]), + data=json.dumps({**payload, "name": "finance7"}), + ) + assert 200 == response.status_code, response.json + + # A role allowing access to the finance6 datasource + role = { + "name": f"document-manager{random.randint(3, 19)}", + "policy": { + "items": [ + {"action": "read", "resource": "datasources/finance6"}, + ] + }, + } + response = client.post( + f"/v1/roles", + headers=tests.get_header(token=admin_session["token"]), + data=json.dumps(role), + ) + assert 200 == response.status_code, response.json + + # Admin creates a regular user assigning them access to the finance6 datasource + user_data = { + "name": "Test User", + "email": "another.user@domain.com", + "password": "password", + "roles": [response.json["id"]], + "enabled": True, + } + + response = client.post( + f"/v1/users", + headers=tests.get_header(token=admin_session["token"]), + data=json.dumps(user_data), + ) + assert 200 == response.status_code, response.json + + user_session = client.post( + f"/v1/sessions", + headers=tests.get_header(), + data=json.dumps(user_data), + ).json + assert 200 == response.status_code, response.json + + get_datasources = client.get( + f"/v1/datasources", + headers=tests.get_header(token=user_session["token"]), + data=json.dumps(user_data), + ).json + + assert 1 == len( + get_datasources["items"] + ), f"Expected 1 datasource by received {len(get_datasources.json['items'])}" + + +def test_delete_permissions(client): + """ + This tests that a user given delete access to a specific datasource, will only be able to + delete that since datasource + :param client: + :return: + """ + + # Create an admin user + admin_session = tests.get_admin_session(app=client) + + # Create a datasource + payload = { + "type": "minio", + "name": "finance6", + "connection": { + "user": "caikuodda", + "password": "some.password", + "host": "localhost", + "port": "5432", + "database": "initdb", + }, + } + + # Create finance6 datasource + datasource1 = client.post( + f"/v1/datasources", + headers=tests.get_header(token=admin_session["token"]), + data=json.dumps(payload), + ).json + + # Create finance7 datasource + datasource2 = client.post( + f"/v1/datasources", + headers=tests.get_header(token=admin_session["token"]), + data=json.dumps({**payload, "name": "finance7"}), + ).json + + # A role allowing access to the finance6 datasource + role = { + "name": f"document-manager{random.randint(3, 19)}", + "policy": { + "items": [ + {"action": "read", "resource": f"datasources/{datasource1['name']}"}, + {"action": "read", "resource": f"datasources/{datasource2['name']}"}, + {"action": "delete", "resource": f"datasources/{datasource2['name']}"}, + ] + }, + } + response = client.post( + f"/v1/roles", + headers=tests.get_header(token=admin_session["token"]), + data=json.dumps(role), + ) + assert 200 == response.status_code, response.json + + # Admin creates a regular user assigning them access to the datasources + user_data = { + "name": "Test User", + "email": "another.user@domain.com", + "password": "password", + "roles": [response.json["id"]], + "enabled": True, + } + response = client.post( + f"/v1/users", + headers=tests.get_header(token=admin_session["token"]), + data=json.dumps(user_data), + ) + assert 200 == response.status_code, response.json + + # Log in as the regular user + user_session = client.post( + f"/v1/sessions", + headers=tests.get_header(), + data=json.dumps(user_data), + ).json + assert 200 == response.status_code, response.json + + # Delete the datasource1. Should fail + response = client.delete( + f"/v1/datasources/{datasource1['id']}", + headers=tests.get_header(token=user_session["token"]), + ) + assert 403 == response.status_code, response.json + + # Get the datasources. Expect the two to exist + get_datasources = client.get( + f"/v1/datasources", headers=tests.get_header(token=user_session["token"]) + ).json + assert 2 == len( + get_datasources["items"] + ), f"Expected 2 datasource but received {len(get_datasources.json['items'])}" + + # Delete the datasource2. Should succeed + response = client.delete( + f"/v1/datasources/{datasource2['id']}", + headers=tests.get_header(token=user_session["token"]), + ) + assert 200 == response.status_code, response.json + + get_datasources = client.get( + f"/v1/datasources", headers=tests.get_header(token=user_session["token"]) + ).json + assert 1 == len( + get_datasources["items"] + ), f"Expected 2 datasource but received {len(get_datasources.json['items'])}" + assert get_datasources["items"][0]["name"] == datasource1["name"] + + +def test_update_permissions(client): + """ + This tests that a user given update access to a specific datasource, will only be able to + update that since datasource + :param client: + :return: + """ + + # Create an admin user + admin_session = tests.get_admin_session(app=client) + + # Create a datasource + payload = { + "type": "minio", + "name": "finance6", + "connection": { + "user": "caikuodda", + "password": "some.password", + "host": "localhost", + "port": "5432", + "database": "initdb", + }, + } + + # Create finance6 datasource + datasource1 = client.post( + f"/v1/datasources", + headers=tests.get_header(token=admin_session["token"]), + data=json.dumps(payload), + ).json + + # Create finance7 datasource + datasource2 = client.post( + f"/v1/datasources", + headers=tests.get_header(token=admin_session["token"]), + data=json.dumps({**payload, "name": "finance7"}), + ).json + + # A role allowing access to the finance6 datasource + role = { + "name": f"document-manager{random.randint(3, 19)}", + "policy": { + "items": [ + {"action": "read", "resource": f"datasources/{datasource1['name']}"}, + {"action": "read", "resource": f"datasources/{datasource2['name']}"}, + {"action": "update", "resource": f"datasources/{datasource2['name']}"}, + ] + }, + } + response = client.post( + f"/v1/roles", + headers=tests.get_header(token=admin_session["token"]), + data=json.dumps(role), + ) + assert 200 == response.status_code, response.json + + # Admin creates a regular user assigning them access to the datasources + user_data = { + "name": "Test User", + "email": "another.user@domain.com", + "password": "password", + "roles": [response.json["id"]], + "enabled": True, + } + response = client.post( + f"/v1/users", + headers=tests.get_header(token=admin_session["token"]), + data=json.dumps(user_data), + ) + assert 200 == response.status_code, response.json + + # Log in as the regular user + user_session = client.post( + f"/v1/sessions", + headers=tests.get_header(), + data=json.dumps(user_data), + ).json + assert 200 == response.status_code, response.json + + # Update the datasource1. Should fail + response = client.put( + f"/v1/datasources/{datasource1['id']}", + headers=tests.get_header(token=user_session["token"]), + data=json.dumps({**datasource1, "type": "postgres"}), + ) + assert 403 == response.status_code, response.json + + # Get the datasources. Expect the datasource unch + got_datasource = client.get( + f"/v1/datasources/{datasource1['id']}", + headers=tests.get_header(token=user_session["token"]), + ).json + assert got_datasource["type"] == payload["type"] + + # Update the datasource2. Should succeed + response = client.put( + f"/v1/datasources/{datasource2['id']}", + headers=tests.get_header(token=user_session["token"]), + data=(json.dumps({**datasource2, "type": "postgres"})), + ) + assert 200 == response.status_code, response.json + + got_datasource = client.get( + f"/v1/datasources/{datasource2['id']}", + headers=tests.get_header(token=user_session["token"]), + ).json + assert got_datasource["type"] == "postgres" diff --git a/nesis/api/tests/core/controllers/test_management_roles.py b/nesis/api/tests/core/controllers/test_management_roles.py index 307ad8f..384b259 100644 --- a/nesis/api/tests/core/controllers/test_management_roles.py +++ b/nesis/api/tests/core/controllers/test_management_roles.py @@ -117,7 +117,7 @@ def test_create_role_as_user(client): print(json.dumps(response.json)) -def test_access_permitted_resources_as_user(client): +def test_read_permitted_resources_as_user(client): """ This tests that a user given read access to a specific datasource, will only be able to list/read from that since datasource diff --git a/nesis/frontend/client/src/components/inputs/SquareButton.js b/nesis/frontend/client/src/components/inputs/SquareButton.js index f512b6d..be3cb77 100644 --- a/nesis/frontend/client/src/components/inputs/SquareButton.js +++ b/nesis/frontend/client/src/components/inputs/SquareButton.js @@ -76,4 +76,8 @@ export const OutlinedSquareButton = styled(SquareButton)` } `; +export const EditOutlinedSquareButton = styled(OutlinedSquareButton)` + min-width: 50px; +`; + export default SquareButton; diff --git a/nesis/frontend/client/src/pages/Settings/Datasources/DatasourcesDetailPage.js b/nesis/frontend/client/src/pages/Settings/Datasources/DatasourcesDetailPage.js index 78c598c..973ad10 100644 --- a/nesis/frontend/client/src/pages/Settings/Datasources/DatasourcesDetailPage.js +++ b/nesis/frontend/client/src/pages/Settings/Datasources/DatasourcesDetailPage.js @@ -78,32 +78,37 @@ export default function DataSourceDetailsPage({ onSuccess }) { } function CreateDataSource({ onSuccess, onError }) { + const location = useLocation(); return ( <> - Connect new Datasource + + {location?.state?.id ? `Edit` : `Create`} datasource + ); } function DataSourceForm({ + datasource, onSuccess, onError, submitButtonText = 'Submit', initialValues = { - module: 'data', - name: '', + id: datasource?.id, + name: datasource?.name, + type: datasource?.type, connection: { - user: '', + user: datasource?.connection?.user, password: '', - endpoint: '', - port: '', - database: '', - dataobjects: '', + endpoint: datasource?.connection?.endpoint, + port: datasource?.connection?.port, + dataobjects: datasource?.connection?.dataobjects, }, }, }) { @@ -119,7 +124,7 @@ function DataSourceForm({ actions.resetForm(); onSuccess(); addToast({ - title: `Dataobject created`, + title: `Datasource created`, content: 'Operation is successful', }); }) @@ -140,6 +145,7 @@ function DataSourceForm({ isDisabled={false} options={TypeOptions} placeholder={`Select type`} + value="connection.type" /> @@ -150,6 +156,7 @@ function DataSourceForm({ placeholder="Name" name="name" validate={required} + disabled={initialValues?.id !== null} /> diff --git a/nesis/frontend/client/src/pages/Settings/Datasources/DatasourcesPage.js b/nesis/frontend/client/src/pages/Settings/Datasources/DatasourcesPage.js index 315acb3..c15fa60 100644 --- a/nesis/frontend/client/src/pages/Settings/Datasources/DatasourcesPage.js +++ b/nesis/frontend/client/src/pages/Settings/Datasources/DatasourcesPage.js @@ -2,6 +2,7 @@ import React from 'react'; import { LightSquareButton, OutlinedSquareButton, + EditOutlinedSquareButton, } from '../../../components/inputs/SquareButton'; import styled from 'styled-components/macro'; import { device } from '../../../utils/breakpoints'; @@ -137,6 +138,7 @@ const DatasourcesPage = () => { const match = useRouteMatch(); const isNew = match.url?.endsWith('/datasources/new'); + const isEdit = match.url?.endsWith('/edit'); const history = useHistory(); const [searchText, setSearchText] = React.useState(); const [currentSort, setCurrentSort] = React.useState(null); @@ -177,7 +179,7 @@ const DatasourcesPage = () => { <> {confirmModal} { setSearchText(''); datasourcesActions.repeat(); @@ -222,8 +224,8 @@ const DatasourcesPage = () => { {datasource.id}{' '}
- - {datasource.enabled ? 'ONLINE' : 'OFFLINE'} + + {datasource.status} {datasource.name}
@@ -313,10 +315,21 @@ const DatasourcesPage = () => { datasource?.connection?.host} - - {datasource.enabled ? 'True' : 'False'} + + {datasource.status} + + history.push({ + pathname: `/settings/datasources/${datasource.id}/edit`, + state: datasource, + }) + } + > + Edit + + { setCurrentItem(datasource.id); diff --git a/nesis/frontend/client/src/pages/Settings/Roles/RolesPage.js b/nesis/frontend/client/src/pages/Settings/Roles/RolesPage.js index bf02fcb..d1b3300 100644 --- a/nesis/frontend/client/src/pages/Settings/Roles/RolesPage.js +++ b/nesis/frontend/client/src/pages/Settings/Roles/RolesPage.js @@ -2,6 +2,7 @@ import React from 'react'; import { LightSquareButton, OutlinedSquareButton, + EditOutlinedSquareButton, } from '../../../components/inputs/SquareButton'; import styled from 'styled-components/macro'; import { device } from '../../../utils/breakpoints'; @@ -200,18 +201,18 @@ const DocumentsGPTPage = () => { */} {DocumentsLoading && } - {paginatedDocuments.map((Roles, index) => ( + {paginatedDocuments.map((role, index) => ( {index + 1}{' '}
- - {Roles.enabled ? 'ONLINE' : 'OFFLINE'} + + {role.enabled ? 'ONLINE' : 'OFFLINE'} - {Roles.name} + {role.name}
} @@ -220,8 +221,8 @@ const DocumentsGPTPage = () => { history.push({ - pathname: `/settings/roles/${Roles.id}/edit`, - state: Roles, + pathname: `/settings/roles/${role.id}/edit`, + state: role, }) } > @@ -229,7 +230,7 @@ const DocumentsGPTPage = () => { { - setCurrentItem(Roles.id); + setCurrentItem(role.id); showConfirmModal(); }} > @@ -286,7 +287,7 @@ const DocumentsGPTPage = () => { showConfirmModal(); }} /> - history.push({ pathname: `/settings/roles/${Roles.id}/edit`, @@ -295,7 +296,7 @@ const DocumentsGPTPage = () => { } > Edit - + ))} diff --git a/nesis/frontend/client/src/pages/Settings/SettingPage.js b/nesis/frontend/client/src/pages/Settings/SettingPage.js index 2478f02..176969f 100644 --- a/nesis/frontend/client/src/pages/Settings/SettingPage.js +++ b/nesis/frontend/client/src/pages/Settings/SettingPage.js @@ -47,6 +47,11 @@ const SettingsPage = () => { path={`${match.path}/datasources`} component={DatasourcesPage} /> + { showConfirmModal(); }} /> - history.push({ pathname: `/settings/users/${Documents.id}/edit`, @@ -312,7 +313,7 @@ const DocumentsGPTPage = () => { } > Edit - + ))} diff --git a/nesis/frontend/server/api/datasources.js b/nesis/frontend/server/api/datasources.js index 658aac4..df3ab2b 100644 --- a/nesis/frontend/server/api/datasources.js +++ b/nesis/frontend/server/api/datasources.js @@ -63,10 +63,14 @@ const post = (requests, profile) => async (request, response) => { }); } - const endpoint = `${url}/datasources`; - logger.info(`Getting endpoint ${endpoint}`); - requests - .post(endpoint) + const post = datasource.id === null || datasource.id === undefined; + const endpoint = post + ? `${url}/datasources` + : `${url}/datasources/${datasource.id}`; + + const backEndRequest = ( + post ? requests.post(endpoint) : requests.put(endpoint) + ) .set('Accept', 'application/json') .set('Content-Type', 'application/json') .set( @@ -77,7 +81,10 @@ const post = (requests, profile) => async (request, response) => { : '' }`, ) - .send(datasource) + .send(datasource); + + logger.info(`Pushing to endpoint ${endpoint}`); + backEndRequest .then((res) => { response.status(res.status).send(res.body); }) diff --git a/nesis/frontend/server/api/datasources.spec.js b/nesis/frontend/server/api/datasources.spec.js index cc6d848..9ef5a49 100644 --- a/nesis/frontend/server/api/datasources.spec.js +++ b/nesis/frontend/server/api/datasources.spec.js @@ -45,6 +45,48 @@ describe('Datasources', () => { ); }); + it('can be updated', () => { + const requests = new Request(); + const post = api.post(requests, config.profile.DEV); + const request = { + headers: { + authorization: 'Usdz3323233eeewe', + }, + body: { + id: '89lio', + name: 'Test', + engine: 'sqlserver', + dataobjects: 'customer', + connection: { + host: 'host.name', + user: 'user', + password: 'password', + }, + }, + }; + + const responseStab = sinon.stub(); + const statusStab = sinon.stub().returns({ send: responseStab }); + const response = { + status: statusStab, + }; + + post(request, response); + sinon.assert.calledWith(statusStab, 200); + sinon.assert.called(responseStab); + const args = responseStab.getCall(0).args[0]; + sinon.assert.match(args, request.body); + sinon.assert.match( + `${config.profile.DEV.SERVICE_ENDPOINT}/datasources/89lio`, + requests.url, + ); + sinon.assert.match('PUT', requests.method); + sinon.assert.match( + request.headers.authorization, + requests.headers['Authorization'], + ); + }); + it('can be retrieved', () => { const requests = new Request(); const datasources = [ diff --git a/nesis/frontend/server/util/test-util.js b/nesis/frontend/server/util/test-util.js index 2b8399d..e15c48e 100644 --- a/nesis/frontend/server/util/test-util.js +++ b/nesis/frontend/server/util/test-util.js @@ -86,6 +86,8 @@ class Request { res = { body: this.data, status: 200 }; } else if (this.url.includes('/predictions/')) { res = { body: this.data, status: 200 }; + } else if (this.url.includes('/datasources/')) { + res = { body: this.data, status: 200 }; } } else if (this.method === 'GET') { if (this.url.endsWith('/rules')) { diff --git a/nesis/rag/Dockerfile b/nesis/rag/Dockerfile index e1c2d3d..5ecf572 100644 --- a/nesis/rag/Dockerfile +++ b/nesis/rag/Dockerfile @@ -5,7 +5,7 @@ COPY nesis/rag/requirements-huggingface.txt /app/nesis/rag/requirements-huggingf RUN apt-get update \ && python -m venv /app/.venv \ - && /app/.venv/bin/pip install -r /app/nesis/rag/requirements.txt -r /app/nesis/rag/requirements-huggingface.txt + && /app/.venv/bin/pip install -r /app/nesis/rag/requirements.txt -r /app/nesis/rag/requirements-huggingface.txt --default-timeout=1200 FROM python:3.11.6-slim-bookworm