diff --git a/ui/package.json b/ui/package.json index f3aaca552..68b20428d 100644 --- a/ui/package.json +++ b/ui/package.json @@ -16,6 +16,7 @@ "prop-types": "^15.7.2", "react": "^17.0.2", "react-dom": "^17.0.2", + "react-infinite-scroll-component": "^6.1.0", "react-router-dom": "^5.2.0", "react-scripts": "4.0.3", "reactstrap": "^8.9.0", diff --git a/ui/src/app/App.js b/ui/src/app/App.js index c979b0201..dcce907a2 100644 --- a/ui/src/app/App.js +++ b/ui/src/app/App.js @@ -5,11 +5,12 @@ import { Redirect, Route } from "react-router-dom"; - +import { Provider as HttpProvider } from 'use-http'; import './App.scss'; import { I18nProvider } from './i18n/context/I18n.provider'; import Footer from './core/components/Footer'; +import { get_cookie } from './core/utility/get_cookie'; import Dashboard from './dashboard/container/Dashboard'; import Header from './core/components/Header'; @@ -17,25 +18,39 @@ import { UserProvider } from './core/user/UserContext'; import { Metadata } from './metadata/Metadata'; function App() { + + const httpOptions = { + interceptors: { + request: async ({options, url, path, route}) => { + options.headers['X-XSRF-TOKEN'] = get_cookie('XSRF-TOKEN'); + return options; + } + } + }; + return (
- - - -
-
- - - - - - - -
-
); } diff --git a/ui/src/app/core/utility/array_move.js b/ui/src/app/core/utility/array_move.js new file mode 100644 index 000000000..011f56578 --- /dev/null +++ b/ui/src/app/core/utility/array_move.js @@ -0,0 +1,12 @@ +export function array_move(arr, old_index, new_index) { + if (new_index >= arr.length) { + let k = new_index - arr.length + 1; + while (k--) { + arr.push(undefined); + } + } + arr.splice(new_index, 0, arr.splice(old_index, 1)[0]); + return arr; +} + +export default array_move; \ No newline at end of file diff --git a/ui/src/app/core/utility/get_cookie.js b/ui/src/app/core/utility/get_cookie.js new file mode 100644 index 000000000..97a5195ac --- /dev/null +++ b/ui/src/app/core/utility/get_cookie.js @@ -0,0 +1,17 @@ +export function get_cookie (cname) { + var name = cname + "="; + var decodedCookie = decodeURIComponent(document.cookie); + var ca = decodedCookie.split(';'); + for (var i = 0; i < ca.length; i++) { + var c = ca[i]; + while (c.charAt(0) == ' ') { + c = c.substring(1); + } + if (c.indexOf(name) == 0) { + return c.substring(name.length, c.length); + } + } + return ""; +} + +export default get_cookie; \ No newline at end of file diff --git a/ui/src/app/dashboard/component/Ordered.js b/ui/src/app/dashboard/component/Ordered.js new file mode 100644 index 000000000..4f352710c --- /dev/null +++ b/ui/src/app/dashboard/component/Ordered.js @@ -0,0 +1,87 @@ +import React from 'react'; +import useFetch from 'use-http'; +import first from 'lodash/first'; +import last from 'lodash/last'; +import API_BASE_PATH from '../../App.constant'; +import { array_move } from '../../core/utility/array_move'; +import { pick } from 'lodash'; + +const orderPaths = { + provider: `/MetadataResolversPositionOrder` +}; + +export const getId = (entity) => { + return entity.resourceId ? entity.resourceId : entity.id; +}; + +export const mergeOrderFn = (entities, order) => { + const ordered = [...entities.sort( + (a, b) => { + const aIndex = order.indexOf(getId(a)); + const bIndex = order.indexOf(getId(b)); + return aIndex > bIndex ? 1 : bIndex > aIndex ? -1 : 0; + } + )]; + return ordered; +}; + +export function Ordered ({type = 'provider', entities, children}) { + + const orderEntities = (orderById, list) => { + setOrdered(mergeOrderFn(list, orderById)); + }; + + const { get, post, response } = useFetch(`${API_BASE_PATH}`, { + cachePolicy: 'no-cache' + }); + + const [order, setOrder] = React.useState([]); + const [ordered, setOrdered] = React.useState([]); + + const [firstId, setFirstId] = React.useState(null); + const [lastId, setLastId] = React.useState(null); + + async function changeOrder(resourceIds) { + const update = await post(`${orderPaths[type]}`, { + resourceIds + }); + if (response.ok) { + loadOrder(); + } + } + + const onOrderUp = (id) => { + const index = order.indexOf(id); + const newOrder = array_move(order, index, index - 1); + changeOrder(newOrder); + }; + + const onOrderDown = (id) => { + const index = order.indexOf(id); + const newOrder = array_move(order, index, index + 1); + changeOrder(newOrder); + }; + + async function loadOrder () { + const o = await get(`${orderPaths[type]}`); + console.log(o) + if (response.ok) { + const ids = o.resourceIds; + setOrder(ids); + setFirstId(first(ids)); + setLastId(last(ids)); + } + } + + React.useEffect(() => loadOrder(),[]); + + React.useEffect(() => orderEntities(order, entities), [order, entities]); + + React.useEffect(() => console.log(ordered.map(e => pick(e, ['resourceId']))), [ordered]); + + return ( + <> + {children(ordered, firstId, lastId, onOrderUp, onOrderDown)} + + ); +} \ No newline at end of file diff --git a/ui/src/app/dashboard/component/Scroller.js b/ui/src/app/dashboard/component/Scroller.js new file mode 100644 index 000000000..10baf42cf --- /dev/null +++ b/ui/src/app/dashboard/component/Scroller.js @@ -0,0 +1,33 @@ +import React from 'react'; + +import InfiniteScroll from 'react-infinite-scroll-component'; + +const PAGE_LIMIT = 20; + +export function Scroller ({ entities, children }) { + + const [page, setPage] = React.useState(1); + + const [limited, setLimited] = React.useState([]); + + React.useEffect(() => { + let maxIndex = (page * PAGE_LIMIT) - 1, + minIndex = 0; + const l = entities.filter((resolver, index) => (maxIndex >= index && index >= minIndex)); + setLimited(l); + }, [entities, page]) + + const loadNext = () => { + setPage(page + 1); + } + + return ( + loadNext()} + hasMore={entities.length > limited.length} + > + { children(limited) } + + ); +} \ No newline at end of file diff --git a/ui/src/app/dashboard/component/Search.js b/ui/src/app/dashboard/component/Search.js new file mode 100644 index 000000000..8c5d73fae --- /dev/null +++ b/ui/src/app/dashboard/component/Search.js @@ -0,0 +1,47 @@ +import React from 'react'; +import pick from 'lodash/pick'; + +import { Form, Label, Input, Button, InputGroup, InputGroupAddon, FormGroup} from 'reactstrap'; +import { includes, some, values } from 'lodash'; + +export function Search ({ entities, searchable, children }) { + + const [searched, setSearched] = React.useState([]); + const [query, setQuery] = React.useState(''); + + React.useEffect(() => setSearched(entities), [entities]); + + const search = (query) => { + setQuery(query); + }; + + React.useEffect(() => { + if (!query) { + setSearched(entities); + } else { + setSearched(entities.filter((e) => { + const picked = values(pick(e, searchable)); + return some(picked, (v) => includes(v.toLowerCase(), query.toLowerCase())); + })); + } + }, [query, entities, searchable]); + + return ( + <> +
+ + + + search(event.target.value) } + value={query} /> + + + + + +
+ { children(searched) } + + ); +} \ No newline at end of file diff --git a/ui/src/app/dashboard/container/ProvidersTab.js b/ui/src/app/dashboard/container/ProvidersTab.js index fde9f260c..54daf1786 100644 --- a/ui/src/app/dashboard/container/ProvidersTab.js +++ b/ui/src/app/dashboard/container/ProvidersTab.js @@ -3,6 +3,10 @@ import React from 'react'; import { useMetadataEntities } from '../../metadata/hooks/api'; import Translate from '../../i18n/components/translate'; import ProviderList from '../../metadata/domain/provider/component/ProviderList'; +import {Search} from '../component/Search'; +import { Ordered } from '../component/Ordered'; + +const searchProps = ['name', '@type', 'createdBy']; export function ProvidersTab () { @@ -28,9 +32,19 @@ export function ProvidersTab () {
- { /* search goes here */} - - + + {(ordered, first, last, onOrderUp, onOrderDown) => + + {(searched) => } + + } +
diff --git a/ui/src/app/dashboard/container/SourcesTab.js b/ui/src/app/dashboard/container/SourcesTab.js index 3a57861f7..601421da5 100644 --- a/ui/src/app/dashboard/container/SourcesTab.js +++ b/ui/src/app/dashboard/container/SourcesTab.js @@ -5,6 +5,9 @@ import API_BASE_PATH from '../../App.constant'; import SourceList from '../../metadata/domain/source/component/SourceList'; import { useMetadataEntities } from '../../metadata/hooks/api'; +import { Search } from '../component/Search'; + +const searchProps = ['serviceProviderName', 'entityId', 'createdBy']; export function SourcesTab () { @@ -37,9 +40,9 @@ export function SourcesTab () {
- { /* search goes here */ } - - + + {(searched) => } +
diff --git a/ui/src/app/metadata/component/MetadataOptions.js b/ui/src/app/metadata/component/MetadataOptions.js index 18b1f354b..e59b1f8d1 100644 --- a/ui/src/app/metadata/component/MetadataOptions.js +++ b/ui/src/app/metadata/component/MetadataOptions.js @@ -37,7 +37,7 @@ export function MetadataOptions () { Version History {type === 'provider' && - diff --git a/ui/src/app/metadata/component/properties/ArrayProperty.js b/ui/src/app/metadata/component/properties/ArrayProperty.js index b655d784d..625d20654 100644 --- a/ui/src/app/metadata/component/properties/ArrayProperty.js +++ b/ui/src/app/metadata/component/properties/ArrayProperty.js @@ -71,7 +71,7 @@ export function ArrayProperty ({ property, columns, index, onPreview }) { {property.differences && Changed: } { property.name } {property.value.map((v, vidx) => - <> + {(!v || !v.length) &&

-

} {(v && v.length > 0) && } - +
)} : property.widget && property.widget.data ? <> {dataList.map((item, itemIdx) => -
- {item.differences && Changed: } - { item.label } - { property.value.map((v, vIdx) => -
- {v && v.indexOf(item.key) > -1 && true } - {(!v || !(v.indexOf(item.key) > -1)) && false } +
+ {item.differences && Changed: } + { item.label } + { property.value.map((v, vIdx) => +
+ {v && v.indexOf(item.key) > -1 && true } + {(!v || !(v.indexOf(item.key) > -1)) && false } +
+ )}
- )} -
)} : ''} diff --git a/ui/src/app/metadata/domain/provider/component/ProviderList.js b/ui/src/app/metadata/domain/provider/component/ProviderList.js index a771fac38..9c353e0de 100644 --- a/ui/src/app/metadata/domain/provider/component/ProviderList.js +++ b/ui/src/app/metadata/domain/provider/component/ProviderList.js @@ -1,16 +1,17 @@ import React from 'react'; import { Link } from 'react-router-dom'; -import { Badge, UncontrolledPopover, PopoverBody } from 'reactstrap'; - +import { Badge, } from 'reactstrap'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faChevronCircleDown, faChevronCircleUp } from '@fortawesome/free-solid-svg-icons'; import FormattedDate from '../../../../core/components/FormattedDate'; import Translate from '../../../../i18n/components/translate'; +import { Scroller } from '../../../../dashboard/component/Scroller'; -export default function ProviderList({ entities, onDelete }) { +export default function ProviderList({ entities, reorder = true, first, last, onOrderUp, onOrderDown }) { return ( -
+ + {(limited) =>
@@ -23,25 +24,36 @@ export default function ProviderList({ entities, onDelete }) { - {entities.map((provider, idx) => + {limited.map((provider, idx) =>
-
{ idx + 1 }
-
+ : +
+ } +   + - - { /* + -
{{ i + 1 }}
+ {/*
 
-
+ } +
); } diff --git a/ui/src/app/metadata/domain/source/component/SourceList.js b/ui/src/app/metadata/domain/source/component/SourceList.js index 482e21b0c..a8358c912 100644 --- a/ui/src/app/metadata/domain/source/component/SourceList.js +++ b/ui/src/app/metadata/domain/source/component/SourceList.js @@ -5,8 +5,13 @@ import { Badge, UncontrolledPopover, PopoverBody, Button, Modal, ModalHeader, Mo import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faTrash, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; + + import FormattedDate from '../../../../core/components/FormattedDate'; import Translate from '../../../../i18n/components/translate'; +import { Scroller } from '../../../../dashboard/component/Scroller'; + + export default function SourceList({ entities, onDelete }) { @@ -16,63 +21,68 @@ export default function SourceList({ entities, onDelete }) { const [deleting, setDeleting] = React.useState(null); - - const deleteSource = (id) => { onDelete(deleting); setDeleting(null); } return ( -
- - - - - - - - - - - - - {entities.map((source, idx) => - - - - - - - + <> + + {(limited) => +
+ +
TitleEntity IDAuthorCreated DateEnabled
- {source.serviceProviderName } - - {source.entityId} - - {source.createdBy } - - - - - - - { source.serviceEnabled && - - A metadata source must be disabled before it can be deleted. - - } -
+ + + + + + + + - ) } - -
TitleEntity IDAuthorCreated DateEnabled
- setDeleting(null)}> + + + {limited.map((source, idx) => + + + {source.serviceProviderName} + + + {source.entityId} + + + {source.createdBy} + + + + + + + + + + {source.serviceEnabled && + + A metadata source must be disabled before it can be deleted. + + } + + + )} + + +
+ } + + setDeleting(null)}> Delete Metadata Source? @@ -89,7 +99,7 @@ export default function SourceList({ entities, onDelete }) { -
+ ); } diff --git a/ui/src/app/metadata/hooks/api.js b/ui/src/app/metadata/hooks/api.js index 3402e7eb8..0b74f445c 100644 --- a/ui/src/app/metadata/hooks/api.js +++ b/ui/src/app/metadata/hooks/api.js @@ -2,17 +2,26 @@ import useFetch from 'use-http'; import API_BASE_PATH from '../../App.constant'; -const paths = { - source: 'EntityDescriptor', - provider: 'MetadataResolver' +const lists = { + source: 'EntityDescriptors', + provider: 'MetadataResolvers' }; +const details = { + source: 'EntityDescriptor', + provider: 'MetadataResolvers' +} + const schema = { source: 'MetadataSources' } export function getMetadataPath(type) { - return `/${paths[type]}`; + return `/${details[type]}`; +} + +export function getMetadataListPath(type) { + return `/${lists[type]}`; } export function getSchemaPath(type) { @@ -20,13 +29,16 @@ export function getSchemaPath(type) { } export function useMetadataEntities(type = 'source') { - return useFetch(`${API_BASE_PATH}${getMetadataPath(type)}s`); + return useFetch(`${API_BASE_PATH}${getMetadataListPath(type)}`); } export function useMetadataEntity(type = 'source') { return useFetch(`${API_BASE_PATH}${getMetadataPath(type)}`); } +export function useMetadataProviderOrder() { + return useFetch(`${API_BASE_PATH}/MetadataResolversPositionOrder`) +} export function useMetadataSchema() { return useFetch(`${API_BASE_PATH}/ui`); diff --git a/ui/src/setupProxy.js b/ui/src/setupProxy.js index 44319e180..abb8ec30d 100644 --- a/ui/src/setupProxy.js +++ b/ui/src/setupProxy.js @@ -6,8 +6,10 @@ module.exports = function (app) { '/api', createProxyMiddleware({ target: 'http://localhost:8080', - secure: false, - logLevel: "debug" + changeOrigin: true, + onProxyRes: function (proxyRes, req, res) { + proxyRes.headers['Access-Control-Allow-Origin'] = '*'; + } }) ); @@ -15,8 +17,7 @@ module.exports = function (app) { '/actuator', createProxyMiddleware({ target: 'http://localhost:8080', - secure: false, - logLevel: "debug" + changeOrigin: true }) ); @@ -24,8 +25,7 @@ module.exports = function (app) { '/login', createProxyMiddleware({ target: 'http://localhost:8080', - secure: false, - logLevel: "debug" + changeOrigin: true }) ); @@ -33,8 +33,7 @@ module.exports = function (app) { '/logout', createProxyMiddleware({ target: 'http://localhost:8080', - secure: false, - logLevel: "debug" + changeOrigin: true }) ); }; diff --git a/ui/src/theme/project/list.scss b/ui/src/theme/project/list.scss index 5acc06806..be94eddd0 100644 --- a/ui/src/theme/project/list.scss +++ b/ui/src/theme/project/list.scss @@ -22,3 +22,7 @@ padding: 0px; } } + +.infinite-scroll-component { + overflow: unset !important; +} \ No newline at end of file diff --git a/ui/yarn.lock b/ui/yarn.lock index ac9e26a66..5f9867d98 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -9087,6 +9087,13 @@ react-error-overlay@^6.0.9: resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a" integrity sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew== +react-infinite-scroll-component@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/react-infinite-scroll-component/-/react-infinite-scroll-component-6.1.0.tgz#7e511e7aa0f728ac3e51f64a38a6079ac522407f" + integrity sha512-SQu5nCqy8DxQWpnUVLx7V7b7LcA37aM7tvoWjTLZp1dk6EJibM5/4EJKzOnl07/BsM1Y40sKLuqjCwwH/xV0TQ== + dependencies: + throttle-debounce "^2.1.0" + react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -10616,6 +10623,11 @@ throat@^5.0.0: resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b" integrity sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA== +throttle-debounce@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-2.3.0.tgz#fd31865e66502071e411817e241465b3e9c372e2" + integrity sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ== + through2@^2.0.0: version "2.0.5" resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"