diff --git a/app/Http/Controllers/Api/Application/Servers/ServerController.php b/app/Http/Controllers/Api/Application/Servers/ServerController.php index 832434cb3..eeb8aa555 100644 --- a/app/Http/Controllers/Api/Application/Servers/ServerController.php +++ b/app/Http/Controllers/Api/Application/Servers/ServerController.php @@ -48,7 +48,7 @@ class ServerController extends ApplicationApiController $servers = QueryBuilder::for(Server::query()) ->allowedFilters(['uuid', 'name', 'image', 'external_id']) - ->allowedSorts(['id', 'uuid']) + ->allowedSorts(['id', 'uuid', 'owner_id', 'node_id', 'status']) ->paginate($perPage); return $this->fractal->collection($servers) @@ -72,7 +72,7 @@ class ServerController extends ApplicationApiController return $this->fractal->item($server) ->transformWith($this->getTransformer(ServerTransformer::class)) - ->respond(201); + ->respond(Response::HTTP_CREATED); } /** diff --git a/app/Http/Controllers/Api/Remote/Servers/ServerTransferController.php b/app/Http/Controllers/Api/Remote/Servers/ServerTransferController.php index 0c1c2956d..e7dcb8805 100644 --- a/app/Http/Controllers/Api/Remote/Servers/ServerTransferController.php +++ b/app/Http/Controllers/Api/Remote/Servers/ServerTransferController.php @@ -125,7 +125,9 @@ class ServerTransferController extends Controller $server = $this->connection->transaction(function () use ($server, $transfer) { $allocations = [$transfer->old_allocation]; if (!empty($transfer->old_additional_allocations)) { - array_push($allocations, $transfer->old_additional_allocations); + foreach ($transfer->old_additional_allocations as $allocation) { + $allocations[] = $allocation; + } } // Remove the old allocations for the server and re-assign the server to the new @@ -169,7 +171,9 @@ class ServerTransferController extends Controller $allocations = [$transfer->new_allocation]; if (!empty($transfer->new_additional_allocations)) { - array_push($allocations, $transfer->new_additional_allocations); + foreach ($transfer->new_additional_allocations as $allocation) { + $allocations[] = $allocation; + } } Allocation::query()->whereIn('id', $allocations)->update(['server_id' => null]); diff --git a/resources/scripts/api/admin/servers/getServers.ts b/resources/scripts/api/admin/servers/getServers.ts index c5e919ab1..121f56825 100644 --- a/resources/scripts/api/admin/servers/getServers.ts +++ b/resources/scripts/api/admin/servers/getServers.ts @@ -42,18 +42,61 @@ export const rawDataToServer = ({ attributes }: FractalResponseData): Server => }, }); +export interface Filters { + uuid?: string; + name?: string; + image?: string; + /* eslint-disable camelcase */ + external_id?: string; + /* eslint-enable camelcase */ +} + interface ctx { page: number; setPage: (value: number | ((s: number) => number)) => void; + + filters: Filters | null; + setFilters: (filters: Filters | null) => void; + + sort: string | null; + setSort: (sort: string | null) => void; + + sortDirection: boolean; + setSortDirection: (direction: boolean) => void; } -export const Context = createContext({ page: 1, setPage: () => 1 }); +export const Context = createContext({ + page: 1, + setPage: () => 1, + + filters: null, + setFilters: () => null, + + sort: null, + setSort: () => null, + + sortDirection: false, + setSortDirection: () => false, +}); export default (include: string[] = []) => { - const { page } = useContext(Context); + const { page, filters, sort, sortDirection } = useContext(Context); - return useSWR>([ 'servers', page ], async () => { - const { data } = await http.get('/api/application/servers', { params: { include: include.join(','), page } }); + const params = {}; + if (filters !== null) { + Object.keys(filters).forEach(key => { + // @ts-ignore + params['filter[' + key + ']'] = filters[key]; + }); + } + + if (sort !== null) { + // @ts-ignore + params.sort = (sortDirection ? '-' : '') + sort; + } + + return useSWR>([ 'servers', page, filters, sort, sortDirection ], async () => { + const { data } = await http.get('/api/application/servers', { params: { include: include.join(','), page, ...params } }); return ({ items: (data.data || []).map(rawDataToServer), diff --git a/resources/scripts/api/http.ts b/resources/scripts/api/http.ts index a642bb16e..4e33541ab 100644 --- a/resources/scripts/api/http.ts +++ b/resources/scripts/api/http.ts @@ -94,7 +94,7 @@ export interface PaginatedResult { pagination: PaginationDataSet; } -interface PaginationDataSet { +export interface PaginationDataSet { total: number; count: number; perPage: number; diff --git a/resources/scripts/components/App.tsx b/resources/scripts/components/App.tsx index f46abc53e..3a76ceb2e 100644 --- a/resources/scripts/components/App.tsx +++ b/resources/scripts/components/App.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, Suspense } from 'react'; +import React, { lazy, useEffect, Suspense } from 'react'; import ReactGA from 'react-ga'; import { hot } from 'react-hot-loader/root'; import { Route, Router, Switch, useLocation } from 'react-router-dom'; @@ -15,9 +15,8 @@ import GlobalStylesheet from '@/assets/css/GlobalStylesheet'; import { history } from '@/components/history'; import { setupInterceptors } from '@/api/interceptors'; import TailwindGlobalStyles from '@/components/GlobalStyles'; -import AdminRouter from '@/routers/AdminRouter'; -// const ChunkedAdminRouter = lazy(() => import(/* webpackChunkName: "admin" */'@/routers/AdminRouter')); +const ChunkedAdminRouter = lazy(() => import(/* webpackChunkName: "admin" */'@/routers/AdminRouter')); interface ExtendedWindow extends Window { SiteConfiguration?: SiteSettings; @@ -94,7 +93,7 @@ const App = () => { - + diff --git a/resources/scripts/components/admin/AdminTable.tsx b/resources/scripts/components/admin/AdminTable.tsx index 353aab91b..1ec140e0b 100644 --- a/resources/scripts/components/admin/AdminTable.tsx +++ b/resources/scripts/components/admin/AdminTable.tsx @@ -1,26 +1,33 @@ -import React from 'react'; +import Input from '@/components/elements/Input'; +import InputSpinner from '@/components/elements/InputSpinner'; +import { debounce } from 'debounce'; +import React, { useCallback, useState } from 'react'; import { TableCheckbox } from '@/components/admin/AdminCheckbox'; import Spinner from '@/components/elements/Spinner'; import styled from 'styled-components/macro'; import tw from 'twin.macro'; -import { PaginatedResult } from '@/api/http'; +import { PaginatedResult, PaginationDataSet } from '@/api/http'; -export const TableHeader = ({ name }: { name?: string }) => { +export const TableHeader = ({ name, onClick, direction }: { name?: string, onClick?: (e: React.MouseEvent) => void, direction?: number | null }) => { if (!name) { return ; } return ( - + - {name} + {name} -
- - - - -
+ {direction !== undefined ? +
+ + {(direction === null || direction === 1) ? : null} + {(direction === null || direction === 2) ? : null} + +
+ : + null + }
); @@ -54,7 +61,7 @@ export const TableRow = ({ children }: { children: React.ReactNode }) => { }; interface Props { - data: PaginatedResult; + data?: PaginatedResult; onPageSelect: (page: number) => void; children: React.ReactNode; @@ -78,7 +85,20 @@ const PaginationArrow = styled.button` } `; -export function Pagination ({ data: { pagination }, onPageSelect, children }: Props) { +export function Pagination ({ data, onPageSelect, children }: Props) { + let pagination: PaginationDataSet; + if (data === undefined) { + pagination = { + total: 0, + count: 0, + perPage: 0, + currentPage: 1, + totalPages: 1, + }; + } else { + pagination = data.pagination; + } + const setPage = (page: number) => { if (page < 1 || page > pagination.totalPages) { return; @@ -173,11 +193,27 @@ export const NoItems = () => { interface Params { checked: boolean; onSelectAllClick: (e: React.ChangeEvent) => void; + onSearch?: (query: string) => Promise; children: React.ReactNode; } -export const ContentWrapper = ({ checked, onSelectAllClick, children }: Params) => { +export const ContentWrapper = ({ checked, onSelectAllClick, onSearch, children }: Params) => { + const [ loading, setLoading ] = useState(false); + const [ inputText, setInputText ] = useState(''); + + const search = useCallback( + debounce((query: string) => { + if (onSearch === undefined) { + return; + } + + setLoading(true); + onSearch(query).then(() => setLoading(false)); + }, 200), + [], + ); + return ( <>
@@ -194,15 +230,19 @@ export const ContentWrapper = ({ checked, onSelectAllClick, children }: Params)
- {/*
- - - - - - - -
*/} +
+ + { + setInputText(e.currentTarget.value); + search(e.currentTarget.value); + }} + /> + +
{children} diff --git a/resources/scripts/components/admin/databases/DatabasesContainer.tsx b/resources/scripts/components/admin/databases/DatabasesContainer.tsx index f993614c0..ac2ebfeac 100644 --- a/resources/scripts/components/admin/databases/DatabasesContainer.tsx +++ b/resources/scripts/components/admin/databases/DatabasesContainer.tsx @@ -1,4 +1,3 @@ -import CopyOnClick from '@/components/elements/CopyOnClick'; import React, { useContext, useEffect, useState } from 'react'; import getDatabases, { Context as DatabasesContext } from '@/api/admin/databases/getDatabases'; import FlashMessageRender from '@/components/FlashMessageRender'; @@ -10,6 +9,7 @@ import AdminContentBlock from '@/components/admin/AdminContentBlock'; import AdminCheckbox from '@/components/admin/AdminCheckbox'; import AdminTable, { TableBody, TableHead, TableHeader, TableRow, Pagination, Loading, NoItems, ContentWrapper } from '@/components/admin/AdminTable'; import Button from '@/components/elements/Button'; +import CopyOnClick from '@/components/elements/CopyOnClick'; const RowCheckbox = ({ id }: { id: number}) => { const isChecked = AdminContext.useStoreState(state => state.databases.selectedDatabases.indexOf(id) >= 0); diff --git a/resources/scripts/components/admin/locations/LocationEditContainer.tsx b/resources/scripts/components/admin/locations/LocationEditContainer.tsx index 869bc0c13..36457a83b 100644 --- a/resources/scripts/components/admin/locations/LocationEditContainer.tsx +++ b/resources/scripts/components/admin/locations/LocationEditContainer.tsx @@ -1,6 +1,8 @@ +import LocationDeleteButton from '@/components/admin/locations/LocationDeleteButton'; import React, { useEffect, useState } from 'react'; +import { useHistory } from 'react-router'; import tw from 'twin.macro'; -import { useHistory, useRouteMatch } from 'react-router-dom'; +import { useRouteMatch } from 'react-router-dom'; import { action, Action, Actions, createContextStore, useStoreActions } from 'easy-peasy'; import { Location } from '@/api/admin/locations/getLocations'; import getLocation from '@/api/admin/locations/getLocation'; @@ -15,7 +17,6 @@ import Field from '@/components/elements/Field'; import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; import { Form, Formik, FormikHelpers } from 'formik'; import updateLocation from '@/api/admin/locations/updateLocation'; -import LocationDeleteButton from '@/components/admin/locations/LocationDeleteButton'; interface ctx { location: Location | undefined; @@ -99,12 +100,12 @@ const EditInformationContainer = () => {
-
- history.push('/admin/locations')} - /> -
+
+ history.push('/admin/locations')} + /> +