+
Welcome to Feathr Feature Store
- You can use Feathr UI to search features, identify data sources, track
- feature lineages and manage access controls.
+ You can use Feathr UI to search features, identify data sources, track feature lineages
+ and manage access controls.
- {" "}
+ {' '}
Learn More
@@ -63,28 +58,21 @@ const Home = () => {
{features.map((item) => {
return (
-
+
+
{item.title}
}
+ className={styles.cardMeta}
+ avatar={item.icon}
description={ {item.linkText}}
/>
- );
+ )
})}
@@ -100,7 +88,7 @@ const Home = () => {
rel="noreferrer"
>
Documentation
- {" "}
+ {' '}
provides docs for getting started
@@ -110,7 +98,7 @@ const Home = () => {
rel="noreferrer"
>
Running Feathr on Cloud
- {" "}
+ {' '}
describes how to run Feathr to Azure with Databricks or Synapse
@@ -120,7 +108,7 @@ const Home = () => {
rel="noreferrer"
>
Cloud Integrations and Architecture on Cloud
- {" "}
+ {' '}
describes Feathr architecture
@@ -130,9 +118,8 @@ const Home = () => {
rel="noreferrer"
>
Slack Channel
- {" "}
- describes how to join Slack channel for questions and
- discussions
+ {' '}
+ describes how to join Slack channel for questions and discussions
{
rel="noreferrer"
>
Community Guidelines
- {" "}
+ {' '}
describes how to contribute to Feathr
@@ -152,9 +139,9 @@ const Home = () => {
rel="noreferrer"
href="https://feathr-ai.github.io/feathr/concepts/feathr-concepts-for-beginners.html"
>
- {" "}
+ {' '}
Feathr Github Homepage
- {" "}
+ {' '}
to learn more.
@@ -167,7 +154,7 @@ const Home = () => {
- );
-};
+ )
+}
-export default Home;
+export default Home
diff --git a/ui/src/pages/Jobs/index.tsx b/ui/src/pages/Jobs/index.tsx
new file mode 100644
index 000000000..1bbd04cc8
--- /dev/null
+++ b/ui/src/pages/Jobs/index.tsx
@@ -0,0 +1,15 @@
+import React from 'react'
+
+import { PageHeader } from 'antd'
+
+const Jobs = () => {
+ return (
+
+
+ Under construction
+
+
+ )
+}
+
+export default Jobs
diff --git a/ui/src/pages/management/components/RoleForm/index.tsx b/ui/src/pages/Management/components/RoleForm/index.tsx
similarity index 51%
rename from ui/src/pages/management/components/RoleForm/index.tsx
rename to ui/src/pages/Management/components/RoleForm/index.tsx
index 5cef3d02c..b2c078c89 100644
--- a/ui/src/pages/management/components/RoleForm/index.tsx
+++ b/ui/src/pages/Management/components/RoleForm/index.tsx
@@ -1,93 +1,93 @@
-import React, { forwardRef, useCallback, useEffect, useState } from "react";
-import { Form, Select, Input, Button, message } from "antd";
-import { listUserRole, addUserRole } from "@/api";
+import React, { forwardRef, useCallback, useEffect, useState } from 'react'
+
+import { Form, Select, Input, Button, message } from 'antd'
+
+import { listUserRole, addUserRole } from '@/api'
export interface RoleFormProps {
- getRole?: (isAdmin: boolean) => void;
+ getRole?: (isAdmin: boolean) => void
}
-const { Item } = Form;
-const { TextArea } = Input;
+const { Item } = Form
+const { TextArea } = Input
const RoleOptions = [
- { label: "Admin", value: "admin" },
- { label: "Producer", value: "producer" },
- { label: "Consumer", value: "consumer" },
-];
+ { label: 'Admin', value: 'admin' },
+ { label: 'Producer', value: 'producer' },
+ { label: 'Consumer', value: 'consumer' }
+]
const ValidateRule = {
- scope: [{ required: true, message: "Please select scope!" }],
- userName: [{ required: true, message: "Please input user name!" }],
- roleName: [{ required: true, message: "Please select role name!" }],
- reason: [{ required: true, message: "Please input reason!" }],
-};
+ scope: [{ required: true, message: 'Please select scope!' }],
+ userName: [{ required: true, message: 'Please input user name!' }],
+ roleName: [{ required: true, message: 'Please select role name!' }],
+ reason: [{ required: true, message: 'Please input reason!' }]
+}
-const RoleForm = (props: RoleFormProps, ref: any) => {
- const [form] = Form.useForm();
- const { getRole } = props;
- const [loading, setLoading] = useState
(false);
+const RoleForm = (props: RoleFormProps) => {
+ const [form] = Form.useForm()
+ const { getRole } = props
+ const [loading, setLoading] = useState(false)
- const [scopeOptions, setScopeOptions] = useState<
- { label: string; value: string }[]
- >([]);
+ const [scopeOptions, setScopeOptions] = useState<{ label: string; value: string }[]>([])
const handleFinish = useCallback(
async (values) => {
try {
- setLoading(true);
- await addUserRole(values);
- form.resetFields();
- message.success("User role is created successfully.");
+ setLoading(true)
+ await addUserRole(values)
+ form.resetFields()
+ message.success('User role is created successfully.')
} catch {
- message.error("Failed to create user role.");
+ message.error('Failed to create user role.')
} finally {
- setLoading(false);
+ setLoading(false)
}
},
[form]
- );
+ )
const handleInit = useCallback(async () => {
try {
- const result = await listUserRole();
+ const result = await listUserRole()
if (result.length) {
const dataset = new Set(
result.reduce(
(list: string[], item) => {
- list.push(item.scope);
- return list;
+ list.push(item.scope)
+ return list
},
- ["global"]
+ ['global']
)
- );
+ )
const options = Array.from(dataset).map((item) => {
return {
label: item,
- value: item,
- };
- });
- setScopeOptions(options);
- return true;
+ value: item
+ }
+ })
+ setScopeOptions(options)
+ return true
} else {
- return false;
+ return false
}
} catch {
- return false;
+ return false
}
- }, []);
+ }, [])
useEffect(() => {
handleInit().then((isAdmin: boolean) => {
- getRole?.(isAdmin);
- });
- }, [handleInit, getRole]);
+ getRole?.(isAdmin)
+ })
+ }, [handleInit, getRole])
return (
- );
-};
+ )
+}
-const RoleFormComponent = forwardRef(RoleForm);
+const RoleFormComponent = forwardRef(RoleForm)
-RoleFormComponent.displayName = "RoleFormComponent";
+RoleFormComponent.displayName = 'RoleFormComponent'
-export default RoleFormComponent;
+export default RoleFormComponent
diff --git a/ui/src/pages/Management/components/SearchBar/index.tsx b/ui/src/pages/Management/components/SearchBar/index.tsx
new file mode 100644
index 000000000..d0070b805
--- /dev/null
+++ b/ui/src/pages/Management/components/SearchBar/index.tsx
@@ -0,0 +1,67 @@
+import React, { forwardRef } from 'react'
+
+import { SearchOutlined } from '@ant-design/icons'
+import { Form, Select, Input, Button } from 'antd'
+import { useNavigate } from 'react-router-dom'
+
+export interface SearchBarProps {
+ onSearch: (values: any) => void
+}
+
+const { Item } = Form
+
+const RoleOptions = [
+ { label: 'Admin', value: 'admin' },
+ { label: 'Producer', value: 'producer' },
+ { label: 'Consumer', value: 'consumer' }
+]
+
+const SearchBar = (props: SearchBarProps) => {
+ const [form] = Form.useForm()
+
+ const navigate = useNavigate()
+
+ const { onSearch } = props
+
+ const onClickRoleAssign = () => {
+ navigate('/role-management')
+ }
+
+ return (
+
+
+
+ + Create Role Assignment
+
+
+ )
+}
+
+const SearchBarComponent = forwardRef(SearchBar)
+
+SearchBarComponent.displayName = 'SearchBarComponent'
+
+export default SearchBarComponent
diff --git a/ui/src/pages/Management/components/UserRolesTable/index.tsx b/ui/src/pages/Management/components/UserRolesTable/index.tsx
new file mode 100644
index 000000000..170f74e72
--- /dev/null
+++ b/ui/src/pages/Management/components/UserRolesTable/index.tsx
@@ -0,0 +1,192 @@
+import React, {
+ forwardRef,
+ useCallback,
+ useEffect,
+ useImperativeHandle,
+ useRef,
+ useState
+} from 'react'
+
+import { DeleteOutlined } from '@ant-design/icons'
+import { Tag, Button, message, Popconfirm } from 'antd'
+import dayjs from 'dayjs'
+
+import { listUserRole, deleteUserRole } from '@/api'
+import ResizeTable, { ResizeColumnType } from '@/components/ResizeTable'
+import { UserRole } from '@/models/model'
+
+export interface UserRolesTableProps {}
+
+export interface UserRolesTableInstance {
+ onSearch?: (values: any) => void
+}
+
+export interface SearchModel {
+ scope?: string
+ roleName?: string
+}
+
+const UserRolesTable = (props: UserRolesTableProps, ref: any) => {
+ const [loading, setLoading] = useState(false)
+
+ const [tableData, setTableData] = useState([])
+
+ const searchRef = useRef()
+
+ const fetchData = useCallback(async () => {
+ setLoading(true)
+ try {
+ let result = await listUserRole()
+ if (searchRef.current) {
+ const { scope, roleName } = searchRef.current
+ result = result.filter((item) => {
+ let value = true
+ if (scope) {
+ value = item.scope.includes(scope.toLocaleLowerCase())
+ }
+ if (value && roleName) {
+ value = item.roleName === roleName
+ }
+ return value
+ })
+ }
+ result.sort((a: UserRole, b: UserRole) => {
+ return dayjs(b.createTime).diff(dayjs(a.createTime), 'milliseconds', true)
+ })
+
+ setTableData(result)
+ } catch {
+ //
+ } finally {
+ setLoading(false)
+ }
+ }, [])
+
+ const onDelete = async (row: UserRole) => {
+ try {
+ await deleteUserRole(row)
+ message.success(`Role ${row.roleName} of user ${row.userName} deleted`)
+ fetchData()
+ } catch {
+ message.error('Failed to delete userrole.')
+ }
+ }
+
+ const columns: ResizeColumnType[] = [
+ {
+ key: 'scope',
+ title: 'Scope (Project / Global)',
+ dataIndex: 'scope',
+ ellipsis: true,
+ width: 330,
+ minWidth: 190
+ },
+ {
+ title: 'Role',
+ dataIndex: 'roleName',
+ ellipsis: true,
+ width: 120
+ },
+ {
+ title: 'User',
+ dataIndex: 'userName',
+ ellipsis: true,
+ width: 300,
+ minWidth: 100
+ },
+ {
+ title: 'Permissions',
+ dataIndex: 'access',
+ ellipsis: true,
+ width: 240,
+ render: (col: string[]) => {
+ return col.map((tag) => {
+ let color = tag.length > 5 ? 'red' : 'green'
+ if (tag === 'write') color = 'blue'
+ return (
+
+ {tag.toUpperCase()}
+
+ )
+ })
+ }
+ },
+ {
+ title: 'Reason',
+ dataIndex: 'createReason',
+ ellipsis: true,
+ width: 300
+ },
+ {
+ title: 'Create By',
+ dataIndex: 'createBy',
+ width: 200,
+ ellipsis: true
+ },
+ {
+ title: 'Create Time',
+ dataIndex: 'createTime',
+ width: 200,
+ sorter: {
+ compare: (a: UserRole, b: UserRole) => {
+ return dayjs(b.createTime).diff(dayjs(a.createTime), 'milliseconds', true)
+ }
+ },
+ render: (col: string) => {
+ return dayjs(col).format('YYYY-MM-DD HH:mm:ss')
+ }
+ },
+ {
+ title: 'Action',
+ fixed: 'right',
+ width: 130,
+ resize: false,
+ render: (col: string, record: UserRole) => {
+ return (
+ {
+ onDelete(record)
+ }}
+ >
+ }>
+ Delete
+
+
+ )
+ }
+ }
+ ]
+
+ useImperativeHandle(ref, () => {
+ return {
+ onSearch: (values: SearchModel) => {
+ searchRef.current = values
+ fetchData()
+ }
+ }
+ })
+
+ useEffect(() => {
+ fetchData()
+ }, [fetchData])
+
+ return (
+
+ )
+}
+
+const UserRolesTableComponent = forwardRef(
+ UserRolesTable
+)
+
+UserRolesTableComponent.displayName = 'UserRolesTableComponent'
+
+export default UserRolesTableComponent
diff --git a/ui/src/pages/management/management.tsx b/ui/src/pages/Management/index.tsx
similarity index 58%
rename from ui/src/pages/management/management.tsx
rename to ui/src/pages/Management/index.tsx
index 882048b1f..0847b9a6a 100644
--- a/ui/src/pages/management/management.tsx
+++ b/ui/src/pages/Management/index.tsx
@@ -1,28 +1,27 @@
-import React, { useRef } from "react";
-import { Card, Typography, Alert, Space } from "antd";
-import UserRolesTable, {
- SearchModel,
- UserRolesTableInstance,
-} from "./components/UserRolesTable";
-import SearchBar from "./components/SearchBar";
+import React, { useRef } from 'react'
-const { Title } = Typography;
+import { Card, Typography, Alert, Space } from 'antd'
+
+import SearchBar from './components/SearchBar'
+import UserRolesTable, { SearchModel, UserRolesTableInstance } from './components/UserRolesTable'
+
+const { Title } = Typography
const Management = () => {
- const tableRef = useRef(null);
+ const tableRef = useRef(null)
const handleSearch = (values: SearchModel) => {
- tableRef.current?.onSearch?.(values);
- };
+ tableRef.current?.onSearch?.(values)
+ }
return (
Role Management
@@ -30,7 +29,7 @@ const Management = () => {
- );
-};
+ )
+}
-export default Management;
+export default Management
diff --git a/ui/src/pages/Monitoring/index.tsx b/ui/src/pages/Monitoring/index.tsx
new file mode 100644
index 000000000..97cd137d3
--- /dev/null
+++ b/ui/src/pages/Monitoring/index.tsx
@@ -0,0 +1,15 @@
+import React from 'react'
+
+import { PageHeader } from 'antd'
+
+const Monitoring = () => {
+ return (
+
+
+ Under construction
+
+
+ )
+}
+
+export default Monitoring
diff --git a/ui/src/pages/feature/newFeature.tsx b/ui/src/pages/NewFeature/index.tsx
similarity index 57%
rename from ui/src/pages/feature/newFeature.tsx
rename to ui/src/pages/NewFeature/index.tsx
index 50afd64c3..41d636385 100644
--- a/ui/src/pages/feature/newFeature.tsx
+++ b/ui/src/pages/NewFeature/index.tsx
@@ -1,6 +1,8 @@
-import React from "react";
-import { PageHeader } from "antd";
-import FeatureForm from "./components/FeatureForm";
+import React from 'react'
+
+import { PageHeader } from 'antd'
+
+import FeatureForm from '../Features/components/FeatureForm'
const NewFeature = () => {
return (
@@ -9,7 +11,7 @@ const NewFeature = () => {
- );
-};
+ )
+}
-export default NewFeature;
+export default NewFeature
diff --git a/ui/src/pages/ProjectLineage/index.tsx b/ui/src/pages/ProjectLineage/index.tsx
new file mode 100644
index 000000000..b065015d0
--- /dev/null
+++ b/ui/src/pages/ProjectLineage/index.tsx
@@ -0,0 +1,92 @@
+import React, { useEffect, useRef, useState } from 'react'
+
+import { PageHeader, Row, Col, Radio, Tabs } from 'antd'
+import { useParams, useSearchParams } from 'react-router-dom'
+
+import { fetchProjectLineages } from '@/api'
+import FlowGraph from '@/components/FlowGraph'
+import { FeatureLineage } from '@/models/model'
+import { FeatureType } from '@/utils/utils'
+
+import NodeDetails from '../Features/components/NodeDetails'
+
+const items = [
+ { label: 'Metadata', key: '1', children: