Adding loaders and data loading error handling
2 files deleted
2 files added
13 files modified
| | |
| | | }, |
| | | { |
| | | "files": [ |
| | | "server.js" |
| | | "server.js", |
| | | "src/Config.js" |
| | | ], |
| | | "env": { |
| | | "node": true |
| | |
| | | ENV REACT_APP_ADOPTION_SERVICE_URL=http://envoy-gateway-adopt-a-pup.apps.ocp-jaime.do328.dev.nextcle.com/ \ |
| | | REACT_APP_ANIMAL_SERVICE_URL=http://envoy-gateway-adopt-a-pup.apps.ocp-jaime.do328.dev.nextcle.com/ \ |
| | | REACT_APP_SHELTER_SERVICE_URL=http://envoy-gateway-adopt-a-pup.apps.ocp-jaime.do328.dev.nextcle.com/ \ |
| | | REACT_APP_NEWS_ENABLED=1 |
| | | # REACT_APP_NEWS_SERVICE_URL="" |
| | | REACT_APP_NEWS_ENABLED=1 \ |
| | | REACT_APP_NEWS_SERVICE_URL=http://locahost:5000/ |
| | | |
| | | # Install dependencies |
| | | COPY package.json package-lock.json ./ |
| | |
| | | RUN npm ci --production |
| | | |
| | | # Copy app |
| | | COPY . ./ |
| | | COPY build server.js ./ |
| | | |
| | | EXPOSE 8080 |
| | | |
| | |
| | | "dotenv": { |
| | | "version": "8.2.0", |
| | | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", |
| | | "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==", |
| | | "dev": true |
| | | "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==" |
| | | }, |
| | | "dotenv-expand": { |
| | | "version": "5.1.0", |
| | |
| | | "dev": true |
| | | }, |
| | | "typescript": { |
| | | "version": "3.7.5", |
| | | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.5.tgz", |
| | | "integrity": "sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw==", |
| | | "version": "3.9.3", |
| | | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.3.tgz", |
| | | "integrity": "sha512-D/wqnB2xzNFIcoBG9FG8cXRDjiqSTbG2wd8DMZeQyJlP1vfTkIxH4GKveWaEBYySKIg+USu+E+EDIR47SqnaMQ==", |
| | | "dev": true |
| | | }, |
| | | "unicode-canonical-property-names-ecmascript": { |
| | |
| | | "ajv-keywords": "^3.1.0" |
| | | } |
| | | }, |
| | | "serialize-javascript": { |
| | | "version": "3.1.0", |
| | | "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.1.0.tgz", |
| | | "integrity": "sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg==", |
| | | "dev": true, |
| | | "requires": { |
| | | "randombytes": "^2.1.0" |
| | | } |
| | | }, |
| | | "ssri": { |
| | | "version": "6.0.1", |
| | | "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", |
| | |
| | | } |
| | | }, |
| | | "terser-webpack-plugin": { |
| | | "version": "1.4.3", |
| | | "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz", |
| | | "integrity": "sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA==", |
| | | "version": "1.4.4", |
| | | "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.4.tgz", |
| | | "integrity": "sha512-U4mACBHIegmfoEe5fdongHESNJWqsGU+W0S/9+BmYGVQDw1+c2Ow05TpMhxjPK1sRb7cuYq1BPl1e5YHJMTCqA==", |
| | | "dev": true, |
| | | "requires": { |
| | | "cacache": "^12.0.2", |
| | | "find-cache-dir": "^2.1.0", |
| | | "is-wsl": "^1.1.0", |
| | | "schema-utils": "^1.0.0", |
| | | "serialize-javascript": "^2.1.2", |
| | | "serialize-javascript": "^3.1.0", |
| | | "source-map": "^0.6.1", |
| | | "terser": "^4.1.2", |
| | | "webpack-sources": "^1.4.0", |
| | |
| | | "version": "0.1.0", |
| | | "private": true, |
| | | "dependencies": { |
| | | "dotenv": "^8.2.0", |
| | | "express": "^4.17.1" |
| | | }, |
| | | "devDependencies": { |
| | | "@types/react-router-dom": "^5.1.5", |
| | | "install": "^0.13.0", |
| | | "@patternfly/react-core": "^3.158.1", |
| | | "@patternfly/react-table": "^2.28.47", |
| | | "@testing-library/jest-dom": "^4.2.4", |
| | |
| | | "@types/node": "^12.0.0", |
| | | "@types/react": "^16.9.0", |
| | | "@types/react-dom": "^16.9.0", |
| | | "@types/react-router-dom": "^5.1.5", |
| | | "axios": "^0.19.2", |
| | | "dockerfile-generator": "^4.0.3", |
| | | "install": "^0.13.0", |
| | | "react": "^16.13.1", |
| | | "react-core": "0.0.0", |
| | | "react-dom": "^16.13.1", |
| | | "react-router-dom": "^5.1.2", |
| | | "react-scripts": "3.4.1", |
| | | "typescript": "~3.7.2" |
| | | "typescript": "^3.7.5" |
| | | }, |
| | | "scripts": { |
| | | "start": "react-scripts start", |
| | | "start:prod": "node server.js", |
| | | "build": "react-scripts build", |
| | | "build:container": "npm run build && docker build -t quay.io/redhattraining/ossm-adopt-a-pup-webapp:1.0 .", |
| | | "test": "react-scripts test", |
| | | "eject": "react-scripts eject", |
| | | "lint": "eslint --ext ts,tsx src --fix" |
| | |
| | | Code to serve the web app in PRODUCTION. |
| | | Injects environment variables at runtime in build/index.html |
| | | */ |
| | | require("dotenv").config(); |
| | | const express = require("express"); |
| | | const path = require("path"); |
| | | const fs = require("fs").promises; |
| | | const app = express(); |
| | | |
| | | const BUILD_PATH = path.join(__dirname, "build"); |
| | | |
| | | app.use(express.static(BUILD_PATH)); |
| | | |
| | | // Return index.html, with injected env variables |
| | | const indexFilePath = path.join(__dirname, "build", "index.html"); |
| | | console.log(`Using: ${indexFilePath}`); |
| | | app.get("/*", async(request, response) => { |
| | | const content = await injectEnvironmentInHTml(indexFilePath); |
| | | response.send(content); |
| | | log(request); |
| | | }); |
| | | |
| | | app.use(express.static(path.join(__dirname, "build"))); |
| | | |
| | | app.get("/*", async(req, res) => { |
| | | const PORT = process.env.PORT || 8080; |
| | | app.listen(PORT, () => { |
| | | console.log(`Server listening at ${PORT}`); |
| | | }); |
| | | |
| | | |
| | | function log(req) { |
| | | console.log(`${(new Date()).toISOString()} - GET ${req.url}`); |
| | | let content; |
| | | content = await render(indexFilePath, { |
| | | REACT_APP_ADOPTION_SERVICE_URL: process.env.REACT_APP_ADOPTION_SERVICE_URL, |
| | | REACT_APP_ANIMAL_SERVICE_URL: process.env.REACT_APP_ANIMAL_SERVICE_URL, |
| | | REACT_APP_SHELTER_SERVICE_URL: process.env.REACT_APP_SHELTER_SERVICE_URL, |
| | | REACT_APP_NEWS_SERVICE_URL: process.env.REACT_APP_NEWS_SERVICE_URL, |
| | | REACT_APP_NEWS_ENABLED: ["true", "1", "True"].includes( |
| | | process.env.REACT_APP_NEWS_ENABLED |
| | | ) |
| | | }); |
| | | |
| | | res.send(content); |
| | | }); |
| | | |
| | | const port = process.env.PORT || 8080; |
| | | app.listen(port, () => { |
| | | console.log(`Server listening at ${port}`); |
| | | }); |
| | | } |
| | | |
| | | |
| | | async function render(filePath, data) { |
| | | console.log(data); |
| | | async function injectEnvironmentInHTml(filePath) { |
| | | // Only consider variables starting with REACT_APP |
| | | const environment = {}; |
| | | Object.keys(process.env) |
| | | .filter(key => key.startsWith("REACT_APP_")) |
| | | .forEach(key => { |
| | | environment[key] = process.env[key]; |
| | | }); |
| | | |
| | | const content = await fs.readFile(filePath); |
| | | return content |
| | | .toString() |
| | | .replace( |
| | | "__PRODUCTION_ENV__", |
| | | JSON.stringify(data) |
| | | JSON.stringify(environment) |
| | | ); |
| | | } |
| | |
| | | import { ShelterService } from "./Services/ShelterService"; |
| | | import AnimalDetailsView from "./Views/AnimalDetailsView"; |
| | | import ShelterDetailsView from "./Views/ShelterDetailsView"; |
| | | import Environment from "./Services/Environment"; |
| | | import Config from "./Config"; |
| | | import NewsRESTService from "./Services/NewsRESTService"; |
| | | |
| | | |
| | |
| | | let adoptionService: AdoptionService; |
| | | let shelterService: ShelterService; |
| | | let newsService: NewsService; |
| | | const ADOPTION_SERVICE_URL = Environment.get("REACT_APP_ADOPTION_SERVICE_URL"); |
| | | const ANIMAL_SERVICE_URL = Environment.get("REACT_APP_ANIMAL_SERVICE_URL"); |
| | | const SHELTER_SERVICE_URL = Environment.get("REACT_APP_SHELTER_SERVICE_URL"); |
| | | const NEWS_ENABLED = Environment.get("REACT_APP_NEWS_ENABLED"); |
| | | const NEWS_SERVICE_URL = Environment.get("REACT_APP_NEWS_SERVICE_URL"); |
| | | |
| | | if (ADOPTION_SERVICE_URL) { |
| | | adoptionService = new AdoptionRESTService(ADOPTION_SERVICE_URL); |
| | | if (Config.ADOPTION_SERVICE_URL) { |
| | | adoptionService = new AdoptionRESTService(Config.ADOPTION_SERVICE_URL); |
| | | } else { |
| | | console.log("Warning: No adoption service url provided. Using AdoptionFakeService"); |
| | | adoptionService = new AdoptionFakeService(); |
| | | } |
| | | |
| | | if (ANIMAL_SERVICE_URL) { |
| | | animalService = new AnimalRESTService(ANIMAL_SERVICE_URL); |
| | | if (Config.ANIMAL_SERVICE_URL) { |
| | | animalService = new AnimalRESTService(Config.ANIMAL_SERVICE_URL); |
| | | } else { |
| | | console.log("Warning: No animal service url provided. Using AnimalFakeService"); |
| | | animalService = new AnimalFakeService(); |
| | | } |
| | | |
| | | if (SHELTER_SERVICE_URL) { |
| | | shelterService = new ShelterRESTService(SHELTER_SERVICE_URL); |
| | | if (Config.SHELTER_SERVICE_URL) { |
| | | shelterService = new ShelterRESTService(Config.SHELTER_SERVICE_URL); |
| | | } else { |
| | | console.log("Warning: No shelter service url provided. Using ShelterFakeService"); |
| | | shelterService = new ShelterFakeService(); |
| | | } |
| | | |
| | | if (NEWS_ENABLED && NEWS_SERVICE_URL) { |
| | | newsService = new NewsRESTService(NEWS_SERVICE_URL); |
| | | if (Config.NEWS_ENABLED && Config.NEWS_SERVICE_URL) { |
| | | newsService = new NewsRESTService(Config.NEWS_SERVICE_URL); |
| | | } else { |
| | | console.log("Warning: No news service url provided. Using NewsFakeService"); |
| | | newsService = new NewsFakeService(); |
| | |
| | | adoptionService={adoptionService} |
| | | /> |
| | | </Route> |
| | | {NEWS_ENABLED && |
| | | {Config.NEWS_ENABLED && |
| | | <Route path="/news" exact> |
| | | <NewsView newsService={newsService} /> |
| | | </Route> |
New file |
| | |
| | | import React from "react"; |
| | | import { Alert, AlertActionCloseButton, Text, TextContent } from "@patternfly/react-core"; |
| | | import BullseyeSpinner from "./BullseyeSpinner"; |
| | | |
| | | |
| | | type LoadingDataProps = { |
| | | title?: string, |
| | | showLoader: boolean, |
| | | showError: boolean |
| | | errorTitle: string, |
| | | errorDescription: string, |
| | | onErrorClosed: () => void |
| | | } |
| | | |
| | | /** |
| | | * Use this component when you need to show a loader while loading data |
| | | * and possibly show an error alert if data can't be loaded |
| | | */ |
| | | export default class LoadingData extends React.Component<LoadingDataProps> { |
| | | |
| | | public render() { |
| | | const { |
| | | title, |
| | | showLoader, |
| | | showError, |
| | | errorTitle, |
| | | errorDescription, |
| | | onErrorClosed, |
| | | children |
| | | } = this.props; |
| | | return ( |
| | | <React.Fragment> |
| | | {showError && |
| | | <Alert |
| | | className="popup" |
| | | variant="danger" |
| | | title={errorTitle} |
| | | action={<AlertActionCloseButton onClose={onErrorClosed} />}> |
| | | {errorDescription} |
| | | </Alert>} |
| | | {title &&<TextContent> |
| | | <Text component="h2">{title}</Text> |
| | | </TextContent>} |
| | | {showLoader && <BullseyeSpinner />} |
| | | {children} |
| | | </React.Fragment> |
| | | ); |
| | | } |
| | | } |
| | |
| | | } from "@patternfly/react-core"; |
| | | |
| | | import { Link} from "react-router-dom"; |
| | | import Environment from "../Services/Environment"; |
| | | import Environment from "../Config"; |
| | | |
| | | |
| | | |
| | |
| | | |
| | | public render() { |
| | | const { pathname } = window.location; |
| | | const enableNews = Environment.get("REACT_APP_NEWS_ENABLED"); |
| | | const enableNews = Environment.getEnv("REACT_APP_NEWS_ENABLED"); |
| | | return ( |
| | | <Nav theme="dark"> |
| | | <NavList> |
| | |
| | | import { ShelterService } from "../Services/ShelterService"; |
| | | import { Shelter } from "../Models/Shelter"; |
| | | import { Link } from "react-router-dom"; |
| | | import { RESTConnectionError } from "../Services/RESTService"; |
| | | import LoadingData from "./LoadingData"; |
| | | |
| | | |
| | | type ShelterListProps = { |
| | |
| | | } |
| | | |
| | | type ShelterListState = { |
| | | shelters: Array<any> |
| | | shelters: Array<Shelter>, |
| | | loading: boolean, |
| | | error: { |
| | | isActive: boolean, |
| | | header: string, |
| | | message: string |
| | | } |
| | | } |
| | | |
| | | export default class ShelterList extends React.Component<ShelterListProps, ShelterListState> { |
| | |
| | | constructor(props: ShelterListProps) { |
| | | super(props); |
| | | this.state = { |
| | | shelters: [] |
| | | shelters: [], |
| | | loading: false, |
| | | error: { |
| | | isActive: false, |
| | | header: "", |
| | | message: "" |
| | | } |
| | | }; |
| | | } |
| | | |
| | | public async componentDidMount() { |
| | | const shelters = await this.props.shelterService.getAll(); |
| | | this.setState({ loading: true }); |
| | | |
| | | try { |
| | | const shelters = await this.props.shelterService.getAll(); |
| | | this.setState({ shelters }); |
| | | } catch (error) { |
| | | if (error instanceof RESTConnectionError) { |
| | | this.showConnectionError(error); |
| | | } |
| | | } finally { |
| | | this.setState({ loading: false }); |
| | | } |
| | | } |
| | | |
| | | private showConnectionError(error: RESTConnectionError) { |
| | | this.setState({ |
| | | shelters |
| | | error: { |
| | | isActive: true, |
| | | header: error.message, |
| | | message: error.description, |
| | | } |
| | | }); |
| | | } |
| | | |
| | | private closeErrorAlert = () => { |
| | | this.setState({ |
| | | error: { |
| | | isActive: false, |
| | | message: "", |
| | | header: "" |
| | | } |
| | | }); |
| | | } |
| | | |
| | | public render() { |
| | | const { shelters } = this.state; |
| | | const { shelters, loading, error } = this.state; |
| | | |
| | | return ( |
| | | <List> |
| | | {shelters.map(shelter => <ListItem key={shelter.shelterId}> |
| | | {this.renderShelter(shelter)} |
| | | </ListItem>)} |
| | | </List> |
| | | |
| | | <LoadingData |
| | | showLoader={loading} |
| | | showError={error.isActive} |
| | | errorTitle={error.header} |
| | | errorDescription={error.message} |
| | | onErrorClosed={this.closeErrorAlert} |
| | | > |
| | | <List> |
| | | {shelters.map(shelter => <ListItem key={shelter.shelterId}> |
| | | {this.renderShelter(shelter)} |
| | | </ListItem>)} |
| | | </List> |
| | | </LoadingData> |
| | | ); |
| | | } |
| | | |
New file |
| | |
| | | export default { |
| | | ADOPTION_SERVICE_URL: getEnv("REACT_APP_ADOPTION_SERVICE_URL"), |
| | | ANIMAL_SERVICE_URL: getEnv("REACT_APP_ANIMAL_SERVICE_URL"), |
| | | SHELTER_SERVICE_URL: getEnv("REACT_APP_SHELTER_SERVICE_URL"), |
| | | NEWS_SERVICE_URL: getEnv("REACT_APP_NEWS_SERVICE_URL"), |
| | | NEWS_ENABLED: getEnv("REACT_APP_NEWS_ENABLED"), |
| | | getEnv |
| | | |
| | | }; |
| | | |
| | | function getEnv(variable: string) { |
| | | // In production, env variables at runtime are read |
| | | // from the global "window" object in the browser |
| | | if (process.env.NODE_ENV === "production") { |
| | | const { env } = window as any; |
| | | return env ? env[variable]: undefined; |
| | | } |
| | | |
| | | // In dev, the React devserver lets us read variables from Nodejs "process.env" |
| | | return process.env[variable]; |
| | | } |
| | |
| | | } |
| | | |
| | | public getAllAdoptable(): Promise<Animal[]> { |
| | | return this.get("/animal/getAllAdoptable"); |
| | | return this.get("/animals/getAllAdoptable"); |
| | | } |
| | | |
| | | public getById(id: string): Promise<Animal> { |
| | |
| | | private readonly axiosInstance: AxiosInstance; |
| | | |
| | | constructor( |
| | | baseUrl: string, |
| | | baseURL: string, |
| | | private readonly remoteServiceName: string, |
| | | private readonly timeoutMs = 3000 |
| | | ) { |
| | | this.axiosInstance = Axios.create({ baseURL: baseUrl }); |
| | | this.axiosInstance = Axios.create({ baseURL }); |
| | | } |
| | | |
| | | protected async get<T>(url: string): Promise<T> { |
| | |
| | | const r = await this.axiosInstance.get<T>(url, { timeout: this.timeoutMs }); |
| | | return r.data; |
| | | } catch (e) { |
| | | throw new RESTConnectionError(e, this.remoteServiceName); |
| | | throw new RESTConnectionError(e, this.remoteServiceName, e.response?.status); |
| | | } |
| | | } |
| | | |
| | |
| | | import { AnimalService } from "../Services/AnimalService"; |
| | | import { Animal } from "../Models/Animal"; |
| | | import AdoptionForm from "../Components/AdoptionForm"; |
| | | import { RESTConnectionError } from "../Services/RESTService"; |
| | | import LoadingData from "../Components/LoadingData"; |
| | | |
| | | type AnimalDetailsViewProps = { |
| | | adoptionService: AdoptionService; |
| | |
| | | } |
| | | |
| | | type AnimalDetailsViewState = { |
| | | animal?: Animal |
| | | animal?: Animal, |
| | | loading: boolean, |
| | | error: { |
| | | isActive: boolean, |
| | | header: string, |
| | | message: string |
| | | } |
| | | } |
| | | |
| | | export default class AnimalDetailsView |
| | |
| | | constructor(props: AnimalDetailsViewProps) { |
| | | super(props); |
| | | this.state = { |
| | | animal: undefined |
| | | animal: undefined, |
| | | loading: false, |
| | | error: { |
| | | isActive: false, |
| | | header: "", |
| | | message: "" |
| | | } |
| | | }; |
| | | } |
| | | |
| | | |
| | | public async componentDidMount() { |
| | | const { animalId } = this.props.match.params; |
| | | const animal = await this.props.animalService.getById(animalId); |
| | | this.setState({ loading: true }); |
| | | |
| | | try { |
| | | const animal = await this.props.animalService.getById(animalId); |
| | | this.setState({ animal }); |
| | | } catch (error) { |
| | | if (error instanceof RESTConnectionError) { |
| | | this.showConnectionError(error); |
| | | } |
| | | } finally { |
| | | this.setState({ loading: false }); |
| | | } |
| | | } |
| | | |
| | | private showConnectionError(error: RESTConnectionError) { |
| | | this.setState({ |
| | | animal |
| | | error: { |
| | | isActive: true, |
| | | header: error.message, |
| | | message: error.description, |
| | | } |
| | | }); |
| | | } |
| | | |
| | | private closeErrorAlert = () => { |
| | | this.setState({ |
| | | error: { |
| | | isActive: false, |
| | | message: "", |
| | | header: "" |
| | | } |
| | | }); |
| | | } |
| | | |
| | | public render() { |
| | | const { animal } = this.state; |
| | | return animal ? this.renderAnimal(animal) : this.renderMissingAnimal(); |
| | | const { animal, loading, error } = this.state; |
| | | return ( |
| | | <LoadingData |
| | | showLoader={loading} |
| | | showError={error.isActive} |
| | | errorTitle={error.header} |
| | | errorDescription={error.message} |
| | | onErrorClosed={this.closeErrorAlert} |
| | | title="Adoptable Animals" |
| | | > |
| | | {animal ? this.renderAnimal(animal) : ""} |
| | | </LoadingData> |
| | | ); |
| | | } |
| | | |
| | | private renderAnimal(animal: Animal) { |
| | |
| | | </TextContent> |
| | | </GridItem> |
| | | </Grid> |
| | | |
| | | |
| | | </PageSection> |
| | | <PageSection> |
| | | <TextContent> |
| | | <Text component="h2">Adopt {animal.animalName}!</Text> |
| | | </TextContent> |
| | | <AdoptionForm adoptionService={this.props.adoptionService} animalId={animal.animalId} /> |
| | | <AdoptionForm |
| | | adoptionService={this.props.adoptionService} |
| | | animalId={animal.animalId} |
| | | /> |
| | | </PageSection> |
| | | </React.Fragment> |
| | | ); |
| | |
| | | import React from "react"; |
| | | import { AnimalService } from "../Services/AnimalService"; |
| | | import { |
| | | PageSection, PageSectionVariants, Text, TextContent, Alert, AlertActionCloseButton |
| | | import { |
| | | PageSection, PageSectionVariants, Text, TextContent |
| | | } from "@patternfly/react-core"; |
| | | import AdoptableAnimalList from "../Components/AdoptableAnimalList"; |
| | | import { AdoptionService } from "../Services/AdoptionService"; |
| | | import { Animal } from "../Models/Animal"; |
| | | import { RESTConnectionError } from "../Services/RESTService"; |
| | | import BullseyeSpinner from "../Components/BullseyeSpinner"; |
| | | import LoadingData from "../Components/LoadingData"; |
| | | |
| | | |
| | | type AnimalsViewProps = { |
| | |
| | | }); |
| | | } |
| | | |
| | | private closeAlert = () => { |
| | | private closeErrorAlert = () => { |
| | | this.setState({ |
| | | error: { |
| | | isActive: false, |
| | |
| | | </TextContent> |
| | | </PageSection> |
| | | <PageSection> |
| | | {error.isActive && |
| | | <Alert |
| | | className="popup" |
| | | variant="danger" |
| | | title={error.header} |
| | | action={<AlertActionCloseButton onClose={this.closeAlert} />}> |
| | | {error.message} |
| | | </Alert>} |
| | | <Text component="h2">Adoptable Animals</Text> |
| | | { loading && <BullseyeSpinner />} |
| | | <AdoptableAnimalList animals={this.state.animals} /> |
| | | <LoadingData |
| | | showLoader={loading} |
| | | showError={error.isActive} |
| | | errorTitle={error.header} |
| | | errorDescription={error.message} |
| | | onErrorClosed={this.closeErrorAlert} |
| | | title="Adoptable Animals" |
| | | > |
| | | <AdoptableAnimalList animals={this.state.animals} /> |
| | | </LoadingData> |
| | | </PageSection> |
| | | </React.Fragment> |
| | | ); |
| | |
| | | </TextContent> |
| | | </PageSection> |
| | | <PageSection> |
| | | <TextContent> |
| | | <Text component="h2">Shelters list</Text> |
| | | </TextContent> |
| | | <ShelterList shelterService={this.props.shelterService}></ShelterList> |
| | | </PageSection> |
| | | <PageSection variant={PageSectionVariants.light}> |