Compare commits

...

14 Commits

22 changed files with 333 additions and 178 deletions

3
.gitignore vendored
View File

@ -10,13 +10,16 @@
# production
/build
build.zip
# misc
.DS_Store
.env
.env.local
.env.development
.env.development.local
.env.test.local
.env.production
.env.production.local
npm-debug.log*

5
package-lock.json generated
View File

@ -13561,6 +13561,11 @@
"symbol-observable": "^1.2.0"
}
},
"redux-thunk": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz",
"integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw=="
},
"regenerate": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.1.tgz",

View File

@ -27,6 +27,7 @@
"react-scripts": "4.0.0",
"reactstrap": "^8.7.1",
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
"web-vitals": "^0.2.4",
"yup": "^0.32.5"
},

View File

@ -1,6 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="stylesheet" href="main.css">
@ -16,17 +15,14 @@
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link rel="stylesheet" href="https://npmcdn.com/react-bootstrap-table/dist/react-bootstrap-table-all.min.css">
</link>
<title>Migrations Tracker</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!-- Compiled and minified JavaScript -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
<script src="https://npmcdn.com/react-bootstrap-table/dist/react-bootstrap-table.min.js"></script>
<!-- This breaks things -->
<!-- <script src="https://npmcdn.com/react-bootstrap-table/dist/react-bootstrap-table.min.js"></script> -->
</body>
</html>

View File

@ -1,9 +1,10 @@
import React, { Component } from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
// Store
import { Provider } from "react-redux";
import store from "../redux/store";
// Store currently in progress
// import { Provider } from "react-redux";
// import store from "../redux/store";
// Routes
import Migrations from "./root/Pages/Migrations";
@ -21,73 +22,61 @@ import Navigation from "./root/Navigation";
class App extends Component {
render() {
return (
<Provider store={store}>
<Router>
<Navigation />
<div className="row">
<div className="col s12 m4 13">
<div className="container-fluid">
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/book" component={Book} />
<Route exact path="/migrations" component={Migrations} />
<Route exact path="/reports" component={Reports} />
<Route path="/migrations/:migrationId" component={IDSingle} />
<Route
exact
path="/upcoming-migrations"
render={(props) => (
<GenericList {...props} APILINK="/pending/" />
)}
/>
<Route
exact
path="/missed"
render={(props) => (
<GenericList {...props} APILINK="/missed/" />
)}
/>
<Route
exact
path="/completed"
render={(props) => (
<GenericList {...props} APILINK="/completed/" />
)}
/>
<Route
exact
path="/pending-terminations"
render={(props) => (
<GenericList {...props} APILINK="/waitingterm/" />
)}
/>
<Route
exact
path="/all-terminations"
render={(props) => (
<GenericList {...props} APILINK="/pendingterm/" />
)}
/>
<Route
exact
path="/historical-migrations"
render={(props) => (
<GenericList {...props} APILINK="/all/" />
)}
/>
<Route
exact
path="/booked"
render={(props) => (
<GenericList {...props} APILINK="/booked/" />
)}
/>
</Switch>
</div>
</div>
</div>
</Router>
</Provider>
// <Provider store={store}>
<Router>
<Navigation />
<div className="col 13">
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/book" component={Book} />
<Route exact path="/migrations" component={Migrations} />
<Route exact path="/reports" component={Reports} />
<Route path="/migrations/:migrationId" component={IDSingle} />
<Route
exact
path="/upcoming-migrations"
render={(props) => <GenericList {...props} APILINK="/pending/" />}
/>
<Route
exact
path="/missed"
render={(props) => <GenericList {...props} APILINK="/missed/" />}
/>
<Route
exact
path="/completed"
render={(props) => (
<GenericList {...props} APILINK="/completed/" />
)}
/>
<Route
exact
path="/pending-terminations"
render={(props) => (
<GenericList {...props} APILINK="/waitingterm/" />
)}
/>
<Route
exact
path="/all-terminations"
render={(props) => (
<GenericList {...props} APILINK="/pendingterm/" />
)}
/>
<Route
exact
path="/historical-migrations"
render={(props) => <GenericList {...props} APILINK="/all/" />}
/>
<Route
exact
path="/booked"
render={(props) => <GenericList {...props} APILINK="/booked/" />}
/>
</Switch>
</div>
</Router>
// </Provider>
);
}
}

