From 84972197c002466d33f2336fe9cdd44bfaed8104 Mon Sep 17 00:00:00 2001 From: Chang Yong Lik <51813538+ahlag@users.noreply.github.com> Date: Thu, 15 Sep 2022 23:22:02 +0900 Subject: [PATCH] UI: Add data source detail page (#620) UI: Add data source detail page --- registry/access_control/api.py | 12 ++ registry/purview-registry/main.py | 11 ++ registry/sql-registry/main.py | 13 +- ui/.env.development | 3 + ui/src/api/api.tsx | 17 +++ ui/src/app.tsx | 5 + ui/src/components/dataSourceList.tsx | 64 +++++++++- ui/src/components/userRoles.tsx | 12 +- ui/src/pages/dataSource/dataSourceDetails.tsx | 112 ++++++++++++++++++ ui/src/pages/dataSource/dataSources.tsx | 8 +- ui/src/site.css | 4 + 11 files changed, 249 insertions(+), 12 deletions(-) create mode 100644 ui/.env.development create mode 100644 ui/src/pages/dataSource/dataSourceDetails.tsx diff --git a/registry/access_control/api.py b/registry/access_control/api.py index 3e7343e47..11f018e23 100644 --- a/registry/access_control/api.py +++ b/registry/access_control/api.py @@ -33,6 +33,18 @@ def get_project_datasources(project: str, requestor: User = Depends(project_read return json.loads(response) +@router.get("/projects/{project}/datasources/{datasource}", name="Get a single data source by datasource Id [Read Access Required]") +def get_project_datasource(project: str, datasource: str, requestor: User = Depends(project_read_access)) -> list: + response = requests.get(url=f"{registry_url}/projects/{project}/datasources/{datasource}", + headers=get_api_header(requestor)).content.decode('utf-8') + ret = json.loads(response) + + datasource_qualifiedName = ret['attributes']['qualifiedName'] + validate_project_access_for_feature( + datasource_qualifiedName, requestor, AccessType.READ) + return ret + + @router.get("/projects/{project}/features", name="Get features under my project [Read Access Required]") def get_project_features(project: str, keyword: Optional[str] = None, requestor: User = Depends(project_read_access)) -> list: response = requests.get(url=f"{registry_url}/projects/{project}/features", diff --git a/registry/purview-registry/main.py b/registry/purview-registry/main.py index 5818cd513..a2e1ecc8d 100644 --- a/registry/purview-registry/main.py +++ b/registry/purview-registry/main.py @@ -62,6 +62,17 @@ def get_project_datasources(project: str) -> list: return list([to_camel(e.to_dict()) for e in sources]) +@router.get("/projects/{project}/datasources/{datasource}",tags=["Project"]) +def get_datasource(project: str, datasource: str) -> dict: + p = registry.get_entity(project,True) + for s in p.attributes.sources: + if str(s.id) == datasource: + return s + # If datasource is not found, raise 404 error + raise HTTPException( + status_code=404, detail=f"Data Source {datasource} not found") + + @router.get("/projects/{project}/features",tags=["Project"]) def get_project_features(project: str, keyword: Optional[str] = None) -> list: atlasEntities = registry.get_project_features(project, keywords=keyword) diff --git a/registry/sql-registry/main.py b/registry/sql-registry/main.py index 00ac1d422..7e9a0ebb4 100644 --- a/registry/sql-registry/main.py +++ b/registry/sql-registry/main.py @@ -47,6 +47,17 @@ def get_project_datasources(project: str) -> list: return list([e.to_dict() for e in sources]) +@router.get("/projects/{project}/datasources/{datasource}") +def get_datasource(project: str, datasource: str) -> dict: + p = registry.get_entity(project) + for s in p.attributes.sources: + if str(s.id) == datasource: + return s + # If datasource is not found, raise 404 error + raise HTTPException( + status_code=404, detail=f"Data Source {datasource} not found") + + @router.get("/projects/{project}/features") def get_project_features(project: str, keyword: Optional[str] = None, page: Optional[int] = None, limit: Optional[int] = None) -> list: if keyword: @@ -54,7 +65,7 @@ def get_project_features(project: str, keyword: Optional[str] = None, page: Opti size = None if page is not None and limit is not None: start = (page - 1) * limit - size = limit + size = limit efs = registry.search_entity( keyword, [EntityType.AnchorFeature, EntityType.DerivedFeature], project=project, start=start, size=size) feature_ids = [ef.id for ef in efs] diff --git a/ui/.env.development b/ui/.env.development new file mode 100644 index 000000000..0c6c0e061 --- /dev/null +++ b/ui/.env.development @@ -0,0 +1,3 @@ +REACT_APP_AZURE_TENANT_ID=common +REACT_APP_API_ENDPOINT=http://127.0.0.1:8000 +REACT_APP_ENABLE_RBAC=false diff --git a/ui/src/api/api.tsx b/ui/src/api/api.tsx index 167bd05ee..a95ab2bd5 100644 --- a/ui/src/api/api.tsx +++ b/ui/src/api/api.tsx @@ -32,6 +32,23 @@ export const fetchDataSources = async (project: string) => { }); }; +export const fetchDataSource = async ( + project: string, + dataSourceId: string +) => { + const axios = await authAxios(msalInstance); + return axios + .get( + `${getApiBaseUrl()}/projects/${project}/datasources/${dataSourceId}`, + { + params: { project: project, datasource: dataSourceId }, + } + ) + .then((response) => { + return response.data; + }); +}; + export const fetchProjects = async () => { const axios = await authAxios(msalInstance); return axios diff --git a/ui/src/app.tsx b/ui/src/app.tsx index be6452636..5984717f9 100644 --- a/ui/src/app.tsx +++ b/ui/src/app.tsx @@ -10,6 +10,7 @@ import Features from "./pages/feature/features"; import NewFeature from "./pages/feature/newFeature"; import FeatureDetails from "./pages/feature/featureDetails"; import DataSources from "./pages/dataSource/dataSources"; +import DataSourceDetails from "./pages/dataSource/dataSourceDetails"; import Jobs from "./pages/jobs/jobs"; import Monitoring from "./pages/monitoring/monitoring"; import LineageGraph from "./pages/feature/lineageGraph"; @@ -44,6 +45,10 @@ const App = () => { path="/projects/:project/features/:featureId" element={} /> + } + /> } diff --git a/ui/src/components/dataSourceList.tsx b/ui/src/components/dataSourceList.tsx index 08e0cf5e7..9d7daa623 100644 --- a/ui/src/components/dataSourceList.tsx +++ b/ui/src/components/dataSourceList.tsx @@ -1,9 +1,17 @@ import React, { useCallback, useEffect, useState } from "react"; -import { Form, Select, Table } from "antd"; +import { useNavigate, Link } from "react-router-dom"; +import { Form, Select, Table, Button, Menu, Dropdown, Tooltip } from "antd"; +import { DownOutlined } from "@ant-design/icons"; import { DataSource } from "../models/model"; import { fetchDataSources, fetchProjects } from "../api"; -const DataSourceList = () => { +type Props = { + projectProp: string; + keywordProp: string; +}; + +const DataSourceList = ({ projectProp, keywordProp }: Props) => { + const navigate = useNavigate(); const columns = [ { title:
Name
, @@ -11,7 +19,16 @@ const DataSourceList = () => { align: "center" as "center", width: 120, render: (row: DataSource) => { - return row.attributes.name; + return ( + + ); }, onCell: () => { return { @@ -54,7 +71,7 @@ const DataSourceList = () => { }, }, { - title:
Pre Processing
, + title:
Preprocessing
, key: "preprocessing", align: "center" as "center", width: 190, @@ -101,6 +118,45 @@ const DataSourceList = () => { }; }, }, + { + title: ( +
+ Action{" "} + + Learn more + + } + > +
+ ), + key: "action", + align: "center" as "center", + width: 120, + render: (name: string, row: DataSource) => ( + { + return ( + + + + + + ); + }} + > + + + ), + }, ]; const [page, setPage] = useState(1); const [loading, setLoading] = useState(false); diff --git a/ui/src/components/userRoles.tsx b/ui/src/components/userRoles.tsx index 3d5d287f7..e4d9e7dea 100644 --- a/ui/src/components/userRoles.tsx +++ b/ui/src/components/userRoles.tsx @@ -39,7 +39,7 @@ const UserRoles = () => { sorter: { compare: (a: UserRole, b: UserRole) => a.scope.localeCompare(b.scope), multiple: 3, - } + }, }, { title:
Role
, @@ -53,9 +53,10 @@ const UserRoles = () => { key: "userName", align: "center" as "center", sorter: { - compare: (a: UserRole, b: UserRole) => a.userName.localeCompare(b.userName), + compare: (a: UserRole, b: UserRole) => + a.userName.localeCompare(b.userName), multiple: 1, - } + }, }, { title:
Permissions
, @@ -93,9 +94,10 @@ const UserRoles = () => { key: "createTime", align: "center" as "center", sorter: { - compare: (a: UserRole, b: UserRole) => a.createTime.localeCompare(b.createTime), + compare: (a: UserRole, b: UserRole) => + a.createTime.localeCompare(b.createTime), multiple: 2, - } + }, }, { title: "Action", diff --git a/ui/src/pages/dataSource/dataSourceDetails.tsx b/ui/src/pages/dataSource/dataSourceDetails.tsx new file mode 100644 index 000000000..2548644b2 --- /dev/null +++ b/ui/src/pages/dataSource/dataSourceDetails.tsx @@ -0,0 +1,112 @@ +import React from "react"; +import { LoadingOutlined } from "@ant-design/icons"; +import { useNavigate, useParams } from "react-router-dom"; +import { Alert, Button, Card, Col, Row, Spin, Typography } from "antd"; +import { QueryStatus, useQuery } from "react-query"; +import { AxiosError } from "axios"; +import { fetchDataSource } from "../../api"; +import { DataSource, DataSourceAttributes } from "../../models/model"; + +const { Title } = Typography; + +type DataSourceKeyProps = { dataSource: DataSource }; +const DataSourceKey = ({ dataSource }: DataSourceKeyProps) => { + const keys = dataSource.attributes; + return ( + <> + {keys && ( + + + Data Source Attributes +
+

Name: {keys.name}

+

Type: {keys.type}

+

Path: {keys.path}

+

Preprocessing: {keys.preprocessing}

+

Event Timestamp Column: {keys.eventTimestampColumn}

+

Timestamp Format: {keys.timestampFormat}

+

Qualified Name: {keys.qualifiedName}

+

Tags: {JSON.stringify(keys.tags)}

+
+
+ + )} + + ); +}; + +type Params = { + project: string; + dataSourceId: string; +}; + +const DataSourceDetails = () => { + const { project, dataSourceId } = useParams() as Params; + const navigate = useNavigate(); + const loadingIcon = ; + const { status, error, data } = useQuery( + ["dataSourceId", dataSourceId], + () => fetchDataSource(project, dataSourceId) + ); + + const render = (status: QueryStatus): JSX.Element => { + switch (status) { + case "error": + return ( + + + + ); + case "idle": + return ( + + + + ); + case "loading": + return ( + + + + ); + case "success": + if (data === undefined) { + return ( + + + + ); + } else { + return ( + <> + + + {data.attributes.name} +
+ + + +
+
+ + ); + } + } + }; + + return
{render(status)}
; +}; + +export default DataSourceDetails; diff --git a/ui/src/pages/dataSource/dataSources.tsx b/ui/src/pages/dataSource/dataSources.tsx index df3c3bf08..6d84aa0af 100644 --- a/ui/src/pages/dataSource/dataSources.tsx +++ b/ui/src/pages/dataSource/dataSources.tsx @@ -1,15 +1,19 @@ -import React from "react"; import { Card, Typography } from "antd"; +import { useSearchParams } from "react-router-dom"; import DataSourceList from "../../components/dataSourceList"; const { Title } = Typography; const DataSources = () => { + const [searchParams] = useSearchParams(); + const project = (searchParams.get("project") as string) ?? ""; + const keyword = (searchParams.get("keyword") as string) ?? ""; + return (
Data Sources - +
); diff --git a/ui/src/site.css b/ui/src/site.css index be060dc20..2d90b28bf 100644 --- a/ui/src/site.css +++ b/ui/src/site.css @@ -56,3 +56,7 @@ .feature-container p { break-inside: avoid-column; } + +.dataSource-container { + column-count: 1; +}