View File

@ -1,8 +0,0 @@
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View File

@ -2,6 +2,8 @@ import axios from "axios";
// Axios create, exporting callAPI
export const callAPI = axios.create({
baseURL: process.env.REACT_APP_API_ADDRESS,
headers: {'Authorization': "Api-Key " + process.env.REACT_APP_API_KEY}
});

View File

@ -10,6 +10,7 @@ export const Error = () => (
<span className="card-title">
<i className="large material-icons">do_not_disturb</i>
</span>
<h3>API OFFLINE</h3>
</center>
</div>
</div>

View File

@ -3,6 +3,8 @@ import React, { Component } from "react";
import { callAPI } from "../actions/API";
import Cards from "./Pages/Cards";
import GenericList from "../root/Pages/GenericList";
import TimeSlotHelper from "./Pages/TimeSlotHelper";
import { UList } from "../root/common/Functionality/UnorderedList";
import { builfArrayFromObject } from "../actions/Error";
@ -23,6 +25,19 @@ export class Home extends Component {
};
}
getTimeslot(days) {
callAPI
.get("/timeslots/?days=" + days)
.then((request) => {
return request.data;
})
.catch((error) => {
this.setState({
error: error,
});
});
}
componentDidMount() {
callAPI
.get("/")
@ -36,40 +51,28 @@ export class Home extends Component {
error: error,
});
});
callAPI
.get("/timeslots")
.then((request) => {
this.setState({
timeslots: request.data,
});
})
.catch((error) => {
this.setState({
error: error,
});
});
}
render() {
return (
<div className="row">
<div className="divider"></div>
<div className="section">
<div className="col s7">
<div className="container">
<div className="row">
<div className="col s12"></div>
</div>
<div className="row">
<div className="col s4 18">
<Cards migs={this.state.migs} />
</div>
<div className="col s5">
<p>Current availability for {Date()}</p>
<UList listItems={builfArrayFromObject(this.state.timeslots)} />
<div className="col s8 12">
{/* <p>Current availability for {Date()}</p>
<UList listItems={builfArrayFromObject(this.state.timeslots)} /> */}
{/* <TimeSlotHelper /> */}
</div>
<div className="divider"></div>
<div className="section">
<div className="row">
<div className="col s12">
<GenericList APILINK="/pending/" />
</div>
</div>
<div className="divider"></div>
</div>
</div>
);

View File

@ -1,5 +1,6 @@
import React, { Component } from "react";
import { callAPI } from "../../actions/API";
import FormPage from "../common/Forms/FormPage";
// Parent page for the Book component,
@ -10,9 +11,22 @@ export default class Book extends Component {
super(props);
this.state = {
migs: [],
timeslots: [],
};
}
componentDidMount() {
callAPI
.get('/gettimeslots/')
.then( (response) => {
this.setState({
timeslots: response.data
})
})
}
render() {
if (this.state.timeslots.length > 0) {
return (
<div className="container-fluid">
<div className="section">
@ -22,10 +36,13 @@ export default class Book extends Component {
<div className="section">
<div className="col s12">
<FormPage />
<FormPage timeslots={this.state.timeslots} />
</div>
</div>
</div>
);
}else{
return(<div></div>)
}
}
}

View File

@ -1,6 +1,6 @@
import React, { Component } from "react";
import CompTable from "../common/Tables/CompTable";
import { callAPI } from "../../actions/API"
import { callAPI } from "../../actions/API";
import { Error } from "../../actions/Error";
// Missing parent page,
@ -18,9 +18,10 @@ class GenericList extends Component {
}
renderItems() {
console.log(this.props.APILINK)
if (!this.state.error) {
return <CompTable data={callAPI(this.props.APILINK)} />;
return (
<CompTable data={callAPI(this.props.APILINK)} key={this.state.error} />
);
} else {
console.log("error");
return <Error />;
@ -28,19 +29,7 @@ class GenericList extends Component {
}
render() {
return (
<div className="row">
<div className="divider"></div>
<div className="section">
<div className="col s12"></div>
</div>
<div className="divider"></div>
<div className="section">
<div className="col s12">{this.renderItems()}</div>
</div>
<div className="divider"></div>
</div>
);
return <div>{this.renderItems()}</div>;
}
}

View File

@ -0,0 +1,54 @@
import React, { Component } from "react";
import TimeSlots from "../common/Tables/TimeSlots";
import { callAPI } from "../../actions/API";
import { Error } from "../../actions/Error";
// Missing parent page,
// Most of the good stuff is happening in UpcomingSingle, which does the
// main rendering of the table,
// may want to eventually do the API call here, to re-use the table
// instead of duplicating it.
class GenericList extends Component {
constructor(props) {
super(props);
this.state = {
error: false,
timeslots: [],
bookedslots: [],
};
}
componentDidMount() {
callAPI.get("/gettimeslots/").then((response) => {
this.setState({
timeslots: response.data,
});
});
}
renderItems() {
if (!this.state.error) {
return (
<TimeSlots
timeSlots={this.state.timeslots}
bookedSlots={this.state.bookedslots}
key={this.state.timeslots}
/>
);
} else {
return <Error />;
}
}
render() {
return <div>{this.renderItems()}</div>;
}
}
export default GenericList;
// if (this.state.timeslots > 0) {
// else {
// return <Error />;
// }
// }

View File

@ -23,16 +23,8 @@ const InputValidation = Yup.object().shape({
.required("Please enter a domain"),
username: Yup.string().min(2, "Too Short!").required("Required"),
original_server: Yup.string()
.matches(
/^((([1-9]?\d|1\d\d|2[0-5][0-5]|2[0-4]\d)\.){3}([1-9]?\d|1\d\d|2[0-5][0-5]|2[0-4]\d))|(((https?):\/\/)?(www.)?[a-z0-9]+(\.[a-z]{2,}){1,3}(#?\/?[a-zA-Z0-9#]+)*\/?(\?[a-zA-Z0-9-_]+=[a-zA-Z0-9-%]+&?)?)|([a-z0-9]{0,4}-[a-z0-9]{0,2}-[a-z0-9]{0,3})$/i,
"Please enter a valid IPv4 Address or domain"
)
.required("Please enter a valid IPv4 or domain"),
new_server: Yup.string()
.matches(
/^((([1-9]?\d|1\d\d|2[0-5][0-5]|2[0-4]\d)\.){3}([1-9]?\d|1\d\d|2[0-5][0-5]|2[0-4]\d))|(((https?):\/\/)?(www.)?[a-z0-9]+(\.[a-z]{2,}){1,3}(#?\/?[a-zA-Z0-9#]+)*\/?(\?[a-zA-Z0-9-_]+=[a-zA-Z0-9-%]+&?)?)|([a-z0-9]{0,4}-[a-z0-9]{0,2}-[a-z0-9]{0,3})$/i,
"Please enter a valid IPv4 Address or domain"
)
.required("Please enter a valid IPv4 or domain"),
agent_booked: Yup.string()
.min(2, "Too short!")
@ -44,7 +36,7 @@ const InputValidation = Yup.object().shape({
booked_date: Yup.date().required("Please enter a date!"),
});
export const CPanelBooking = () => {
export const CPanelBooking = (timeslots) => {
const [respID, setRespID] = useState(0);
const [error, setError] = useState(0);
@ -56,7 +48,7 @@ export const CPanelBooking = () => {
const onSubmit = async (values, { setSubmitting, resetForm }) => {
console.log(values);
callAPI
.post("/", values)
.post("/book/", values)
.then(function (response) {
// console.log(response);
// add function here
@ -70,6 +62,7 @@ export const CPanelBooking = () => {
});
setSubmitting(false);
};
return (
<Formik
initialValues={initialValues}
@ -108,12 +101,7 @@ export const CPanelBooking = () => {
className="has-success"
>
<option>Select</option>
<option>00:00-03:00</option>
<option>03:00-06:00</option>
<option>06:00-09:00</option>
<option>08:00-12:00</option>
<option>12:00-18:00</option>
<option>18:00-00:00</option>
{timeslots.timeslots.map((slot) => <option>{slot}</option>)}
</Input>
</FormGroup>
</Col>

View File

@ -13,7 +13,7 @@ import classnames from "classnames";
import { CPanelBooking } from "./CPanelBooking";
import EmailBooking from "./EmailBooking";
const FormPage = ({ item }) => {
const FormPage = ({ timeslots, item }) => {
const [activeTab, setActiveTab] = useState("1");
const toggle = (tab) => {
@ -23,7 +23,6 @@ const FormPage = ({ item }) => {
// Parent page from the Book section,
// Can be expanded to offer email migration bookings,
// elements below do the POST requests.
return (
<div>
<Nav tabs>
@ -52,7 +51,7 @@ const FormPage = ({ item }) => {
<TabPane tabId="1">
<Row>
<Col sm="12">
<CPanelBooking />
<CPanelBooking timeslots={timeslots} />
</Col>
</Row>
</TabPane>

View File

@ -5,11 +5,11 @@ export function UList(props) {
<div>
<ul>
{props.listItems.map((item) => (
<li liClass="white-text" className={props.liClass}>
<li liclass="white-text" className={props.liclass} key={item}>
{item}
</li>
))}
</ul>
</div>
);
}
}

View File

@ -14,7 +14,6 @@ import ToolkitProvider, {
const CompTable = (props) => {
const [list, setList] = useState([]);
const [loading, setLoading] = useState(false);
const { SearchBar } = Search;
const { ExportCSVButton } = CSVExport;
const sizePerPageRenderer = ({
@ -107,7 +106,6 @@ const CompTable = (props) => {
),
},
{ dataField: "booked_date", text: "Booked Date", sort: true },
// { dataField: "booked_time", text: "Booked Time", sort: true },
{ dataField: "agent_booked", text: "Agent initials", sort: true },
{
dataField: "domain",
@ -120,9 +118,7 @@ const CompTable = (props) => {
{ dataField: "migration_type", text: "Type", sort: true },
{ dataField: "original_server", text: "Original Server", sort: true },
{ dataField: "new_server", text: "New Server", sort: true },
// { dataField: "term_date", text: "Termination Date", sort: true },
{ dataField: "notes", text: "Notes", sort: true },
// { dataField: "submit_time", text: "Submit Time", sort: true },
];
useEffect(() => {
@ -130,7 +126,7 @@ const CompTable = (props) => {
}, []);
return (
<div className="HistoricalSingle">
<div>
{loading ? (
<ToolkitProvider
keyField="name"
@ -138,12 +134,13 @@ const CompTable = (props) => {
columns={columns}
search
exportCSV
key={columns.id}
>
{(props) => (
<div>
<div className="serSec">
<h3 className="hdrOne"></h3>
<SearchBar {...props.searchProps} />
<SearchBar {...props.searchProps} key={columns} />
</div>
<div className="table-responsive">
@ -154,9 +151,11 @@ const CompTable = (props) => {
striped
hover
condensed
key={columns.id}
keyField="id"
/>
</div>
<ExportCSVButton {...props.csvProps}>
<ExportCSVButton {...props.csvProps} key={columns}>
Export CSV!!
</ExportCSVButton>
</div>

View File

@ -0,0 +1,133 @@
import React, { useState, useEffect } from "react";
import BootstrapTable from "react-bootstrap-table-next";
import paginationFactory from "react-bootstrap-table2-paginator";
import * as ReactBootstrap from "react-bootstrap";
import filterFactory from "react-bootstrap-table2-filter";
import ToolkitProvider, {
Search,
CSVExport,
} from "react-bootstrap-table2-toolkit";
// notes
//
const TimeSlots = (props) => {
const [list, setList, setTimeSlots, setBookedSlots] = useState([]);
const [loading, setLoading] = useState(false);
const { SearchBar } = Search;
const { ExportCSVButton } = CSVExport;
const sizePerPageRenderer = ({
options,
currSizePerPage,
onSizePerPageChange,
}) => (
<div className="btn-group" role="group">
{options.map((option) => {
const isSelect = currSizePerPage === `${option.page}`;
return (
<button
key={option.text}
type="button"
onClick={() => onSizePerPageChange(option.page)}
className={`btn ${isSelect ? "btn-secondary" : "btn-success"}`}
>
{option.text}
</button>
);
})}
</div>
);
const getListData = async () => {
try {
const setList = await props.setList;
setList();
setLoading(true);
} catch (e) {
console.log(e);
}
};
const options = {
// paginationSize: 4,
// pageStartIndex: 0,
// alwaysShowAllBtns: true, // Always show next and previous button
// withFirstAndLast: false, // Hide the going to First and Last page button
// hideSizePerPage: true, // Hide the sizePerPage dropdown always
// hidePageListOnlyOnePage: true, // Hide the pagination list when only one page
// firstPageText: "First",
// prePageText: "Back",
nextPageText: "Next",
lastPageText: "Last",
// nextPageTitle: "First page",
// prePageTitle: "Pre page",
// firstPageTitle: "Next page",
// lastPageTitle: "Last page",
// showTotal: true,
// disablePageTitle: true,
// sizePerPageList: [
// {
// text: "50",
// value: 50,
// },
// {
// text: "100",
// value: 100,
// },
// ],
sizePerPageRenderer, // A numeric array is also available. the purpose of above example is custom the text
};
const columns = [
{ dataField: "bookedSlots", text: "bookedslots", sort: true },
{ dataField: "timeSlots", text: "timeslots", sort: true },
];
useEffect(() => {
getListData();
}, []);
return (
<div>
{loading ? (
<ToolkitProvider
keyField="name"
data={list}
columns={columns}
search
exportCSV
key={columns.id}
keyField="id"
>
{(props) => (
<div>
<div className="serSec">
<h3 className="hdrOne"></h3>
<SearchBar {...props.searchProps} key={columns} />
</div>
<div className="table-responsive">
<BootstrapTable
{...props.baseProps}
filter={filterFactory()}
pagination={paginationFactory(options)}
striped
hover
condensed
key={columns.id}
/>
</div>
<ExportCSVButton {...props.csvProps} key={columns}>
Export CSV!!
</ExportCSVButton>
</div>
)}
</ToolkitProvider>
) : (
<ReactBootstrap.Spinner animation="border" />
)}
</div>
);
};
export default TimeSlots;

View File

@ -1,13 +0,0 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

View File

@ -1,6 +1,5 @@
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./components/App";
import reportWebVitals from "./reportWebVitals";
@ -11,7 +10,4 @@ ReactDOM.render(
document.getElementById("root")
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

View File

@ -1,3 +1,3 @@
export const ADD_ARTICLE = "ADD_ARTICLE";
export const GET_MIGRATIONS = "GET_MIGRATIONS";
export const DATA_LOADED = "DATA_LOADED";
export const API_ERRORED = "API_ERRORED";

View File

@ -1,4 +1,4 @@
import { ADD_ARTICLE } from "../constants/action-types";
import { GET_MIGRATIONS } from "../constants/action-types";
const forbiddenWords = ["spam", "money"];
@ -6,7 +6,7 @@ export function forbiddenWordsMiddleware({ dispatch }) {
return function (next) {
return function (action) {
// do your stuff
if (action.type === ADD_ARTICLE) {
if (action.type === GET_MIGRATIONS) {
const foundWord = forbiddenWords.filter((word) =>
action.payload.title.includes(word)
);

View File

@ -1,20 +1,21 @@
import { ADD_ARTICLE, DATA_LOADED } from "../constants/action-types";
import { GET_MIGRATIONS, DATA_LOADED } from "../constants/action-types";
const initialState = {
articles: [],
remoteArticles: [],
migs: [],
timeslots: [],
error: false,
};
function rootReducer(state = initialState, action) {
if (action.type === ADD_ARTICLE) {
if (action.type === GET_MIGRATIONS) {
return Object.assign({}, state, {
articles: state.articles.concat(action.payload),
migs: state.migs.concat(action.payload),
});
}
if (action.type === DATA_LOADED) {
return Object.assign({}, state, {
remoteArticles: state.remoteArticles.concat(action.payload),
timeslots: state.timeslots.concat(action.payload),
});
}
return state;