Compare commits
No commits in common. "main" and "old-site" have entirely different histories.
18
.github/workflows/docker-image.yml
vendored
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
name: Docker Image CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "main" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "main" ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Build the Docker image
|
||||||
|
run: docker build . --file Dockerfile --tag my-image-name:$(date +%s)
|
27
.github/workflows/main.yml
vendored
|
@ -1,9 +1,13 @@
|
||||||
name: Create and publish a Docker image
|
name: Create and publish a Docker image
|
||||||
|
|
||||||
|
# This workflow uses actions that are not certified by GitHub.
|
||||||
|
# They are provided by a third-party and are governed by
|
||||||
|
# separate terms of service, privacy policy, and support
|
||||||
|
# documentation.
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: ['main']
|
branches: ['main']
|
||||||
repository_dispatch:
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: ghcr.io
|
REGISTRY: ghcr.io
|
||||||
|
@ -19,8 +23,6 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Log in to the Container registry
|
- name: Log in to the Container registry
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v1
|
||||||
|
@ -35,27 +37,10 @@ jobs:
|
||||||
with:
|
with:
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
|
||||||
- name: Setup Python
|
|
||||||
uses: actions/setup-python@v4.0.0
|
|
||||||
with:
|
|
||||||
python-version: 3.12
|
|
||||||
|
|
||||||
- uses: paulhatch/semantic-version@v5.0.2
|
|
||||||
id: vnum
|
|
||||||
with:
|
|
||||||
# The prefix to use to identify tags
|
|
||||||
tag_prefix: ""
|
|
||||||
major_pattern: "(MAJOR)"
|
|
||||||
minor_pattern: "(MINOR)"
|
|
||||||
version_format: "${major}.${minor}.${patch}-${increment}"
|
|
||||||
bump_each_commit: true
|
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v2
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.vnum.outputs.version_tag }}
|
|
||||||
${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|
82
.github/workflows/nws-deploy.yaml
vendored
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
name: NWS Publish React JS
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "main" ]
|
||||||
|
repository_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push-image:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Create Dockerfile
|
||||||
|
run: |
|
||||||
|
cat <<\EOT > Dockerfile
|
||||||
|
FROM node:13.12.0-alpine as build
|
||||||
|
WORKDIR /app
|
||||||
|
ENV PATH /app/node_modules/.bin:$PATH
|
||||||
|
COPY package.json ./
|
||||||
|
COPY package-lock.json ./
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
RUN npm install react-scripts@3.4.1 -g --silent
|
||||||
|
COPY . ./
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# production environment
|
||||||
|
FROM nginx:stable-alpine
|
||||||
|
COPY --from=build /app/build /usr/share/nginx/html
|
||||||
|
COPY --from=build /app/nginx/nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
EXPOSE 80
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
|
EOT
|
||||||
|
|
||||||
|
- name: Log in to the Container registry
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v3
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@v4.0.0
|
||||||
|
with:
|
||||||
|
python-version: 3.12.0-alpha.3
|
||||||
|
|
||||||
|
- uses: paulhatch/semantic-version@v5.0.2
|
||||||
|
id: vnum
|
||||||
|
with:
|
||||||
|
tag_prefix: ""
|
||||||
|
major_pattern: "(MAJOR)"
|
||||||
|
minor_pattern: "(MINOR)"
|
||||||
|
version_format: "${major}.${minor}.${patch}-${increment}"
|
||||||
|
bump_each_commit: true
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.vnum.outputs.version_tag }}
|
||||||
|
${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
25
.gitignore
vendored
|
@ -1,2 +1,23 @@
|
||||||
/target
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
.env
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
8
.idea/modules.xml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/nws-site.iml" filepath="$PROJECT_DIR$/.idea/nws-site.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
12
.idea/nws-site.iml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
6
.idea/vcs.xml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
90
.idea/workspace.xml
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ChangeListManager">
|
||||||
|
<list default="true" id="03fafda4-e2c1-4602-a731-a2f96e84badd" name="Default Changelist" comment="">
|
||||||
|
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/components/DashboardPage.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/DashboardPage.tsx" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/nws-api/types.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/nws-api/types.ts" afterDir="false" />
|
||||||
|
</list>
|
||||||
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
|
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||||
|
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||||
|
</component>
|
||||||
|
<component name="FileTemplateManagerImpl">
|
||||||
|
<option name="RECENT_TEMPLATES">
|
||||||
|
<list>
|
||||||
|
<option value="HTTP Request" />
|
||||||
|
<option value="TypeScript File" />
|
||||||
|
<option value="TypeScript JSX File" />
|
||||||
|
<option value="CSS File" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
<component name="Git.Settings">
|
||||||
|
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||||
|
</component>
|
||||||
|
<component name="MarkdownSettingsMigration">
|
||||||
|
<option name="stateVersion" value="1" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectId" id="24cZXAEHtOTkBPpX5NH0IuXFSFd" />
|
||||||
|
<component name="ProjectLevelVcsManager" settingsEditedManually="true" />
|
||||||
|
<component name="ProjectViewState">
|
||||||
|
<option name="hideEmptyMiddlePackages" value="true" />
|
||||||
|
<option name="showLibraryContents" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PropertiesComponent">
|
||||||
|
<property name="WebServerToolWindowFactoryState" value="false" />
|
||||||
|
<property name="nodejs_package_manager_path" value="npm" />
|
||||||
|
<property name="ts.external.directory.path" value="$PROJECT_DIR$/node_modules/typescript/lib" />
|
||||||
|
<property name="vue.rearranger.settings.migration" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="RecentsManager">
|
||||||
|
<key name="CopyFile.RECENT_KEYS">
|
||||||
|
<recent name="$PROJECT_DIR$/src/components" />
|
||||||
|
<recent name="$PROJECT_DIR$/src/static/images" />
|
||||||
|
</key>
|
||||||
|
</component>
|
||||||
|
<component name="RunManager">
|
||||||
|
<configuration name="Debug Application" type="JavascriptDebugType" uri="http://localhost:3000">
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
||||||
|
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
|
||||||
|
<component name="TaskManager">
|
||||||
|
<task active="true" id="Default" summary="Default task">
|
||||||
|
<changelist id="03fafda4-e2c1-4602-a731-a2f96e84badd" name="Default Changelist" comment="" />
|
||||||
|
<created>1643931872831</created>
|
||||||
|
<option name="number" value="Default" />
|
||||||
|
<option name="presentableId" value="Default" />
|
||||||
|
<updated>1643931872831</updated>
|
||||||
|
<workItem from="1643931873891" duration="52000" />
|
||||||
|
<workItem from="1643931940066" duration="3064000" />
|
||||||
|
<workItem from="1643936709048" duration="547000" />
|
||||||
|
<workItem from="1648500425230" duration="246000" />
|
||||||
|
<workItem from="1658028513357" duration="2964000" />
|
||||||
|
<workItem from="1666469240565" duration="7361000" />
|
||||||
|
<workItem from="1666543043382" duration="3699000" />
|
||||||
|
<workItem from="1668047509596" duration="7310000" />
|
||||||
|
<workItem from="1673378530233" duration="327000" />
|
||||||
|
<workItem from="1673538703809" duration="6147000" />
|
||||||
|
<workItem from="1674698447115" duration="249000" />
|
||||||
|
<workItem from="1688939993548" duration="2649000" />
|
||||||
|
</task>
|
||||||
|
<servers />
|
||||||
|
</component>
|
||||||
|
<component name="TypeScriptGeneratedFilesManager">
|
||||||
|
<option name="version" value="3" />
|
||||||
|
</component>
|
||||||
|
<component name="Vcs.Log.Tabs.Properties">
|
||||||
|
<option name="TAB_STATES">
|
||||||
|
<map>
|
||||||
|
<entry key="MAIN">
|
||||||
|
<value>
|
||||||
|
<State />
|
||||||
|
</value>
|
||||||
|
</entry>
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
2122
Cargo.lock
generated
24
Cargo.toml
|
@ -1,24 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "website"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
askama = { version = "0.12.1", features = ["with-axum"] }
|
|
||||||
askama_axum = "0.4.0"
|
|
||||||
axum = "0.7.4"
|
|
||||||
tower-http = { version = "0.5.1", features = ["fs", "trace"] }
|
|
||||||
tokio = { version = "1", features = ["full"] }
|
|
||||||
reqwest = { version = "0.12.4", features = ["json"] }
|
|
||||||
serde = "1.0.201"
|
|
||||||
serde_json = "1.0.117"
|
|
||||||
chrono = "0.4.38"
|
|
||||||
rust_decimal = "1.35.0"
|
|
||||||
anyhow = "1.0.83"
|
|
||||||
env_logger = "0.11.3"
|
|
||||||
log = "0.4.21"
|
|
||||||
dotenv = "0.15.0"
|
|
||||||
dotenv_codegen = "0.15.0"
|
|
||||||
lazy_static = "1.4.0"
|
|
26
Dockerfile
|
@ -1,14 +1,18 @@
|
||||||
FROM rust:1.74.1 as build
|
# build environment
|
||||||
|
FROM node:13.12.0-alpine as build
|
||||||
|
WORKDIR /app
|
||||||
|
ENV PATH /app/node_modules/.bin:$PATH
|
||||||
|
COPY package.json ./
|
||||||
|
COPY package-lock.json ./
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
ENV PKG_CONFIG_ALLOW_CROSS=1
|
RUN npm install react-scripts@3.4.1 -g --silent
|
||||||
|
COPY . ./
|
||||||
WORKDIR .
|
RUN npm run build
|
||||||
COPY . .
|
|
||||||
|
|
||||||
RUN cargo install --path .
|
|
||||||
|
|
||||||
ENV RUST_LOG=info
|
|
||||||
ENV EXPOSE_PORT=80
|
|
||||||
|
|
||||||
|
# production environment
|
||||||
|
FROM nginx:stable-alpine
|
||||||
|
COPY --from=build /app/build /usr/share/nginx/html
|
||||||
|
COPY --from=build /app/nginx/nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
ENTRYPOINT ["website"]
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
|
|
10
README.md
|
@ -1 +1,9 @@
|
||||||
# Nick Web Services Web(site/API)
|
<img src="./src/static/images/NWS_Logo.png" alt="NWS Logo" width="200"/>
|
||||||
|
|
||||||
|
# nws-site
|
||||||
|
The offical website for Nick Web Services (aka NWS)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- Shows uptime of NWS servers
|
||||||
|
- Running on over-engineered infrastructure
|
||||||
|
- Very simple yet effective design
|
||||||
|
|
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 220 B |
Before Width: | Height: | Size: 476 B |
|
@ -1,45 +0,0 @@
|
||||||
* {
|
|
||||||
font-family: serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
th {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
td {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
table, th, td {
|
|
||||||
border: 1px solid black;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: #ccf2b3; /* #ffed8f; */
|
|
||||||
margin: 10px auto;
|
|
||||||
max-width: 750px;
|
|
||||||
width: 95%;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
text-decoration: none;
|
|
||||||
color: #114488;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flag-img {
|
|
||||||
height: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
p.lineitem::after {
|
|
||||||
content: " ";
|
|
||||||
flex: 1;
|
|
||||||
border-bottom: 1px dotted #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link {
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
10
makefile
|
@ -1,10 +0,0 @@
|
||||||
.PHONY: run
|
|
||||||
|
|
||||||
run:
|
|
||||||
RUST_LOG=debug cargo run
|
|
||||||
|
|
||||||
docker-build:
|
|
||||||
docker build . -t smc-website:dev
|
|
||||||
|
|
||||||
docker-run: docker-build
|
|
||||||
docker run -p 8085:80 smc-website:dev
|
|
30
nginx/nginx.conf
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
server {
|
||||||
|
gzip on;
|
||||||
|
gzip_types
|
||||||
|
application/atom+xml
|
||||||
|
application/geo+json
|
||||||
|
application/javascript
|
||||||
|
application/x-javascript
|
||||||
|
application/json
|
||||||
|
application/ld+json
|
||||||
|
application/manifest+json
|
||||||
|
application/rdf+xml
|
||||||
|
application/rss+xml
|
||||||
|
application/xhtml+xml
|
||||||
|
application/xml
|
||||||
|
font/eot
|
||||||
|
font/otf
|
||||||
|
font/ttf
|
||||||
|
image/svg+xml
|
||||||
|
text/css
|
||||||
|
text/javascript
|
||||||
|
text/plain
|
||||||
|
text/xml;
|
||||||
|
gzip_min_length 256;
|
||||||
|
listen 80;
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html index.htm;
|
||||||
|
try_files $uri $uri/ /index.html =404;
|
||||||
|
}
|
||||||
|
}
|
29692
package-lock.json
generated
Normal file
60
package.json
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
{
|
||||||
|
"name": "nws-site",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@testing-library/jest-dom": "^5.16.2",
|
||||||
|
"@testing-library/react": "^12.1.2",
|
||||||
|
"@testing-library/user-event": "^13.5.0",
|
||||||
|
"@types/jest": "^27.4.0",
|
||||||
|
"@types/node": "^16.11.22",
|
||||||
|
"@types/react": "^17.0.39",
|
||||||
|
"@types/react-dom": "^17.0.11",
|
||||||
|
"bootstrap": "^5.1.3",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-bootstrap": "^2.4.0",
|
||||||
|
"react-copy-code": "^2.1.2",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-markdown": "^8.0.3",
|
||||||
|
"react-modal": "^3.16.1",
|
||||||
|
"react-router-dom": "^6.4.2",
|
||||||
|
"react-scripts": "5.0.0",
|
||||||
|
"react-tooltip": "^4.4.3",
|
||||||
|
"strip-markdown": "^5.0.0",
|
||||||
|
"typescript": "^4.5.5",
|
||||||
|
"urijs": "^1.19.11",
|
||||||
|
"web-vitals": "^2.1.4"
|
||||||
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"@types/react": "17.0.2",
|
||||||
|
"@types/react-dom": "17.0.2"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "react-scripts start",
|
||||||
|
"build": "react-scripts build",
|
||||||
|
"test": "react-scripts test",
|
||||||
|
"eject": "react-scripts eject"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"react-app",
|
||||||
|
"react-app/jest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react-modal": "^3.13.1",
|
||||||
|
"@types/urijs": "^1.19.19"
|
||||||
|
}
|
||||||
|
}
|
BIN
public/favicon.ico
Normal file
After Width: | Height: | Size: 3.8 KiB |
42
public/index.html
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Nick Web Services - The best cloud compute platform"
|
||||||
|
/>
|
||||||
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||||
|
<!--
|
||||||
|
manifest.json provides metadata used when your web app is installed on a
|
||||||
|
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||||
|
-->
|
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
|
<!--
|
||||||
|
Notice the use of %PUBLIC_URL% in the tags above.
|
||||||
|
It will be replaced with the URL of the `public` folder during the build.
|
||||||
|
Only files inside the `public` folder can be referenced from the HTML.
|
||||||
|
|
||||||
|
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||||
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
|
-->
|
||||||
|
<title>NWS</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
<!--
|
||||||
|
This HTML file is a template.
|
||||||
|
If you open it directly in the browser, you will see an empty page.
|
||||||
|
|
||||||
|
You can add webfonts, meta tags, or analytics to this file.
|
||||||
|
The build step will place the bundled scripts into the <body> tag.
|
||||||
|
|
||||||
|
To begin the development, run `npm start` or `yarn start`.
|
||||||
|
To create a production bundle, use `npm run build` or `yarn build`.
|
||||||
|
-->
|
||||||
|
</body>
|
||||||
|
</html>
|
BIN
public/logo192.png
Normal file
After Width: | Height: | Size: 5.2 KiB |
BIN
public/logo512.png
Normal file
After Width: | Height: | Size: 9.4 KiB |
25
public/manifest.json
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"short_name": "React App",
|
||||||
|
"name": "Create React App Sample",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "favicon.ico",
|
||||||
|
"sizes": "64x64 32x32 24x24 16x16",
|
||||||
|
"type": "image/x-icon"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo192.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "192x192"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo512.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "512x512"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"theme_color": "#000000",
|
||||||
|
"background_color": "#ffffff"
|
||||||
|
}
|
3
public/robots.txt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
76
src/App.css
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
.App {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
border-radius: 10px;
|
||||||
|
border-color: #000;
|
||||||
|
border-width: 1px;
|
||||||
|
border-style: solid;
|
||||||
|
margin-bottom: 1px;
|
||||||
|
display: block;
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border-radius: 10px !important;
|
||||||
|
border-width: 0px;
|
||||||
|
|
||||||
|
background-color: cornflowerblue;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 3px
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-banner {
|
||||||
|
background-color: #f08080;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 3px;
|
||||||
|
height: max-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.low-severity {
|
||||||
|
background-color: #98fb98
|
||||||
|
}
|
||||||
|
|
||||||
|
.med-severity {
|
||||||
|
background-color: #eee8aa
|
||||||
|
}
|
||||||
|
|
||||||
|
.high-severity {
|
||||||
|
background-color: #f08080
|
||||||
|
}
|
||||||
|
|
||||||
|
.severity-label {
|
||||||
|
border-radius: 10px;
|
||||||
|
text-align: center;
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
padding-top: 5px;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media only screen
|
||||||
|
and (max-width: 992px) {
|
||||||
|
.nws-card-md {
|
||||||
|
border-radius: 20px;
|
||||||
|
background-color: #eee;
|
||||||
|
padding-bottom: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.nws-card {
|
||||||
|
border-radius: 20px;
|
||||||
|
background-color: #eee;
|
||||||
|
padding: .75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
9
src/App.test.tsx
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import React from 'react';
|
||||||
|
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();
|
||||||
|
});
|
16
src/App.tsx
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import React, {useEffect, useState} from 'react';
|
||||||
|
import './App.css';
|
||||||
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
|
import StatusPage from "./components/StatusPage";
|
||||||
|
import Footer from "./components/Footer";
|
||||||
|
import { BrowserRouter, Route, Outlet, Link } from "react-router-dom";
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
11
src/components/Blog.css
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
.blog-card {
|
||||||
|
transition: 1s;
|
||||||
|
width: 80%;
|
||||||
|
background-color: #eee;
|
||||||
|
border-radius: 20px;
|
||||||
|
overflow: clip;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-card:hover {
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
41
src/components/Blogs.tsx
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import {useEffect, useState} from "react";
|
||||||
|
import {Blog, Incident, UptimeResponse} from "../nws-api/types";
|
||||||
|
import {getBlogs, getIncidents, getUptime} from "../nws-api/calls";
|
||||||
|
import "./Blog.css";
|
||||||
|
import ReactMarkdown from 'react-markdown';
|
||||||
|
import strip from 'strip-markdown';
|
||||||
|
|
||||||
|
export default function Blogs(){
|
||||||
|
const [blogs, setBlogs] = useState<Blog[]>([]);
|
||||||
|
|
||||||
|
const fetchBlogs = async () => {
|
||||||
|
let resp: Blog[] = await getBlogs();
|
||||||
|
setBlogs(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchBlogs();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return(
|
||||||
|
<div>
|
||||||
|
<h1>Blogs</h1>
|
||||||
|
<div className={"d-flex justify-content-center"}>
|
||||||
|
{blogs.map((e)=>{
|
||||||
|
return(
|
||||||
|
<div className={"blog-card row"} onClick={()=>{window.location.href=`/blog?id=${e.id}`}}>
|
||||||
|
<img src={e.imageUrl} className={"col-md-4 m-0 p-0"}/>
|
||||||
|
<div className={"p-2 col-md-8"}>
|
||||||
|
<h2>{e.title}</h2>
|
||||||
|
<p>By: {e.author}</p>
|
||||||
|
<p style={{maxHeight: 100, overflow: "clip"}}><ReactMarkdown remarkPlugins={[strip]}>{e.content}</ReactMarkdown>...</p>
|
||||||
|
|
||||||
|
<p><b>Click to read more</b></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
32
src/components/CreateCruisePage.css
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
.pill {
|
||||||
|
padding: 1px;
|
||||||
|
transition: .5s;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill-selected{
|
||||||
|
font-weight: bold;
|
||||||
|
border-radius: 20px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill-container {
|
||||||
|
padding: 2px;
|
||||||
|
border-radius: 20px;
|
||||||
|
background-color: #aaaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-text {
|
||||||
|
font-style: italic;
|
||||||
|
margin: 0;
|
||||||
|
font-size: .9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-text{
|
||||||
|
margin: 1em 0 0;
|
||||||
|
}
|
212
src/components/CreateCruisePage.tsx
Normal file
|
@ -0,0 +1,212 @@
|
||||||
|
import {useState} from "react";
|
||||||
|
import URI from "urijs";
|
||||||
|
import {Namespace} from "../nws-api/types";
|
||||||
|
import {useNWSAccount, useNWSAuthKey} from "../nws-api/hooks";
|
||||||
|
import {useSearchParams} from "react-router-dom";
|
||||||
|
import './CreateCruisePage.css';
|
||||||
|
|
||||||
|
export default function CreateCruisePage() {
|
||||||
|
const [page, setPage] = useState('info');
|
||||||
|
const [strat, setStrat] = useState<'raw-html' | 'react-js' | 'raw-html-anthracite' | 'docker' | 'default'>('default');
|
||||||
|
const [owner, setOwner] = useState('');
|
||||||
|
const [repo, setRepo] = useState('');
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
|
||||||
|
const [gitUriInput, setGUI] = useState('');
|
||||||
|
const [hostUriInput, setHUI] = useState('');
|
||||||
|
|
||||||
|
const authKey = useNWSAuthKey();
|
||||||
|
const acct = useNWSAccount();
|
||||||
|
|
||||||
|
const [search, useSearch] = useSearchParams();
|
||||||
|
|
||||||
|
function deploy() {
|
||||||
|
fetch("https://api-nws.nickorlow.com/" + acct!.id + "/service",
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
"Authorization": authKey,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
"serviceName": name,
|
||||||
|
"containerUrl": `ghcr.io/${owner}/${repo}`,
|
||||||
|
"namespaceId": search.get("namespaceId"),
|
||||||
|
"serviceUrl": "https://"+hostUriInput,
|
||||||
|
"hostnames": []
|
||||||
|
})
|
||||||
|
}).then((response)=> {
|
||||||
|
if(response.status === 200) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}).catch((ex) =>{
|
||||||
|
alert(ex)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={"App"}>
|
||||||
|
<h1 className={"mb-5 mt-3 fw-bolder"}>Create Container Deployment</h1>
|
||||||
|
<div className={"pill-container row justify-content-evenly mb-5"} style={{width: "80%"}}>
|
||||||
|
<p className={"pill col-md-2 " + (page === 'info' ? "pill-selected" : "")}>About</p>
|
||||||
|
<p className={"pill col-md-2 " + (page === 'framework-hostname' ? "pill-selected" : "")}>Deployment Info</p>
|
||||||
|
<p className={"pill col-md-2 " + (page === 'scriptgen' ? "pill-selected" : "")}>Repo Setup</p>
|
||||||
|
<p className={"pill col-md-2 " + (page === 'dns' ? "pill-selected" : "")}>DNS Configuration</p>
|
||||||
|
</div>
|
||||||
|
<div style={{width: "75vw"}}>
|
||||||
|
{
|
||||||
|
page === 'info' &&
|
||||||
|
<div>
|
||||||
|
<h3>Some information before we get started:</h3>
|
||||||
|
<ul>
|
||||||
|
<li>NWS is free to use</li>
|
||||||
|
<li>Currently, your DNS provider must support DNS flattening if you intend to point your root domain (e.g. nickorlow.com) to NWS. Subdomains should work fine though. (Cloudflare, Route 53, and Pagely). (Moving to Cloudflare is pretty easy)</li>
|
||||||
|
<li>Through the Web UI, you may only add one domain name. If you need to add more, <a href={"mailto:nws-support@nickorlow.com"}>contact me</a></li>
|
||||||
|
<li>NWS does not guarantee any uptime</li>
|
||||||
|
<li>NWS is run by a college student with little free time, support may reflect this</li>
|
||||||
|
<li>This platform is very early in development. It may require you to have some technical
|
||||||
|
knowledge.
|
||||||
|
</li>
|
||||||
|
<li>NWS may cease operations in the event of a widespread viral infection transmitted via
|
||||||
|
bites or contact with bodily fluids that causes human corpses to reanimate and seek to
|
||||||
|
consume living human flesh, blood, brain or nerve tissue and is likely to result in the
|
||||||
|
fall of organized civilization.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<button className={"float-end"} onClick={()=>setPage('framework-hostname')}>I Understand, Continue</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
page === 'framework-hostname' &&
|
||||||
|
<div>
|
||||||
|
<h5 className={"label-text"}>What is this deployment's name?</h5>
|
||||||
|
<p className={"help-text"}>May only be lowercase letters and dashes, max 20 chars</p>
|
||||||
|
|
||||||
|
<input value={name} onChange={(e)=>{setName(e.currentTarget.value)}}/>
|
||||||
|
|
||||||
|
<h5 className={"label-text"}>Choose a Dockerfile template</h5>
|
||||||
|
<p className={"help-text"}>Don't see your technology/framework? Email me: <a href={"mailto:nws-support@nickorlow.com"}>nws-support@nickorlow.com</a></p>
|
||||||
|
<select>
|
||||||
|
<option hidden>Select a template</option>
|
||||||
|
<option id={"raw-html"} onClick={()=>setStrat('docker')}>I already have a Dockerfile in the root of my repository</option>
|
||||||
|
<option id={"raw-html"} onClick={()=>setStrat('raw-html-anthracite')}>Raw HTML (with Anthracite Web Server, created by Nick)</option>
|
||||||
|
<option id={"raw-html"} onClick={()=>setStrat('raw-html')}>Raw HTML (with NGINX)</option>
|
||||||
|
<option id={"react-js"} onClick={()=>setStrat('react-js')}>React JS</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<h5 className={"label-text"}>What is the url of the GitHub repo where your code is hosted?</h5>
|
||||||
|
<p className={"help-text"}>Other git hosting providers are not currently supported through the Web UI</p>
|
||||||
|
<p className={"help-text"}>The repo must be public to create it through the Web UI</p>
|
||||||
|
<input placeholder={"https://github.com/nickorlow/personal-site"} value={gitUriInput} onInput={(e)=>{setGUI(e.currentTarget.value)}}/>
|
||||||
|
|
||||||
|
<h5 className={"label-text"}>What domain name will you use with your website?</h5>
|
||||||
|
<input placeholder={"nws.nickorlow.com"} value={hostUriInput} onChange={(e)=>{setHUI(e.currentTarget.value)}}/>
|
||||||
|
|
||||||
|
<button onClick={()=>{setPage('info')}}>Back</button>
|
||||||
|
<button className={"float-end"} onClick={()=>{
|
||||||
|
try {
|
||||||
|
let git_url = new URL(gitUriInput);
|
||||||
|
|
||||||
|
if (git_url.host !== 'github.com') {
|
||||||
|
alert('Only github is supported!')
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
console.log(git_url.pathname.split('/'))
|
||||||
|
setOwner(git_url.pathname.split('/')[1])
|
||||||
|
setRepo(git_url.pathname.split('/')[2])
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert('Invalid GitHub URL. Should be of format https://github.com/owner/repo')
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
let url = new URL("https://"+hostUriInput);
|
||||||
|
} catch (e) {
|
||||||
|
alert('Invalid domain! Should be format subdomain.domain.tld. Don\'t include protocols in front (i.e https://)')
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!/^[a-z-]+$/.test(name) || name.length > 20 || name.length == 0) {
|
||||||
|
alert('may only be lowercase and dashes and under 20 chars')
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setPage('scriptgen')
|
||||||
|
}}>Continue</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
page === 'scriptgen' &&
|
||||||
|
<div>
|
||||||
|
<h4>Copy & Paste the below into your terminal to add NWS deployment scripts to your webapp</h4>
|
||||||
|
<code lang={"shell"} style={{backgroundColor: "#bbbbbb", padding: 5, borderRadius: 10}}>
|
||||||
|
curl -s https://raw.githubusercontent.com/nickorlow/nws-ghactions-templates/main/add-nws.sh | bash -s {strat} {owner} {repo}
|
||||||
|
</code>
|
||||||
|
<br/><span>Ensure the script finishes running before continuing</span>
|
||||||
|
<br/><span>For your security, you may view the source code of the script <a href="https://github.com/nickorlow/nws-ghactions-templates/blob/main/add-nws.sh" target="_blank">here</a></span>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<button onClick={()=>setPage('framework-hostname')}>Back</button>
|
||||||
|
<button className={"float-end"} onClick={()=>{
|
||||||
|
deploy();
|
||||||
|
setPage('dns');
|
||||||
|
}}>Continue</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
page === 'dns' &&
|
||||||
|
<div>
|
||||||
|
<h4>Add the following DNS entry to {new URI("https://"+hostUriInput).hostname()}</h4>
|
||||||
|
{
|
||||||
|
new URI("https://"+hostUriInput).subdomain().length == 0 &&
|
||||||
|
<div>
|
||||||
|
<p>If your DNS provider is:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Cloudflare</li>
|
||||||
|
<li>Route 53</li>
|
||||||
|
<li>Pagely</li>
|
||||||
|
</ul>
|
||||||
|
<p>Type: CNAME</p>
|
||||||
|
<p>Name: @ ({hostUriInput})</p>
|
||||||
|
<p>Value: entry.nws.nickorlow.com</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
new URI("https://"+hostUriInput).subdomain().length > 0 &&
|
||||||
|
<div>
|
||||||
|
<p>Type: CNAME</p>
|
||||||
|
<p>Name: {new URI("https://"+hostUriInput).subdomain()} ({new URI("https://"+hostUriInput).hostname()})</p>
|
||||||
|
<p>Value: entry.nws.nickorlow.com</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<br/>
|
||||||
|
<button onClick={()=>setPage('done')}>Finish Setup</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
page === 'done' &&
|
||||||
|
<div>
|
||||||
|
<h3>Welcome to NWS!</h3> <br/>
|
||||||
|
<p>Your site should be avaliable on NWS momentairly</p> <br/>
|
||||||
|
<p>It would be great if you could add the following to your website:</p><br/>
|
||||||
|
<code lang={"html"} style={{ backgroundColor: "#bbbbbb", padding: 5, borderRadius: 10 }}>
|
||||||
|
<p>Hosting provided by <a href="https://nws.nickorlow.com">NWS</a></p>
|
||||||
|
</code> <br/> <br/>
|
||||||
|
{ strat === "raw-html-anthracite" &&
|
||||||
|
<div>
|
||||||
|
<p>It would be great if you could add the following to your website as well:</p><br/>
|
||||||
|
<code lang={"html"} style={{ backgroundColor: "#bbbbbb", padding: 5, borderRadius: 10 }}>
|
||||||
|
<p>Powered by <a href="https://github.com/nickorlow/anthracite">Anthracite Web Server</a></p>
|
||||||
|
</code> <br/> <br/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
}
|
||||||
|
<button onClick={()=>{window.location.href="/dashboard"}}>Go to Dashboard</button> <br/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
113
src/components/DashboardPage.tsx
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
import {Account, Namespace, Service, SessionKey} from "../nws-api/types";
|
||||||
|
import {
|
||||||
|
useGetAccountNamespaces,
|
||||||
|
useGetAccountServices,
|
||||||
|
useGetServicesInNamespace,
|
||||||
|
useLoggedInRedirect,
|
||||||
|
useNWSAccount,
|
||||||
|
useNWSAuthKey
|
||||||
|
} from "../nws-api/hooks";
|
||||||
|
import {
|
||||||
|
createNamespace
|
||||||
|
} from "../nws-api/calls"
|
||||||
|
import {useState, useEffect} from "react";
|
||||||
|
import {enableSSL} from "../nws-api/calls";
|
||||||
|
|
||||||
|
|
||||||
|
export default function DashboardPage() {
|
||||||
|
useLoggedInRedirect();
|
||||||
|
let account: Account | undefined = useNWSAccount();
|
||||||
|
let {setNs, services, ns} = useGetServicesInNamespace();
|
||||||
|
let namespaces: Namespace[] = useGetAccountNamespaces();
|
||||||
|
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
|
return(
|
||||||
|
<div style={{minHeight: "100vh", padding: "50px"}}>
|
||||||
|
<div className={"row"}>
|
||||||
|
<p>I don't really know what I was on when I wrote this but a lot of things in the web ui are goofy, sorry about that.. :/ A new one is on its way.</p>
|
||||||
|
<h1 className={"col-md-10 col-12"}>Welcome to NWS, {account?.name}!</h1>
|
||||||
|
<div className={"col-12 col-md-2"}>
|
||||||
|
<p>Namespace</p>
|
||||||
|
<select className="w-100">
|
||||||
|
<option value="" disabled selected={!urlParams.has('namespace')}>Select Namespace...</option>
|
||||||
|
{
|
||||||
|
namespaces.map((e)=>{
|
||||||
|
if (urlParams.get('namespace') === e.id && ns?.id != e.id)
|
||||||
|
setNs(e);
|
||||||
|
return <option onClick={(a)=>{
|
||||||
|
const url = new URL(window.location.toString());
|
||||||
|
url.searchParams.set('namespace', e.id);
|
||||||
|
window.history.pushState(null, '', url.toString());
|
||||||
|
setNs(e);
|
||||||
|
}} selected={urlParams.get('namespace') === e.id}>{e.name}</option>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
<div>
|
||||||
|
<button className="w-100 p-0 mt-2" onClick={async () => {
|
||||||
|
let name = prompt("Enter a name for the namespace");
|
||||||
|
let rawSession: string | null = localStorage.getItem("session_key");
|
||||||
|
|
||||||
|
if (rawSession != null) {
|
||||||
|
let session: SessionKey = JSON.parse(rawSession);
|
||||||
|
let newNamespace = await createNamespace(name!, account!.id!, session);
|
||||||
|
const url = new URL(window.location.toString());
|
||||||
|
url.searchParams.set('namespace', newNamespace.id);
|
||||||
|
window.history.pushState(null, '', url.toString());
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
alert("Error creating namespace");
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
Create Namespace
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr/>
|
||||||
|
<div className={"d-flex justify-content-between"}>
|
||||||
|
<h2>Container Deployment Services</h2>
|
||||||
|
<button onClick={(e) => {
|
||||||
|
if (ns != null)
|
||||||
|
window.location.href = "/cruise/new?namespaceId="+ns!.id
|
||||||
|
else
|
||||||
|
alert("Please select a namespace!")
|
||||||
|
}}>Create Container Deployment</button>
|
||||||
|
</div>
|
||||||
|
<div className={"row"}>
|
||||||
|
|
||||||
|
{services.map((e)=>{
|
||||||
|
return (
|
||||||
|
<div className={"col-4"} style={{ padding: 5}}>
|
||||||
|
<div style={{backgroundColor: "#eee", borderRadius: 20, padding: 5}}>
|
||||||
|
<h3>{e.serviceName}</h3>
|
||||||
|
<p><b>Application Id</b></p>
|
||||||
|
<p>{e.serviceId}</p>
|
||||||
|
{e.hostnames.map((host)=>{
|
||||||
|
return (
|
||||||
|
<div className={"mb-2 p-2 d-flex justify-content-between"}>
|
||||||
|
<a href={"http://"+host.hostname}>{host.hostname}</a>
|
||||||
|
{!host.isSslEnabled ? <div><button onClick={async () => {
|
||||||
|
let rawSession: string | null = localStorage.getItem("session_key");
|
||||||
|
|
||||||
|
if (rawSession != null) {
|
||||||
|
let session: SessionKey = JSON.parse(rawSession);
|
||||||
|
await enableSSL(account!.id!, e.serviceId, host.hostname, session);
|
||||||
|
alert(`SSL has been enabled on the hostname ${host.hostname}. It should be ready in 2-5 minutes.`);
|
||||||
|
// hack but whatever
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
}}>Enable SSL</button></div> : <p>SSL is enabled!</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
10
src/components/Footer.tsx
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export default function Footer() {
|
||||||
|
return (
|
||||||
|
<footer className={"mt-2 p-3"} style={{backgroundColor: "#eee"}}>
|
||||||
|
<p>SMC is owned and operated by <a href={"http://nickorlow.com"}>Nicholas Orlowsky</a>.</p>
|
||||||
|
<p>Copyright © Nicholas Orlowsky {new Date().getFullYear()}</p>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
68
src/components/HomePage.tsx
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import NWSLogo from "../static/images/NWS_Logo_Transparent.png";
|
||||||
|
import UptimeCard from "./UptimeCard";
|
||||||
|
import IncidentCard from "./IncidentCard";
|
||||||
|
import Footer from "./Footer";
|
||||||
|
import React, {useEffect, useState} from "react";
|
||||||
|
import {Incident, UptimeResponse} from "../nws-api/types";
|
||||||
|
import {getIncidents, getUptime} from "../nws-api/calls";
|
||||||
|
import "../App.css";
|
||||||
|
import UptimeComparisonCard from "./UptimeComparisonCard";
|
||||||
|
import UptimeLabelCard from "./UptimeLabelCard";
|
||||||
|
|
||||||
|
|
||||||
|
export default function HomePage() {
|
||||||
|
|
||||||
|
const [uptime, setUptime] = useState<UptimeResponse>({datacenters: [], services: [], competitors: [], lastUpdated: ""});
|
||||||
|
const [incidents, setIncidents] = useState<Incident[]>([]);
|
||||||
|
|
||||||
|
const fetchUptime = async () => {
|
||||||
|
let resp: UptimeResponse = await getUptime();
|
||||||
|
setUptime(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchIncidents = async () => {
|
||||||
|
let resp: Incident[] = await getIncidents();
|
||||||
|
setIncidents(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchUptime();
|
||||||
|
fetchIncidents();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return(
|
||||||
|
<div className="App">
|
||||||
|
<div className={"w-100 row"}>
|
||||||
|
<div className={"col-md-6 d-flex justify-content-center flex-column align-items-center"}>
|
||||||
|
<img src={NWSLogo} alt="nws-logo" style={{width: "70%"}}/>
|
||||||
|
</div>
|
||||||
|
<div className={"col-md-6 text-center d-flex justify-content-center flex-column align-items-center"}>
|
||||||
|
<h1>Sharpe Mountain Compute</h1>
|
||||||
|
<p className={"col-md-6 text-center"}>
|
||||||
|
Sharpe Mountain Compute (fka Nick Web Services) is a reliable cloud compute provider. SMC is dedicated to achieving maximum uptime at a lower cost than traditional cloud compute providers.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={"w-100 mt-2 flex justify-content-center align-content-center text-center"}>
|
||||||
|
<h3><i>100% Uptime from 1/1/2023 - 11/8/2023</i></h3>
|
||||||
|
<h4><a href={"https://youtu.be/WHdXWMFHuqA"} target="_blank" rel="noopener noreferrer">Watch the SMC Deployment Demo</a></h4>
|
||||||
|
</div>
|
||||||
|
<div style={{width: '75vw'}}>
|
||||||
|
<hr/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={"text-left row"} style={{width: '75vw'}}>
|
||||||
|
<h2>Compare us to our competitors</h2>
|
||||||
|
<p>Last updated at {new Date(uptime.lastUpdated).toLocaleString()}</p>
|
||||||
|
<div className={"col-12 row w-100 m-0 align-content-center d-flex justify-content-center pt-2"}>
|
||||||
|
<UptimeLabelCard/>
|
||||||
|
{uptime.competitors.sort((a,b)=>{return b.uptimeMonth === a.uptimeMonth ? (a.name === "NWS" ? -1000 : b.name.localeCompare(a.name)) : b.uptimeMonth - a.uptimeMonth}).map((e) => {
|
||||||
|
return (
|
||||||
|
<UptimeComparisonCard uptime={e} isService={false}/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
30
src/components/IncidentCard.tsx
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import {Incident} from "../nws-api/types";
|
||||||
|
import ReactTooltip from "react-tooltip";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export default function IncidentCard(props: {incident: Incident}) {
|
||||||
|
|
||||||
|
const severityText: string[] = [
|
||||||
|
"Low Severity means that this issue does not affect any services running on NWS.",
|
||||||
|
"Medium Severity means that this issue may cause some slowdowns or outages on some services.",
|
||||||
|
"High Severity means that this issue causes an outage on the entire NWS network or most of the services running on it."
|
||||||
|
];
|
||||||
|
let severityClass: string = props.incident.severity == 0 ? 'low' : (props.incident.severity == 1 ? 'med' : 'high');
|
||||||
|
let severityString: string = props.incident.severity == 0 ? 'Low' : (props.incident.severity == 1 ? 'Medium' : 'High');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`row text-left nws-card`} style={{width: '75vw'}}>
|
||||||
|
<p className={"col-md-10 col-12 mb-2"}><b>{props.incident.title}</b></p>
|
||||||
|
<div className={`col-md-2 col-12 mb-2`}>
|
||||||
|
<div className={`severity-label w-max-content ${severityClass}-severity`}
|
||||||
|
data-tip={severityText[props.incident.severity]}>
|
||||||
|
<b>
|
||||||
|
{severityString} Severity ⓘ
|
||||||
|
</b>
|
||||||
|
<ReactTooltip />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className={"mb-0"}>{props.incident.description}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
32
src/components/Login.css
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
.login-box {
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 1px;
|
||||||
|
border-color: #aaa;
|
||||||
|
border-radius: 10px;
|
||||||
|
|
||||||
|
align-self: center;
|
||||||
|
justify-self: center;
|
||||||
|
|
||||||
|
width: 500px;
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-label {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button {
|
||||||
|
border-radius: 10px;
|
||||||
|
border-width: 0px;
|
||||||
|
|
||||||
|
background-color: cornflowerblue;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 3px
|
||||||
|
}
|
64
src/components/LoginPage.tsx
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
import "./Login.css";
|
||||||
|
import {useEffect, useState} from "react";
|
||||||
|
import {Account, ApiError, SessionKey} from "../nws-api/types";
|
||||||
|
import {useNonLoggedInRedirect} from "../nws-api/hooks";
|
||||||
|
|
||||||
|
export default function LoginPage() {
|
||||||
|
|
||||||
|
useNonLoggedInRedirect();
|
||||||
|
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string>("");
|
||||||
|
const [email, setEmail] = useState<string>("");
|
||||||
|
const [password, setPassword] = useState<string>("");
|
||||||
|
|
||||||
|
function loginUser() {
|
||||||
|
if(email == "" || password == "") {
|
||||||
|
setErrorMessage("Please enter an email and password");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let acc: Account = {
|
||||||
|
email: email,
|
||||||
|
password: password
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch("https://api-nws.nickorlow.com/Account/session",{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(acc)
|
||||||
|
}).then((result) => {
|
||||||
|
if(result.status == 200) {
|
||||||
|
result.json().then((o: SessionKey)=>{
|
||||||
|
localStorage.setItem("session_key", JSON.stringify(o));
|
||||||
|
window.location.href = '/dashboard';
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
setErrorMessage("Server Error (This is NWS' fault)");
|
||||||
|
}
|
||||||
|
}).catch((e) =>{
|
||||||
|
setErrorMessage("Server Error (This is NWS' fault)");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return(
|
||||||
|
<div style={{minHeight: "100vh", display: "grid", width: "100%"}}>
|
||||||
|
<div className={"login-box"}>
|
||||||
|
<h3>Login to NWS Dashboard</h3>
|
||||||
|
{ errorMessage != "" &&
|
||||||
|
<div className={"error-banner"}>
|
||||||
|
<p style={{color: "black"}}>{errorMessage}</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<p className={"login-label"}>E-Mail Address</p>
|
||||||
|
<input onChange={(e)=>{setEmail(e.target.value)}} className={"login-input"}/>
|
||||||
|
<p className={"login-label"}>Password</p>
|
||||||
|
<input onChange={(e)=>{setPassword(e.target.value)}}className={"login-input"} type={"password"}/>
|
||||||
|
<button className={"login-button"} onClick={loginUser}>Login</button>
|
||||||
|
<p>No account? <a href={"/register"}>Register Here!</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
10
src/components/NotFoundPage.tsx
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
export default function NotFoundPage() {
|
||||||
|
return(
|
||||||
|
<div style={{width: "100vw", height: "100vh", display: "flex", justifyContent: "center", alignContent: "center", alignItems: "center"}}>
|
||||||
|
<div>
|
||||||
|
<h1>Not Found :(</h1>
|
||||||
|
<a href={"/"}>Home</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
32
src/components/RegisterPage.css
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
.reg-box {
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 1px;
|
||||||
|
border-color: #aaa;
|
||||||
|
border-radius: 10px;
|
||||||
|
|
||||||
|
align-self: center;
|
||||||
|
justify-self: center;
|
||||||
|
|
||||||
|
width: 500px;
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reg-label {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reg-button {
|
||||||
|
border-radius: 10px;
|
||||||
|
border-width: 0px;
|
||||||
|
|
||||||
|
background-color: cornflowerblue;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 3px
|
||||||
|
}
|
105
src/components/RegisterPage.tsx
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
import "./RegisterPage.css";
|
||||||
|
import {useState} from "react";
|
||||||
|
import {Account, ApiError} from "../nws-api/types";
|
||||||
|
import {useNonLoggedInRedirect} from "../nws-api/hooks";
|
||||||
|
|
||||||
|
export default function RegisterPage() {
|
||||||
|
|
||||||
|
const [errorMessage, setErrorMessage] = useState<String>("");
|
||||||
|
const [name, setName] = useState<string>("");
|
||||||
|
const [email, setEmail] = useState<string>("");
|
||||||
|
const [password, setPassword] = useState<string>("");
|
||||||
|
const [cpassword, setCpassword] = useState<string>("");
|
||||||
|
const [inviteCode, setInviteCode] = useState<string>("");
|
||||||
|
const [didRegister, setDidRegister] = useState<Boolean>(false);
|
||||||
|
|
||||||
|
async function createAccount() {
|
||||||
|
|
||||||
|
if(name == "" || email == "" || password == "" || cpassword == "" || inviteCode == "") {
|
||||||
|
setErrorMessage("You must fill out all information before registering.")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(cpassword != password) {
|
||||||
|
setErrorMessage("Passwords don't match!")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!email
|
||||||
|
.toLowerCase()
|
||||||
|
.match(
|
||||||
|
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
||||||
|
)) {
|
||||||
|
setErrorMessage("You have entered an invalid E-Mail address.")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let newAcc: Account = {
|
||||||
|
email: email,
|
||||||
|
name: name,
|
||||||
|
password: password
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch("https://api-nws.nickorlow.com/Account",{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Invite-Code': inviteCode
|
||||||
|
},
|
||||||
|
body: JSON.stringify(newAcc)
|
||||||
|
}).then((result) => {
|
||||||
|
if(result.status == 200) {
|
||||||
|
setDidRegister(true);
|
||||||
|
} else {
|
||||||
|
result.json().then((o: ApiError) => {
|
||||||
|
setErrorMessage(o.ErrorMessage);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).catch((e) =>{
|
||||||
|
setErrorMessage("Server Error (This is NWS' fault)")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
useNonLoggedInRedirect();
|
||||||
|
|
||||||
|
return(
|
||||||
|
<div style={{minHeight: "100vh", display: "grid", width: "100%"}}>
|
||||||
|
<div className={"reg-box"} style={{display: didRegister ? "none" : "flex"}}>
|
||||||
|
<h3>Create an NWS Account</h3>
|
||||||
|
|
||||||
|
{ errorMessage != "" &&
|
||||||
|
<div className={"error-banner"}>
|
||||||
|
<p style={{color: "black"}}>{errorMessage}</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<p className={"reg-label"}>Name</p>
|
||||||
|
<input onChange={(e)=>setName(e.target.value)} className={"reg-input"}/>
|
||||||
|
|
||||||
|
<p className={"reg-label"}>E-Mail Address</p>
|
||||||
|
<input onChange={(e)=>setEmail(e.target.value)} className={"reg-input"}/>
|
||||||
|
|
||||||
|
<p className={"reg-label"}>Password</p>
|
||||||
|
<input onChange={(e)=>setPassword(e.target.value)} className={"reg-input"} type={"password"}/>
|
||||||
|
|
||||||
|
<p className={"reg-label"}>Confirm</p>
|
||||||
|
<input onChange={(e)=>setCpassword(e.target.value)} className={"reg-input"} type={"password"}/>
|
||||||
|
|
||||||
|
<div style={{width: "100%", marginTop: 10, marginBottom: 10, padding: 10, backgroundColor: "#eee", borderRadius: 10}}>
|
||||||
|
<p className={"reg-label"}>Invite Code</p>
|
||||||
|
<input onChange={(e)=>setInviteCode(e.target.value)} style={{width: "100%"}}/>
|
||||||
|
<small>Currently, NWS is invite only. Email me to get an invite code.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button onClick={createAccount} className={"reg-button"}>Create Account</button>
|
||||||
|
<p>Already have an account? <a href={"/login"}>Login Here!</a></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={"reg-box"} style={{display: didRegister ? "flex" : "none"}}>
|
||||||
|
<h3>Successfully Registered!</h3>
|
||||||
|
|
||||||
|
<p><a href="/login">Proceed to login</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
73
src/components/StatusPage.tsx
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
import NWSLogo from "../static/images/NWS_Logo.png";
|
||||||
|
import UptimeCard from "./UptimeCard";
|
||||||
|
import IncidentCard from "./IncidentCard";
|
||||||
|
import Footer from "./Footer";
|
||||||
|
import React, {useEffect, useState} from "react";
|
||||||
|
import {Incident, UptimeResponse} from "../nws-api/types";
|
||||||
|
import {getIncidents, getUptime} from "../nws-api/calls";
|
||||||
|
import "../App.css";
|
||||||
|
|
||||||
|
|
||||||
|
export default function StatusPage() {
|
||||||
|
|
||||||
|
const [uptime, setUptime] = useState<UptimeResponse>({datacenters: [], services: [], competitors: [], lastUpdated: ""});
|
||||||
|
const [incidents, setIncidents] = useState<Incident[]>([]);
|
||||||
|
|
||||||
|
const fetchUptime = async () => {
|
||||||
|
let resp: UptimeResponse = await getUptime();
|
||||||
|
setUptime(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchIncidents = async () => {
|
||||||
|
let resp: Incident[] = await getIncidents();
|
||||||
|
setIncidents(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchUptime();
|
||||||
|
fetchIncidents();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return(
|
||||||
|
<div className="App" style={{padding: 20}}>
|
||||||
|
<div className={"text-left row"} style={{width: '75vw'}}>
|
||||||
|
<h1>NWS System Status</h1>
|
||||||
|
<p>Last updated at {new Date(uptime.lastUpdated).toLocaleString()}</p>
|
||||||
|
<div className={"col-md-6 col-12"}>
|
||||||
|
<h3>Service Status</h3>
|
||||||
|
{uptime.services.map((e) => {
|
||||||
|
return (
|
||||||
|
<UptimeCard uptime={e} isService={true}/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className={"col-md-6 col-12"}>
|
||||||
|
<h3>Datacenter Status</h3>
|
||||||
|
{uptime.datacenters.map((e) => {
|
||||||
|
return (
|
||||||
|
<UptimeCard uptime={e} isService={false}/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{width: '75vw'}}>
|
||||||
|
<hr/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3>Service Alerts</h3>
|
||||||
|
{incidents.map((e) => {
|
||||||
|
return (
|
||||||
|
<IncidentCard incident={e}/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{incidents.length == 0 &&
|
||||||
|
<div className={`row text-center`} style={{width: '75vw'}}>
|
||||||
|
<h5 className={"col-12"}>No service alerts.</h5>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
46
src/components/UptimeCard.css
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
.uptime-lnk {
|
||||||
|
font-weight: bolder;
|
||||||
|
color: #2f2f2f;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: .5s;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uptime-lnk:hover {
|
||||||
|
color: #e08b0d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uptime-modal {
|
||||||
|
border-radius: 20px;
|
||||||
|
border-color: transparent;
|
||||||
|
background-color: #eee;
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
-webkit-transform: translate(-50%, -50%);
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
|
||||||
|
|
||||||
|
width: 500px;
|
||||||
|
max-width: 100vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-perfect {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #069D06
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-middle {
|
||||||
|
color: #877E1C
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-bad {
|
||||||
|
color: #921111
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
height: 1.1px;
|
||||||
|
}
|
52
src/components/UptimeCard.tsx
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import {UptimeRecord} from "../nws-api/types";
|
||||||
|
import React, {useState} from "react";
|
||||||
|
import '../App.css';
|
||||||
|
import "./UptimeCard.css"
|
||||||
|
import Modal from "react-modal";
|
||||||
|
|
||||||
|
export default function UptimeCard(props: {uptime: UptimeRecord, isService: boolean}) {
|
||||||
|
const [isModalOpen, setModalOpen] = useState(false);
|
||||||
|
return(
|
||||||
|
<div className={"nws-card row mb-2 m-0"} style={{maxWidth: '100%'}}>
|
||||||
|
<h4 className={"col-md-9 col-12 uptime-lnk"} onClick={()=>setModalOpen(true)}>{props.uptime.name}</h4>
|
||||||
|
|
||||||
|
<div className={`col-md-3 col-12 d-flex d-md-none justify-content-start`}>
|
||||||
|
<p className={`fw-bold severity-label w-100
|
||||||
|
${props.uptime.isUp ? 'low' : (props.uptime.undergoingMaintenance ? 'med' : 'high')}-severity`}
|
||||||
|
>
|
||||||
|
{props.uptime.isUp ? 'Up' : (props.uptime.undergoingMaintenance ? 'Maintenance' : 'Down')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className={`d-md-flex col-md-3 col-12 d-none justify-content-end`}>
|
||||||
|
<p className={`fw-bold severity-label
|
||||||
|
${props.uptime.isUp ? 'low' : (props.uptime.undergoingMaintenance ? 'med' : 'high')}-severity`}
|
||||||
|
style={{width: "max-content", height: 'max-content'}}>
|
||||||
|
{props.uptime.isUp ? 'Up' : (props.uptime.undergoingMaintenance ? 'Maintenance' : 'Down')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p className={"col-md-12 col-12"}><b>Last Month Uptime:</b> {props.uptime.uptimeMonth}%</p>
|
||||||
|
<p className={"col-md-6 col-12"}><b>{new Date().getFullYear()} Uptime:</b> {props.uptime.uptimeYtd}%</p>
|
||||||
|
<p className={"col-md-6 col-12"}><b>Avg Response Time:</b> {props.uptime.averageResponseTime}ms</p>
|
||||||
|
<Modal className={"uptime-modal"} isOpen={isModalOpen}>
|
||||||
|
<div className={"mb-3"}>
|
||||||
|
<h1 className={"mb-0"}>{props.uptime.name}</h1>
|
||||||
|
{props.uptime.url && <p>(<a href={props.uptime.url}>{props.uptime.url}</a>)</p>}
|
||||||
|
</div>
|
||||||
|
<div className={"mb-3"}>
|
||||||
|
<p>Monitoring since {props.uptime.monitorStart}</p>
|
||||||
|
<p><b>{new Date().getFullYear()} Uptime (YTD):</b> {props.uptime.uptimeYtd}%</p>
|
||||||
|
<p><b>Last Month Uptime:</b> {props.uptime.uptimeMonth}%</p>
|
||||||
|
<p><b>All-Time Uptime:</b> {props.uptime.uptimeAllTime}%</p>
|
||||||
|
<p><b>Average Response Time:</b> {props.uptime.averageResponseTime}ms</p>
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
props.isService &&
|
||||||
|
<div className={"mb-3"}>
|
||||||
|
<i>Note that the uptime and performance of services hosted on NWS may be affected by factors not controlled by NWS such as bad bad optimization or buggy software.</i>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<button className={"w-100"} onClick={()=>setModalOpen(false)}>Close</button>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
90
src/components/UptimeComparisonCard.tsx
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
import {UptimeRecord} from "../nws-api/types";
|
||||||
|
import React, {useState} from "react";
|
||||||
|
import '../App.css';
|
||||||
|
import "./UptimeCard.css"
|
||||||
|
import Modal from "react-modal";
|
||||||
|
|
||||||
|
function getUptimeClass(uptime: number) {
|
||||||
|
if(uptime === 100) {
|
||||||
|
return "stat-perfect";
|
||||||
|
}
|
||||||
|
|
||||||
|
if(uptime >= 95) {
|
||||||
|
return "stat-middle";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "stat-bad";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getResponseTimeClass(respTime: number) {
|
||||||
|
// https://www.littledata.io/average/server-response-time
|
||||||
|
if(respTime < 205) {
|
||||||
|
return "stat-perfect";
|
||||||
|
}
|
||||||
|
|
||||||
|
if(respTime < 495) {
|
||||||
|
return "stat-middle";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "stat-bad";
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UptimeComparisonCard(props: {uptime: UptimeRecord, isService: boolean}) {
|
||||||
|
const [isModalOpen, setModalOpen] = useState(false);
|
||||||
|
return(
|
||||||
|
<div className={"col-12 col-lg-2 mb-2 p-lg-0 m-lg-0 m-2 text-center nws-card-md"}>
|
||||||
|
<div style={{height: 25, margin: 0}} className={"pt-2 pt-lg-0"}>
|
||||||
|
<h4 className={"uptime-lnk"} onClick={()=>setModalOpen(true)}>{props.uptime.name}</h4>
|
||||||
|
</div>
|
||||||
|
<hr className={" w-100"}/>
|
||||||
|
<p className={"fw-bold d-lg-none"}>Uptime (Last Month)</p>
|
||||||
|
<div style={{height: 25, margin: 0}} className={"pb-2 pt-lg-0"}>
|
||||||
|
<p className={getUptimeClass(props.uptime.uptimeMonth)}>{props.uptime.uptimeMonth}%</p>
|
||||||
|
</div>
|
||||||
|
<hr className={"d-lg-block d-none w-100"}/>
|
||||||
|
<p className={"fw-bold d-lg-none"}>Uptime ({new Date().getFullYear()} YTD)</p>
|
||||||
|
<div style={{height: 25, margin: 0}} className={"pb-2 pt-lg-0"}>
|
||||||
|
<p className={getUptimeClass(props.uptime.uptimeYtd)}>{props.uptime.uptimeYtd}%</p>
|
||||||
|
</div>
|
||||||
|
<hr className={"d-lg-block d-none w-100"}/>
|
||||||
|
<p className={"fw-bold d-lg-none"}>Avg Response Time (24hr)</p>
|
||||||
|
<div style={{height: 25, margin: 0}} className={"pb-2 pt-lg-0"}>
|
||||||
|
<p className={getResponseTimeClass(props.uptime.averageResponseTime)}>{props.uptime.averageResponseTime}ms</p>
|
||||||
|
</div>
|
||||||
|
<hr className={"d-lg-block d-none w-100"} />
|
||||||
|
<p className={"fw-bold d-lg-none"}>Current Status</p>
|
||||||
|
<div style={{height: 25, margin: 0}} className={"pb-2 pt-lg-0"}>
|
||||||
|
|
||||||
|
<div className={`p-1 d-flex justify-content-start w-100`} >
|
||||||
|
<p className={`fw-bold severity-label w-100
|
||||||
|
${props.uptime.isUp ? 'low' : (props.uptime.undergoingMaintenance ? 'med' : 'high')}-severity`}
|
||||||
|
>
|
||||||
|
{props.uptime.isUp ? 'Up' : (props.uptime.undergoingMaintenance ? 'Maintenance' : 'Down')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<Modal className={"uptime-modal"} isOpen={isModalOpen}>
|
||||||
|
<div className={"mb-3"}>
|
||||||
|
<h1 className={"mb-0"}>{props.uptime.name}</h1>
|
||||||
|
{props.uptime.url && <p>(<a href={props.uptime.url}>{props.uptime.url}</a>)</p>}
|
||||||
|
</div>
|
||||||
|
<div className={"mb-3"}>
|
||||||
|
<p>Monitoring since {props.uptime.monitorStart}</p>
|
||||||
|
<p><b>{new Date().getFullYear()} Uptime (YTD):</b> {props.uptime.uptimeYtd}%</p>
|
||||||
|
<p><b>Last Month Uptime:</b> {props.uptime.uptimeMonth}%</p>
|
||||||
|
<p><b>All-Time Uptime:</b> {props.uptime.uptimeAllTime}%</p>
|
||||||
|
<p><b>Average Response Time:</b> {props.uptime.averageResponseTime}ms</p>
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
props.isService &&
|
||||||
|
<div className={"mb-3"}>
|
||||||
|
<i>Note that the uptime and performance of services hosted on NWS may be affected by factors not controlled by NWS such as bad bad optimization or buggy software.</i>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<button className={"w-100"} onClick={()=>setModalOpen(false)}>Close</button>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
35
src/components/UptimeLabelCard.tsx
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import {UptimeRecord} from "../nws-api/types";
|
||||||
|
import React, {useState} from "react";
|
||||||
|
import '../App.css';
|
||||||
|
import "./UptimeCard.css"
|
||||||
|
import Modal from "react-modal";
|
||||||
|
|
||||||
|
export default function UptimeLabelCard() {
|
||||||
|
|
||||||
|
return(
|
||||||
|
<div className={"col-2 p-0 d-none d-lg-block mb-2 m-0 text-center"}>
|
||||||
|
<div style={{height: 25, margin: 0}}>
|
||||||
|
<p className={"fw-bold"} style={{fontSize: ".9em"}}>Service Name</p>
|
||||||
|
</div>
|
||||||
|
<hr className={"w-100"}/>
|
||||||
|
<div style={{height: 25, margin: 0}}>
|
||||||
|
<p className={"fw-bold"} style={{fontSize: ".9em"}}>Uptime (Last Month)</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr className={"w-100"}/>
|
||||||
|
<div style={{height: 25, margin: 0}}>
|
||||||
|
<p className={"fw-bold"} style={{fontSize: ".9em"}}>Uptime ({new Date().getFullYear()} YTD)</p>
|
||||||
|
</div>
|
||||||
|
<hr className={"w-100"}/>
|
||||||
|
<div style={{height: 25, margin: 0}}>
|
||||||
|
<p className={"fw-bold"} style={{fontSize: ".9em"}}>Avg Response Time (24hr)</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr className={"w-100"}/>
|
||||||
|
<div style={{height: 25, margin: 0}}>
|
||||||
|
<p className={"fw-bold"} style={{fontSize: ".9em"}}>Current Status</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
32
src/components/VerifyPage.css
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
.verify-box {
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 1px;
|
||||||
|
border-color: #aaa;
|
||||||
|
border-radius: 10px;
|
||||||
|
|
||||||
|
align-self: center;
|
||||||
|
justify-self: center;
|
||||||
|
|
||||||
|
width: 500px;
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verify-label {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verify-button {
|
||||||
|
border-radius: 10px;
|
||||||
|
border-width: 0px;
|
||||||
|
|
||||||
|
background-color: cornflowerblue;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 3px
|
||||||
|
}
|
67
src/components/VerifyPage.tsx
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import "./RegisterPage.css";
|
||||||
|
import {useEffect, useState} from "react";
|
||||||
|
import {Account, SessionKey} from "../nws-api/types";
|
||||||
|
import {useSearchParams} from "react-router-dom";
|
||||||
|
import {Session} from "inspector";
|
||||||
|
|
||||||
|
export default function VerifyPage() {
|
||||||
|
const [pageState, setPageState] = useState<string>("");
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
|
useEffect(()=>{
|
||||||
|
let verificationKey: string | null = searchParams.get("key");
|
||||||
|
|
||||||
|
if(verificationKey == null) {
|
||||||
|
setPageState("invalid_code");
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
fetch("https://api-nws.nickorlow.com/Account/verify?verificationKey=" + verificationKey, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.status == 200) {
|
||||||
|
result.json().then((o: SessionKey)=>{
|
||||||
|
localStorage.setItem("session_key", JSON.stringify(o));
|
||||||
|
window.location.href = '/dashboard';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.status == 500) {
|
||||||
|
setPageState("server_error");
|
||||||
|
} else {
|
||||||
|
result.json().then((o) => {
|
||||||
|
if (o.ErrorMessage == "Invalid verification key.") {
|
||||||
|
setPageState("invalid_code");
|
||||||
|
} else {
|
||||||
|
setPageState("expired_code");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).catch((e) => {
|
||||||
|
setPageState("invalid_code");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return(
|
||||||
|
<div style={{minHeight: "100vh", display: "grid", width: "100%"}}>
|
||||||
|
<div className={"reg-box"} style={{display: pageState == "invalid_code" ? "flex" : "none"}}>
|
||||||
|
<h3>Uh Oh!</h3>
|
||||||
|
|
||||||
|
<p>Looks like the verification code you provided didn't work!</p>
|
||||||
|
|
||||||
|
<p className={"mt-2"}>Try to click on the link in the E-Mail sent to you instead of copying it.</p>
|
||||||
|
</div>
|
||||||
|
<div className={"reg-box"} style={{display: pageState == "expired_code" ? "flex" : "none"}}>
|
||||||
|
<h3>Expired Link</h3>
|
||||||
|
|
||||||
|
<p>It looks like the link you used to verify your account has expired.</p>
|
||||||
|
|
||||||
|
<p className={"mt-2"}>We've sent a new link to your email that is valid for 30 minutes.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
26
src/index.css
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-lnk {
|
||||||
|
font-weight: bold;
|
||||||
|
color: black;
|
||||||
|
text-decoration: none;
|
||||||
|
padding-left: 30px;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
transition: .5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-lnk:hover {
|
||||||
|
color: #F7BA00;
|
||||||
|
}
|
175
src/index.tsx
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import * as ReactDOM from "react-dom";
|
||||||
|
import {
|
||||||
|
createBrowserRouter, NavLink,
|
||||||
|
RouterProvider,
|
||||||
|
} from "react-router-dom";
|
||||||
|
import StatusPage from "./components/StatusPage";
|
||||||
|
import reportWebVitals from "./reportWebVitals";
|
||||||
|
import "./index.css";
|
||||||
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
|
import UptimeCard from "./components/UptimeCard";
|
||||||
|
import Footer from "./components/Footer";
|
||||||
|
import {Nav, Navbar, NavbarBrand, NavDropdown} from "react-bootstrap";
|
||||||
|
import NWSLogo from "./static/images/NWS_Logo_Transparent.png";
|
||||||
|
import Blogs from "./components/Blogs";
|
||||||
|
import NotFoundPage from "./components/NotFoundPage";
|
||||||
|
import LoginPage from "./components/LoginPage";
|
||||||
|
import RegisterPage from "./components/RegisterPage";
|
||||||
|
import VerifyPage from "./components/VerifyPage";
|
||||||
|
import DashboardPage from "./components/DashboardPage";
|
||||||
|
import CreateCruisePage from "./components/CreateCruisePage";
|
||||||
|
import HomePage from "./components/HomePage";
|
||||||
|
import NavbarCollapse from "react-bootstrap/NavbarCollapse";
|
||||||
|
import NavbarOffcanvas from "react-bootstrap/NavbarOffcanvas";
|
||||||
|
|
||||||
|
function Layout (props: {children: any}) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<header className={"w-100 sticky-top"}>
|
||||||
|
<div className={"w-100"}>
|
||||||
|
<Navbar sticky={"top"} expand="md" className={"row justify-content-center m-0 p-0"} style={{backgroundColor: "#eee"}}>
|
||||||
|
<div className={"row w-100"}>
|
||||||
|
<div className="row w-100 d-md-none d-sm-block">
|
||||||
|
<div className={"col-9"}>
|
||||||
|
<Navbar.Brand href="/">
|
||||||
|
<img src={NWSLogo} width={150}/>
|
||||||
|
</Navbar.Brand>
|
||||||
|
</div>
|
||||||
|
<div className={"col-2 ml-3 d-flex align-content-center justify-content-center"}>
|
||||||
|
<Navbar.Toggle className={"h-50 align-self-center"} aria-controls="basic-navbar-nav"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={"d-md-block d-none col-2"}>
|
||||||
|
<Navbar.Brand href="/">
|
||||||
|
<img src={NWSLogo} width={150}/>
|
||||||
|
</Navbar.Brand>
|
||||||
|
</div>
|
||||||
|
<Navbar.Collapse id="basic-navbar-nav" className={"col-10"}>
|
||||||
|
<Nav className="row w-100 ml-5">
|
||||||
|
<div className="col-md-4 row">
|
||||||
|
<NavLink className="col-sm-12 col-md-3 nav-lnk align-self-center" to={"/"}>Home</NavLink>
|
||||||
|
<NavLink className="col-sm-12 col-md-3 nav-lnk align-self-center" to={"/status"}>Status</NavLink>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-6"/>
|
||||||
|
<div className={"col-md-2 d-md-block d-none"}>
|
||||||
|
{ localStorage.getItem("session_key") === null &&
|
||||||
|
(
|
||||||
|
<NavLink className={"nav-lnk"} to={"/login"}>
|
||||||
|
Login
|
||||||
|
</NavLink>
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{ localStorage.getItem("session_key") === null ||
|
||||||
|
(
|
||||||
|
<NavDropdown title={"Account"} className={"nav-lnk"}>
|
||||||
|
<NavLink className={"nav-lnk"} to={"/dashboard"}>
|
||||||
|
Dashboard
|
||||||
|
</NavLink>
|
||||||
|
<hr/>
|
||||||
|
<NavLink className={"nav-lnk"} to={"/login"} onClick={()=>{localStorage.removeItem("session_key")}}>
|
||||||
|
Logout
|
||||||
|
</NavLink>
|
||||||
|
</NavDropdown>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</Nav>
|
||||||
|
</Navbar.Collapse>
|
||||||
|
</div>
|
||||||
|
</Navbar>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div style={{minHeight: "92vh"}}>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
<Footer/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const router = createBrowserRouter([
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
element:
|
||||||
|
<Layout>
|
||||||
|
<HomePage/>
|
||||||
|
</Layout>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "status",
|
||||||
|
element:
|
||||||
|
<Layout>
|
||||||
|
<StatusPage/>
|
||||||
|
</Layout>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "blog",
|
||||||
|
element:
|
||||||
|
<Layout>
|
||||||
|
<Blogs/>
|
||||||
|
</Layout>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "blogs",
|
||||||
|
element:
|
||||||
|
<Layout>
|
||||||
|
<Blogs/>
|
||||||
|
</Layout>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "login",
|
||||||
|
element:
|
||||||
|
<Layout>
|
||||||
|
<LoginPage/>
|
||||||
|
</Layout>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "verify",
|
||||||
|
element:
|
||||||
|
<Layout>
|
||||||
|
<VerifyPage/>
|
||||||
|
</Layout>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "dashboard",
|
||||||
|
element:
|
||||||
|
<Layout>
|
||||||
|
<DashboardPage/>
|
||||||
|
</Layout>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "register",
|
||||||
|
element:
|
||||||
|
<Layout>
|
||||||
|
<RegisterPage/>
|
||||||
|
</Layout>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "cruise/new",
|
||||||
|
element:
|
||||||
|
<Layout>
|
||||||
|
<CreateCruisePage/>
|
||||||
|
</Layout>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "*",
|
||||||
|
element:
|
||||||
|
<Layout>
|
||||||
|
<NotFoundPage/>
|
||||||
|
</Layout>
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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();
|
1
src/logo.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
After Width: | Height: | Size: 2.6 KiB |
272
src/main.rs
|
@ -1,272 +0,0 @@
|
||||||
use axum::{routing::get, Router};
|
|
||||||
use chrono::offset::Utc;
|
|
||||||
use chrono::DateTime;
|
|
||||||
use log::*;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
#[macro_use]
|
|
||||||
extern crate dotenv_codegen;
|
|
||||||
extern crate dotenv;
|
|
||||||
use dotenv::dotenv;
|
|
||||||
use std::env;
|
|
||||||
|
|
||||||
use lazy_static::lazy_static;
|
|
||||||
|
|
||||||
mod uptime_service;
|
|
||||||
|
|
||||||
use uptime_service::{Uptime, UptimeService, UptimeStatus, UptimeType};
|
|
||||||
|
|
||||||
#[derive(askama::Template)]
|
|
||||||
#[template(path = "layout.html")]
|
|
||||||
struct ContentTemplate<T: askama::Template> {
|
|
||||||
content: T,
|
|
||||||
page_title: Option<String>,
|
|
||||||
page_desc: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(askama::Template)]
|
|
||||||
#[template(path = "layout.html")]
|
|
||||||
struct RawContentTemplate {
|
|
||||||
content: String,
|
|
||||||
page_title: Option<String>,
|
|
||||||
page_desc: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct UptimeInfo {
|
|
||||||
name: String,
|
|
||||||
uptime: String,
|
|
||||||
response_time: String,
|
|
||||||
status: String,
|
|
||||||
url: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(askama::Template)]
|
|
||||||
#[template(path = "index.html")]
|
|
||||||
struct IndexTemplate {
|
|
||||||
uptime_infos: Vec<UptimeInfo>,
|
|
||||||
last_updated: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(askama::Template)]
|
|
||||||
#[template(path = "system_status.html")]
|
|
||||||
struct StatusTemplate {
|
|
||||||
dctr_uptime_infos: Vec<UptimeInfo>,
|
|
||||||
svc_uptime_infos: Vec<UptimeInfo>,
|
|
||||||
last_updated: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct BlogInfo<'a> {
|
|
||||||
title: &'a str,
|
|
||||||
date: &'a str,
|
|
||||||
url: &'a str,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(askama::Template)]
|
|
||||||
#[template(path = "blog.html")]
|
|
||||||
struct BlogTemplate<'a> {
|
|
||||||
blogs: Vec<BlogInfo<'a>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(askama::Template)]
|
|
||||||
#[template(path = "dashboard.html")]
|
|
||||||
struct DashboardTemplate {}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct AppState {
|
|
||||||
uptime_service: UptimeService,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
dotenv().ok();
|
|
||||||
env_logger::init();
|
|
||||||
|
|
||||||
info!("Starting Sharpe Mountain Compute Website");
|
|
||||||
|
|
||||||
let uptime_service: UptimeService = UptimeService::new();
|
|
||||||
uptime_service.start();
|
|
||||||
|
|
||||||
let state = AppState { uptime_service };
|
|
||||||
|
|
||||||
let app = Router::new()
|
|
||||||
.route("/", get(index_handler))
|
|
||||||
.route("/system_status", get(status_handler))
|
|
||||||
.route("/dashboard", get(dashboard_handler))
|
|
||||||
.route("/blog", get(blog_handler))
|
|
||||||
.route("/blogs/:blog_name", get(single_blog_handler))
|
|
||||||
.nest_service("/assets", tower_http::services::ServeDir::new("assets"))
|
|
||||||
.with_state(state);
|
|
||||||
|
|
||||||
let port_num = env::var("EXPOSE_PORT").unwrap_or("3000".to_string());
|
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", port_num))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
info!("Listening on port {}", port_num);
|
|
||||||
|
|
||||||
axum::serve(listener, app).await.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
static ref blogs: HashMap<&'static str, BlogInfo<'static>> = {
|
|
||||||
let mut m = HashMap::new();
|
|
||||||
m.insert(
|
|
||||||
"11-28-2024-onward-postmortem",
|
|
||||||
BlogInfo {
|
|
||||||
title: "Downtime Incident Postmortem (Nov 2024 - Present)",
|
|
||||||
date: "December 28th, 2024",
|
|
||||||
url: "11-28-2023-postmortem",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
m.insert(
|
|
||||||
"11-08-2023-postmortem",
|
|
||||||
BlogInfo {
|
|
||||||
title: "Downtime Incident Postmortem (Nov 2023)",
|
|
||||||
date: "November 11th, 2023",
|
|
||||||
url: "11-08-2023-postmortem",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
m.insert(
|
|
||||||
"ssl-on-cds",
|
|
||||||
BlogInfo {
|
|
||||||
title: "SSL on Container Deployment Service (at nickorlow.com)",
|
|
||||||
date: "July 12th, 2023",
|
|
||||||
url: "https://nickorlow.com/blogs/side-project-7-12-23.html",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
m
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn blog_handler(
|
|
||||||
) -> Result<ContentTemplate<impl askama::Template>, (axum::http::StatusCode, String)> {
|
|
||||||
Ok(ContentTemplate {
|
|
||||||
page_title: Some("NWS | Blog".to_string()),
|
|
||||||
page_desc: Some("Read about the engineering behind NWS.".to_string()),
|
|
||||||
content: BlogTemplate {
|
|
||||||
blogs: blogs.values().cloned().collect::<Vec<BlogInfo>>()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn single_blog_handler(
|
|
||||||
axum::extract::Path((blog_name)): axum::extract::Path<(String)>,
|
|
||||||
) -> Result<RawContentTemplate, (axum::http::StatusCode, String)> {
|
|
||||||
let blog_content = match std::fs::read_to_string(format!("templates/blogs/{}.html", blog_name))
|
|
||||||
{
|
|
||||||
Ok(ctn) => ctn,
|
|
||||||
_ => String::from("<h1>Not Found!</h1>"),
|
|
||||||
};
|
|
||||||
Ok(RawContentTemplate {
|
|
||||||
page_title: Some("NWS | Blog Post".to_string()),
|
|
||||||
page_desc: Some("A Nick Web Services Blog Post.".to_string()),
|
|
||||||
content: blog_content,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn dashboard_handler(
|
|
||||||
) -> Result<ContentTemplate<impl askama::Template>, (axum::http::StatusCode, String)> {
|
|
||||||
Ok(ContentTemplate {
|
|
||||||
page_title: Some("NWS | Dashboard".to_string()),
|
|
||||||
page_desc: Some("Manage the services you have deployed on NWS.".to_string()),
|
|
||||||
content: DashboardTemplate {},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn index_handler(
|
|
||||||
axum::extract::State(state): axum::extract::State<AppState>,
|
|
||||||
) -> Result<ContentTemplate<impl askama::Template>, (axum::http::StatusCode, String)> {
|
|
||||||
let uptimes: Vec<Uptime> = state.uptime_service.get_data();
|
|
||||||
let lu: DateTime<Utc> = state.uptime_service.get_last_updated().into();
|
|
||||||
let lu_str = format!("{} UTC", lu.format("%B %e, %Y %T"));
|
|
||||||
|
|
||||||
let mut uptime_infos: Vec<UptimeInfo> = vec![];
|
|
||||||
|
|
||||||
for uptime in uptimes {
|
|
||||||
if uptime.uptime_type != UptimeType::Provider {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
uptime_infos.push(UptimeInfo {
|
|
||||||
name: uptime.name,
|
|
||||||
uptime: uptime.uptime,
|
|
||||||
response_time: uptime.response_time,
|
|
||||||
status: match uptime.status {
|
|
||||||
UptimeStatus::Up => String::from("Up"),
|
|
||||||
UptimeStatus::Down => String::from("DOWN"),
|
|
||||||
UptimeStatus::Maintenance => String::from("Undergoing Maintenance"),
|
|
||||||
_ => String::from("Unknown"),
|
|
||||||
},
|
|
||||||
url: None,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let index_template = IndexTemplate {
|
|
||||||
uptime_infos,
|
|
||||||
last_updated: lu_str,
|
|
||||||
};
|
|
||||||
Ok(ContentTemplate {
|
|
||||||
page_title: None,
|
|
||||||
page_desc: None,
|
|
||||||
content: index_template,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn status_handler(
|
|
||||||
axum::extract::State(state): axum::extract::State<AppState>,
|
|
||||||
) -> Result<ContentTemplate<impl askama::Template>, (axum::http::StatusCode, String)> {
|
|
||||||
let uptimes: Vec<Uptime> = state.uptime_service.get_data();
|
|
||||||
let lu: DateTime<Utc> = state.uptime_service.get_last_updated().into();
|
|
||||||
let lu_str = format!("{} UTC", lu.format("%B %e, %Y %T"));
|
|
||||||
|
|
||||||
let mut dc_uptime_infos: Vec<UptimeInfo> = vec![];
|
|
||||||
let mut sv_uptime_infos: Vec<UptimeInfo> = vec![];
|
|
||||||
|
|
||||||
for uptime in uptimes {
|
|
||||||
match uptime.uptime_type {
|
|
||||||
UptimeType::Datacenter => {
|
|
||||||
dc_uptime_infos.push(UptimeInfo {
|
|
||||||
name: uptime.name,
|
|
||||||
uptime: uptime.uptime,
|
|
||||||
response_time: uptime.response_time,
|
|
||||||
status: match uptime.status {
|
|
||||||
UptimeStatus::Up => String::from("Up"),
|
|
||||||
UptimeStatus::Down => String::from("DOWN"),
|
|
||||||
UptimeStatus::Maintenance => String::from("Undergoing Maintenance"),
|
|
||||||
_ => String::from("Unknown"),
|
|
||||||
},
|
|
||||||
url: None,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
UptimeType::Service => {
|
|
||||||
sv_uptime_infos.push(UptimeInfo {
|
|
||||||
name: uptime.name,
|
|
||||||
uptime: uptime.uptime,
|
|
||||||
response_time: uptime.response_time,
|
|
||||||
status: match uptime.status {
|
|
||||||
UptimeStatus::Up => String::from("Up"),
|
|
||||||
UptimeStatus::Down => String::from("DOWN"),
|
|
||||||
UptimeStatus::Maintenance => String::from("Undergoing Maintenance"),
|
|
||||||
_ => String::from("Unknown"),
|
|
||||||
},
|
|
||||||
url: Some(uptime.url),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
_ => continue,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let service_template = StatusTemplate {
|
|
||||||
dctr_uptime_infos: dc_uptime_infos,
|
|
||||||
svc_uptime_infos: sv_uptime_infos,
|
|
||||||
last_updated: lu_str,
|
|
||||||
};
|
|
||||||
Ok(ContentTemplate {
|
|
||||||
page_title: Some("NWS | System Status".to_string()),
|
|
||||||
page_desc: Some("Check the health of NWS datacenters and services hosted on NWS.".to_string()),
|
|
||||||
content: service_template,
|
|
||||||
})
|
|
||||||
}
|
|
76
src/nws-api/calls.ts
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
import {Blog, Incident, Namespace, Service, SessionKey, UptimeResponse} from "./types";
|
||||||
|
|
||||||
|
export async function getUptime(): Promise<UptimeResponse> {
|
||||||
|
let response: Response = await fetch('https://api-nws.nickorlow.com/uptime');
|
||||||
|
let uptime: UptimeResponse = await response.json();
|
||||||
|
return uptime;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getIncidents(): Promise<Incident[]> {
|
||||||
|
let response: Response = await fetch('https://api-nws.nickorlow.com/incidents');
|
||||||
|
try {
|
||||||
|
let incidents: Incident[] = await response.json();
|
||||||
|
if(incidents === null || incidents === undefined || !Array.isArray(incidents)) return [];
|
||||||
|
return incidents;
|
||||||
|
} catch (e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getBlogs(): Promise<Blog[]> {
|
||||||
|
let response: Response = await fetch('https://api-nws.nickorlow.com/blogs');
|
||||||
|
let blogs: Blog[] = await response.json();
|
||||||
|
return blogs;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSessionKey(accountId: string, password: string): Promise<SessionKey> {
|
||||||
|
let response: Response = await fetch('https://api-nws.nickorlow.com/Account/session',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
'id': accountId,
|
||||||
|
'password': password
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
let sessionKey: SessionKey = await response.json();
|
||||||
|
return sessionKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getNamespaces(accountId: string, skey: SessionKey): Promise<Namespace[]> {
|
||||||
|
let response: Response = await fetch('https://api-nws.nickorlow.com/'+accountId+'/namespaces', {
|
||||||
|
headers: {
|
||||||
|
Authorization: skey.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let namespaces: Namespace[] = await response.json();
|
||||||
|
return namespaces;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createNamespace(name: string, accountId: string, session: SessionKey): Promise<Namespace> {
|
||||||
|
let response: Response = await fetch('https://api-nws.nickorlow.com/namespaces', {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: btoa(session.accountId + ":" + session.id)
|
||||||
|
},
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
'name': name,
|
||||||
|
'ownerId': accountId
|
||||||
|
})
|
||||||
|
});
|
||||||
|
let namespace: Namespace = await response.json();
|
||||||
|
return namespace;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function enableSSL(accountId: string, serviceId: string, hostname: string, session: SessionKey) {
|
||||||
|
await fetch('https://api-nws.nickorlow.com/'+accountId+'/service/'+serviceId+"/hosts/"+hostname+"/ssl", {
|
||||||
|
headers: {
|
||||||
|
Authorization: btoa(session.accountId + ":" + session.id)
|
||||||
|
},
|
||||||
|
method: "POST"
|
||||||
|
});
|
||||||
|
}
|
156
src/nws-api/hooks.ts
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
import {useEffect, useState} from "react";
|
||||||
|
import {Account, Namespace, Service, SessionKey} from "./types";
|
||||||
|
|
||||||
|
export function useNonLoggedInRedirect() {
|
||||||
|
useEffect(()=>{
|
||||||
|
let rawSession: string | null = localStorage.getItem("session_key");
|
||||||
|
|
||||||
|
if(rawSession != null) {
|
||||||
|
let session: SessionKey = JSON.parse(rawSession);
|
||||||
|
if(session.expiry < new Date()) {
|
||||||
|
localStorage.removeItem("session_key");
|
||||||
|
} else {
|
||||||
|
window.location.href = "/dashboard";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useLoggedInRedirect() {
|
||||||
|
useEffect(()=>{
|
||||||
|
let rawSession: string | null = localStorage.getItem("session_key");
|
||||||
|
|
||||||
|
if(rawSession != null) {
|
||||||
|
let session: SessionKey = JSON.parse(rawSession);
|
||||||
|
if(session.expiry > new Date()) {
|
||||||
|
window.location.href = "/login";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
window.location.href = "/login";
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useGetAccountServices() {
|
||||||
|
const [services, setService] = useState<Service[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let rawSession: string | null = localStorage.getItem("session_key");
|
||||||
|
|
||||||
|
if(rawSession != null) {
|
||||||
|
let session: SessionKey = JSON.parse(rawSession);
|
||||||
|
fetch("https://api-nws.nickorlow.com/Account/services?accountId=" + session.accountId,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Authorization": btoa(session.accountId + ":" + session.id)
|
||||||
|
}
|
||||||
|
}).then((response)=>{
|
||||||
|
response.json().then((svcs: Service[]) => {
|
||||||
|
console.log(svcs)
|
||||||
|
setService(svcs);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useGetAccountNamespaces() {
|
||||||
|
const [namespaces, setNamespaces] = useState<Namespace[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let rawSession: string | null = localStorage.getItem("session_key");
|
||||||
|
|
||||||
|
if(rawSession != null) {
|
||||||
|
let session: SessionKey = JSON.parse(rawSession);
|
||||||
|
fetch("https://api-nws.nickorlow.com/Account/" + session.accountId + "/namespaces",
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Authorization": btoa(session.accountId + ":" + session.id)
|
||||||
|
}
|
||||||
|
}).then((response)=>{
|
||||||
|
response.json().then((svcs: Namespace[]) => {
|
||||||
|
console.log(svcs)
|
||||||
|
setNamespaces(svcs);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return namespaces;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useNWSAuthKey() {
|
||||||
|
const [key, setKey] = useState('');
|
||||||
|
useEffect(() => {
|
||||||
|
let rawSession: string | null = localStorage.getItem("session_key");
|
||||||
|
|
||||||
|
if(rawSession != null) {
|
||||||
|
let session: SessionKey = JSON.parse(rawSession);
|
||||||
|
setKey(btoa(session.accountId + ":" + session.id))
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useGetServicesInNamespace() {
|
||||||
|
const [services, setServices] = useState<Service[]>([]);
|
||||||
|
const [ns, setNs] = useState<Namespace | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(ns !== null ? ns.id : "null")
|
||||||
|
if(ns === null) return;
|
||||||
|
let rawSession: string | null = localStorage.getItem("session_key");
|
||||||
|
|
||||||
|
if(rawSession != null) {
|
||||||
|
let session: SessionKey = JSON.parse(rawSession);
|
||||||
|
fetch("https://api-nws.nickorlow.com/Account/" + session.accountId + "/namespaces/" + ns.id + "/services",
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Authorization": btoa(session.accountId + ":" + session.id)
|
||||||
|
}
|
||||||
|
}).then((response)=>{
|
||||||
|
response.json().then((svcs: Service[]) => {
|
||||||
|
console.log(svcs)
|
||||||
|
setServices(svcs);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [ns]);
|
||||||
|
|
||||||
|
return {setNs, services, ns};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useNWSAccount() {
|
||||||
|
const [accountInfo, setAccountInfo] = useState<Account>();
|
||||||
|
|
||||||
|
useEffect(()=>{
|
||||||
|
let rawSession: string | null = localStorage.getItem("session_key");
|
||||||
|
|
||||||
|
if(rawSession != null) {
|
||||||
|
let session: SessionKey = JSON.parse(rawSession);
|
||||||
|
fetch("https://api-nws.nickorlow.com/Account/"+session.accountId, {
|
||||||
|
headers: {
|
||||||
|
"Authorization": btoa(session.accountId+":"+session.id)
|
||||||
|
}
|
||||||
|
}).then((e)=>{
|
||||||
|
if(e.status == 200) {
|
||||||
|
e.json().then((o: Account) => {
|
||||||
|
setAccountInfo(o)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem("session_key");
|
||||||
|
window.location.href = "/login";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem("session_key");
|
||||||
|
window.location.href = "/login";
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return accountInfo;
|
||||||
|
}
|
82
src/nws-api/types.ts
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
export type UptimeRecord = {
|
||||||
|
name: string,
|
||||||
|
url: string,
|
||||||
|
uptimeMonth: number,
|
||||||
|
uptimeAllTime: number,
|
||||||
|
uptimeYtd: number,
|
||||||
|
averageResponseTime: number,
|
||||||
|
monitorStart: string,
|
||||||
|
isUp: boolean,
|
||||||
|
undergoingMaintenance: boolean
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UptimeResponse = {
|
||||||
|
datacenters: UptimeRecord[],
|
||||||
|
services: UptimeRecord[],
|
||||||
|
competitors: UptimeRecord[],
|
||||||
|
lastUpdated: string
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Blog = {
|
||||||
|
id: number,
|
||||||
|
title: string,
|
||||||
|
author: string,
|
||||||
|
content: string,
|
||||||
|
imageUrl: string
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Incident = {
|
||||||
|
id: number,
|
||||||
|
severity: IncidentSeverity,
|
||||||
|
title: string,
|
||||||
|
description: string
|
||||||
|
};
|
||||||
|
|
||||||
|
enum IncidentSeverity {
|
||||||
|
LOW,
|
||||||
|
MEDIUM,
|
||||||
|
HIGH
|
||||||
|
};
|
||||||
|
|
||||||
|
// Below is primarily for user-facing things
|
||||||
|
|
||||||
|
export type Account = {
|
||||||
|
id?: string,
|
||||||
|
email: string,
|
||||||
|
name?: string,
|
||||||
|
password?: string,
|
||||||
|
status?: string
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Host = {
|
||||||
|
serviceId: string,
|
||||||
|
isSslEnabled: boolean,
|
||||||
|
hostname: string
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Service = {
|
||||||
|
serviceId: string,
|
||||||
|
serviceName: string,
|
||||||
|
namespace: string,
|
||||||
|
containerUrl: string,
|
||||||
|
ownerId: string,
|
||||||
|
hostnames: Host[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ApiError = {
|
||||||
|
StatusCode: number,
|
||||||
|
ErrorMessage: string
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SessionKey = {
|
||||||
|
id: string,
|
||||||
|
expiry: Date,
|
||||||
|
accountId: string,
|
||||||
|
ip: string
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Namespace = {
|
||||||
|
id: string,
|
||||||
|
accountId: string,
|
||||||
|
name: string
|
||||||
|
}
|
1
src/react-app-env.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/// <reference types="react-scripts" />
|
15
src/reportWebVitals.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { ReportHandler } from 'web-vitals';
|
||||||
|
|
||||||
|
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
||||||
|
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||||
|
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||||
|
getCLS(onPerfEntry);
|
||||||
|
getFID(onPerfEntry);
|
||||||
|
getFCP(onPerfEntry);
|
||||||
|
getLCP(onPerfEntry);
|
||||||
|
getTTFB(onPerfEntry);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default reportWebVitals;
|
5
src/setupTests.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||||
|
// allows you to do things like:
|
||||||
|
// expect(element).toHaveTextContent(/react/i)
|
||||||
|
// learn more: https://github.com/testing-library/jest-dom
|
||||||
|
import '@testing-library/jest-dom';
|
BIN
src/static/images/NWS_Logo.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
src/static/images/NWS_Logo_Transparent.png
Normal file
After Width: | Height: | Size: 24 KiB |
|
@ -1,250 +0,0 @@
|
||||||
use anyhow::anyhow;
|
|
||||||
use anyhow::Context;
|
|
||||||
use chrono::{Datelike, NaiveDate};
|
|
||||||
use log::*;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
use std::time::Duration;
|
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
|
||||||
use tokio::time::sleep;
|
|
||||||
|
|
||||||
#[macro_use]
|
|
||||||
use dotenv::dotenv;
|
|
||||||
use std::env;
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone)]
|
|
||||||
pub enum UptimeType {
|
|
||||||
Provider,
|
|
||||||
Service,
|
|
||||||
Datacenter,
|
|
||||||
Unknown,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone)]
|
|
||||||
pub enum UptimeStatus {
|
|
||||||
Up,
|
|
||||||
Down,
|
|
||||||
Maintenance,
|
|
||||||
Unknown,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Uptime {
|
|
||||||
pub name: String,
|
|
||||||
pub uptime: String,
|
|
||||||
pub response_time: String,
|
|
||||||
pub status: UptimeStatus,
|
|
||||||
pub uptime_type: UptimeType,
|
|
||||||
pub url: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct UptimeServiceState {
|
|
||||||
uptimes: Vec<Uptime>,
|
|
||||||
last_updated: SystemTime,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct UptimeService {
|
|
||||||
state: Arc<Mutex<UptimeServiceState>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UptimeService {
|
|
||||||
const UPDATE_SECONDS: u64 = 300;
|
|
||||||
|
|
||||||
pub fn new() -> Self {
|
|
||||||
let init_state = Arc::new(Mutex::new(UptimeServiceState {
|
|
||||||
uptimes: vec![],
|
|
||||||
last_updated: UNIX_EPOCH,
|
|
||||||
}));
|
|
||||||
Self { state: init_state }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn start(&self) {
|
|
||||||
info!("Starting UptimeService");
|
|
||||||
let cloned_state = Arc::clone(&self.state);
|
|
||||||
tokio::spawn(async move {
|
|
||||||
loop {
|
|
||||||
let clonedx_state = Arc::clone(&cloned_state);
|
|
||||||
let res = Self::update_data(clonedx_state).await;
|
|
||||||
match res {
|
|
||||||
Err(err) => {
|
|
||||||
error!("{}", err);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
sleep(tokio::time::Duration::from_secs(Self::UPDATE_SECONDS)).await;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_data(&self) -> Vec<Uptime> {
|
|
||||||
let state = self.state.lock().unwrap();
|
|
||||||
let uptimes = state.uptimes.clone();
|
|
||||||
return uptimes;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_last_updated(&self) -> SystemTime {
|
|
||||||
let state = self.state.lock().unwrap();
|
|
||||||
let lu = state.last_updated.clone();
|
|
||||||
return lu;
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn update_data(arc_state: Arc<Mutex<UptimeServiceState>>) -> ::anyhow::Result<()> {
|
|
||||||
debug!("Starting data update for UptimeService");
|
|
||||||
|
|
||||||
let mut request_vars = HashMap::new();
|
|
||||||
let api_key = env::var("UPTIMEROBOT_API_KEY")?;
|
|
||||||
request_vars.insert("api_key", api_key.as_str());
|
|
||||||
request_vars.insert("all_time_uptime_ratio", "1");
|
|
||||||
let now = SystemTime::now();
|
|
||||||
//let thirty_days_ago = now - Duration::from_secs(30 * 24 * 3600);
|
|
||||||
|
|
||||||
let current_year = chrono::Utc::today().year();
|
|
||||||
let january_1st = NaiveDate::from_ymd(current_year, 1, 1).and_hms(0, 0, 0);
|
|
||||||
let duration =
|
|
||||||
january_1st.signed_duration_since(NaiveDate::from_ymd(1970, 1, 1).and_hms(0, 0, 0));
|
|
||||||
let year_start = UNIX_EPOCH + Duration::from_secs(duration.num_seconds() as u64);
|
|
||||||
|
|
||||||
//let ranges = &format!(
|
|
||||||
// "{}_{}-{}_{}",
|
|
||||||
// thirty_days_ago.duration_since(SystemTime::UNIX_EPOCH)?.as_secs(),
|
|
||||||
// now.duration_since(SystemTime::UNIX_EPOCH)?.as_secs(),
|
|
||||||
// year_start.duration_since(SystemTime::UNIX_EPOCH)?.as_secs(),
|
|
||||||
// now.duration_since(SystemTime::UNIX_EPOCH)?.as_secs(),
|
|
||||||
//);
|
|
||||||
let ranges = &format!(
|
|
||||||
"{}_{}",
|
|
||||||
year_start.duration_since(SystemTime::UNIX_EPOCH)?.as_secs(),
|
|
||||||
now.duration_since(SystemTime::UNIX_EPOCH)?.as_secs(),
|
|
||||||
);
|
|
||||||
request_vars.insert("custom_uptime_ranges", ranges);
|
|
||||||
request_vars.insert("response_times_average", "1440");
|
|
||||||
request_vars.insert("response_times", "1");
|
|
||||||
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
let res = client
|
|
||||||
.post("https://api.uptimerobot.com/v2/getMonitors")
|
|
||||||
.form(&request_vars)
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let resp = res.json::<serde_json::Value>().await?;
|
|
||||||
|
|
||||||
let monitors = resp
|
|
||||||
.get("monitors")
|
|
||||||
.context("Response did not have a monitors subobject")?
|
|
||||||
.as_array()
|
|
||||||
.context("Monitors subobject was not an array")?;
|
|
||||||
|
|
||||||
let mut state = match arc_state.lock() {
|
|
||||||
Ok(val) => val,
|
|
||||||
Err(_) => {
|
|
||||||
return Err(anyhow!("Could not lock shared state"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
state.uptimes.clear();
|
|
||||||
for monitor in monitors {
|
|
||||||
let monitor_fqn = monitor
|
|
||||||
.get("friendly_name")
|
|
||||||
.context("Monitor did not have property 'friendly_name'")?;
|
|
||||||
|
|
||||||
debug!("Monitor '{}' processing", monitor_fqn);
|
|
||||||
|
|
||||||
let split_str: Vec<&str> = monitor_fqn
|
|
||||||
.as_str()
|
|
||||||
.context("Expected 'friendly_name' to be a string")?
|
|
||||||
.split(".")
|
|
||||||
.collect();
|
|
||||||
if split_str.len() != 2 {
|
|
||||||
debug!("Monitor '{}' excluded due to bad format", monitor_fqn);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let monitor_nt = String::from(
|
|
||||||
*split_str
|
|
||||||
.get(0)
|
|
||||||
.context("Expected name to have first part")?,
|
|
||||||
);
|
|
||||||
let monitor_name = String::from(
|
|
||||||
*split_str
|
|
||||||
.get(1)
|
|
||||||
.context("Expected name to have second part")?,
|
|
||||||
);
|
|
||||||
let monitor_type = match monitor_nt.as_str() {
|
|
||||||
"datacenter" => UptimeType::Datacenter,
|
|
||||||
"service" => UptimeType::Service,
|
|
||||||
"competitor" => UptimeType::Provider,
|
|
||||||
_ => UptimeType::Unknown,
|
|
||||||
};
|
|
||||||
|
|
||||||
if monitor_type == UptimeType::Unknown {
|
|
||||||
debug!("Monitor '{}' excluded due to unknown type", monitor_fqn);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let monitor_status_num = monitor
|
|
||||||
.get("status")
|
|
||||||
.context("Expected monitor to have 'status' property")?
|
|
||||||
.as_u64()
|
|
||||||
.context("Expected 'status' property to be u64")?;
|
|
||||||
|
|
||||||
let monitor_status = match monitor_status_num {
|
|
||||||
0 => UptimeStatus::Maintenance,
|
|
||||||
1 | 8 | 9 => UptimeStatus::Down,
|
|
||||||
2 => UptimeStatus::Up,
|
|
||||||
_ => UptimeStatus::Unknown,
|
|
||||||
};
|
|
||||||
|
|
||||||
if monitor_status == UptimeStatus::Unknown {
|
|
||||||
debug!(
|
|
||||||
"Monitor '{}' excluded due to unknown status (status was {})",
|
|
||||||
monitor_fqn, monitor_status_num
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let monitor_rt_val = monitor
|
|
||||||
.get("average_response_time")
|
|
||||||
.context("Expected monitor to have property 'average_response_time'")?;
|
|
||||||
|
|
||||||
// Because UptimeRobot has the world's worst API ever
|
|
||||||
// and decided that it's okay to return multiple datatypes
|
|
||||||
// for one property based on how they're feeling
|
|
||||||
let monitor_rt = match monitor_rt_val.as_str() {
|
|
||||||
Some(string) => format!("{}ms", string),
|
|
||||||
_ => format!("N/A"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let monitor_uptime = format!(
|
|
||||||
"{}%",
|
|
||||||
monitor
|
|
||||||
.get("custom_uptime_ranges")
|
|
||||||
.context("Expected monitor to have property 'custom_uptime_ranges'")?
|
|
||||||
.as_str()
|
|
||||||
.context("Expected 'custom_uptime_ranges' to be String")?
|
|
||||||
);
|
|
||||||
|
|
||||||
let monitor_url = String::from(
|
|
||||||
monitor
|
|
||||||
.get("url")
|
|
||||||
.context("Expected monitor to have property 'url'")?
|
|
||||||
.as_str()
|
|
||||||
.context("Expected 'url' to be String")?,
|
|
||||||
);
|
|
||||||
|
|
||||||
state.uptimes.push(Uptime {
|
|
||||||
name: monitor_name,
|
|
||||||
uptime: monitor_uptime,
|
|
||||||
response_time: monitor_rt,
|
|
||||||
status: monitor_status,
|
|
||||||
uptime_type: monitor_type,
|
|
||||||
url: monitor_url,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
state.last_updated = SystemTime::now();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
<h1>Blog</h1>
|
|
||||||
|
|
||||||
{% for blog in blogs %}
|
|
||||||
<div style="margin-top: 10px; margin-bottom: 10px;">
|
|
||||||
<h3 style="margin-bottom: 0px;">
|
|
||||||
<a
|
|
||||||
{% if blog.url.contains("https://") %}
|
|
||||||
href="{{ blog.url }}"
|
|
||||||
{% else %}
|
|
||||||
href="/blogs/{{ blog.url }}"
|
|
||||||
{% endif %}
|
|
||||||
>
|
|
||||||
[ {{ blog.title }} ]
|
|
||||||
</a>
|
|
||||||
</h3>
|
|
||||||
<p style="margin-top: 0px;"><i>{{ blog.date }}</i></p>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
|
@ -1,89 +0,0 @@
|
||||||
<h1>NWS Incident Postmortem 11/08/2023</h1>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
On November 8th, 2023 at approximately 09:47 UTC, NWS suffered
|
|
||||||
a complete outage. This outage resulted in the downtime of all
|
|
||||||
services hosted on NWS and the downtime of the NWS Management
|
|
||||||
Engine and the NWS dashboard.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
The incident lasted 38 minutes after which it was automatically
|
|
||||||
resolved and all services were restored. This is NWS' first
|
|
||||||
outage event of 2023.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>Cause</h2>
|
|
||||||
<p>
|
|
||||||
NWS utilizes several tactics to ensure uptime. A component of
|
|
||||||
this is load balancing and failover. This service is currently
|
|
||||||
provided by Cloudflare at the DNS level. Cloudflare sends
|
|
||||||
health check requests to NWS servers at specified intervals. If
|
|
||||||
it detects that one of the servers is down, it will remove the
|
|
||||||
A record from entry.nws.nickorlow.com for that server (this domain
|
|
||||||
is where all services on NWS direct their traffic via a
|
|
||||||
CNAME).
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
At around 09:47 UTC, Cloudflare detected that our servers in
|
|
||||||
Texas (Austin and Hill Country) were down. It did not detect an
|
|
||||||
error, but rather an HTTP timeout. This is an indication that the
|
|
||||||
server may have lost network connectivity. When Cloudflare detected that the
|
|
||||||
servers were down, it removed their A records from the
|
|
||||||
entry.nws.nickorlow.com domain. Since NWS Pennsylvania servers
|
|
||||||
have been undergoing maintenance since August 2023, this left no
|
|
||||||
servers able to serve requests routed to entry.nws.nickorlow.com,
|
|
||||||
resulting in the outage.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
NWS utilizes UptimeRobot for monitoring the uptime statistics of
|
|
||||||
services on NWS and NWS servers. This is the source of the
|
|
||||||
statistics shown on the NWS status page.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
UptimeRobot did not detect either of the Texas NWS servers as being
|
|
||||||
offline for the duration of the outage. This is odd, as UptimeRobot
|
|
||||||
and Cloudflare did not agree on the status of NWS servers. Logs
|
|
||||||
on NWS servers showed that requests from UptimeRobot were being
|
|
||||||
served while no requests from Cloudflare were shown in the logs.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
No firewall rules existed that could have blocked the healthcheck traffic from Cloudflare
|
|
||||||
for either of the NWS servers. There was no other configuration
|
|
||||||
found that would have blocked these requests. As these servers
|
|
||||||
are on different networks inside different buildings in different
|
|
||||||
parts of Texas, their networking equipment is entirely separate.
|
|
||||||
This rules out any failure of networking equipment owned
|
|
||||||
by NWS. This leads us to believe that the issue may have been
|
|
||||||
caused due to an internet traffic anomaly, although we are currently
|
|
||||||
unable to confirm that this is the cause of the issue.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
This is being actively investigated to find a more concrete root
|
|
||||||
cause. This postmortem will be updated if any new information is
|
|
||||||
found.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
A similar event occurred on November 12th, 2023 lasting for 2 seconds.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>Fix</h2>
|
|
||||||
<p>
|
|
||||||
The common factor between both of these servers is that they both use
|
|
||||||
Spectrum for their ISP and that they are located near Austin, Texas.
|
|
||||||
The Pennsylvania server maintenance will be expedited so that we have
|
|
||||||
servers online that operate with no commonalities.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
NWS will also investigate other methods of failover and load
|
|
||||||
balancing.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>Last updated on November 16th, 2023</p>
|
|
|
@ -1,51 +0,0 @@
|
||||||
<h1>NWS Incident Postmortem 11/28/2024 - Present</h1>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
On November 28th, 2024 at approximately 07:37 UTC, NWS suffered
|
|
||||||
a complete outage. This outage resulted in the downtime of all
|
|
||||||
services hosted on NWS and the downtime of the NWS Management
|
|
||||||
Engine and the NWS dashboard.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
The incident lasted 10 days and 15 hours after which it was manually
|
|
||||||
resolved and all services were restored. This was NWS' first
|
|
||||||
outage event of 2024.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
Since then, similar outages have occurred.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>Cause</h2>
|
|
||||||
<p>
|
|
||||||
NWS utilizes several tactics to ensure uptime. A component of
|
|
||||||
this is load balancing and failover. Due to logistical issues,
|
|
||||||
only one NWS point of presence has been operating since early
|
|
||||||
November 2024. This means that any issue with the remaining
|
|
||||||
datacenter will result in a total outage. More points of presence
|
|
||||||
are expected to be brought online in August 2024. Similar incidents are
|
|
||||||
expected until then.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
This outage lasted 10 days due to the fact that I was busy with
|
|
||||||
school. I'm not super concerned about maintaining high uptime with
|
|
||||||
only one server, and I'm pretty happy with NWS since we hit 100% uptime
|
|
||||||
for a >365 day period.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
The cause of the outage was that the Xfinity ( yeah :( ) router that
|
|
||||||
NWS uses in the Pottsville location encountered an issue which caused
|
|
||||||
it to automatically drop all port forwards. To combat this issue, a new
|
|
||||||
Ubiquiti EdgeMax router is scheduled to be installed in December 2024.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
|
|
||||||
<h2>Fix</h2>
|
|
||||||
<p>
|
|
||||||
The port forwards were restored and the router is scheduled to be replaced.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>Last updated on December 28th, 2024</p>
|
|
|
@ -1,9 +0,0 @@
|
||||||
<h1>Goodbye, NWS</h1>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<b>
|
|
||||||
Nick Web Services (NWS) is now Nick Web Services (NWS).
|
|
||||||
</b>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>That is all</p>
|
|
|
@ -1,2 +0,0 @@
|
||||||
<h1>Under Construction</h1>
|
|
||||||
<p>The new dashboard isn't ready yet! Nobody but me used it anyways!</p>
|
|
|
@ -1,31 +0,0 @@
|
||||||
{%- import "uptime_table.html" as scope -%}
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h1 style="margin-bottom: 0px;margin-top: 0px;">Nick Web Services</h1>
|
|
||||||
<p style="margin-top: 0px;">Pottsville, PA - Philadelphia, PA - Austin, TX</p>
|
|
||||||
|
|
||||||
|
|
||||||
<p>
|
|
||||||
Nick Web Services is a hosting service based out of the Commonwealth of Pennsylvania
|
|
||||||
and the State of Texas.
|
|
||||||
We are committed to achieving maximum uptime with better performance and a lower
|
|
||||||
cost than any of the major cloud services.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
We operate four datacenters located across three cities in two states. This infrastructure setup ensures redundancy and failover capabilities, minimizing downtime risks. Additionally, the geographical distribution enhances speed and accessibility, reducing latency for users across different regions.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
This has led to us maintaining four nines availability (99.9931% ; 38 minutes of downtime
|
|
||||||
all year) for 2023 and <b>100% uptime for the period from 11/8/2023 to 11/28/2024 (over a year!). This was the original goal of NWS.</b>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
Currently, NWS is only able to operate with one point of presence and as such, will
|
|
||||||
have reduced uptime. This is expected to be resolved around August 2024.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>Compare us to our competitors!</h2>
|
|
||||||
{% call scope::uptime_table(uptime_infos) %}
|
|
||||||
</div>
|
|
|
@ -1,54 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
|
|
||||||
{% if let Some(title) = page_title %}
|
|
||||||
<title>{{ title }}</title>
|
|
||||||
{% else %}
|
|
||||||
<title>Nick Web Services</title>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if let Some(desc) = page_desc %}
|
|
||||||
<meta name="{{ desc }}" />
|
|
||||||
{% else %}
|
|
||||||
<meta name="Nick Web Services" />
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/style.css">
|
|
||||||
<link rel="icon" type="image/x-icon" href="/assets/favicon.ico">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<nav>
|
|
||||||
<div style="display: flex; justify-content: space-between;">
|
|
||||||
<div>
|
|
||||||
<a class="nav-link" href="/">[ Home ]</a>
|
|
||||||
<a class="nav-link" href="/system_status">[ System Status ]</a>
|
|
||||||
<a class="nav-link" href="/blog">[ Blog ]</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a class="nav-link" href="/dashboard">[ Dashboard ]</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
<hr/>
|
|
||||||
|
|
||||||
{{ content|safe }}
|
|
||||||
|
|
||||||
<footer>
|
|
||||||
<hr />
|
|
||||||
<div style="display: flex; justify-content: space-between;">
|
|
||||||
<div>
|
|
||||||
<p style="margin-bottom: 0px; margin-top:0px;"><b>Nick Web Services</b></p>
|
|
||||||
<p style="margin-bottom: 0px;margin-top: 0px;">
|
|
||||||
<small>Copyright © <a href="https://nickorlow.com">Nicholas Orlowsky</a> 2024</small>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<img class="flag-img" src="/assets/flag-images/us.png" title="The United States of America"/>
|
|
||||||
<img class="flag-img" src="/assets/flag-images/us-pa.png" title="The Commonwealth of Pennsylvania"/>
|
|
||||||
<img class="flag-img" src="/assets/flag-images/us-tx.png" title="The State of Texas"/>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,26 +0,0 @@
|
||||||
{%- import "uptime_table.html" as scope -%}
|
|
||||||
|
|
||||||
<h1>System Status</h1>
|
|
||||||
|
|
||||||
<h2>Datacenter Status</h2>
|
|
||||||
<p>
|
|
||||||
The status of each of Nick Web Services's 4
|
|
||||||
datacenters.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{% call scope::uptime_table(dctr_uptime_infos) %}
|
|
||||||
|
|
||||||
<p>
|
|
||||||
Notice: Due to leasing issues, the Philadelphia datacenter will be offline until
|
|
||||||
at least May or August 2025 or it may be discontinued as an NWS location.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>Service Status</h2>
|
|
||||||
<p>
|
|
||||||
The status of services people host on Nick Web Services.
|
|
||||||
Note that the uptime and performance of services hosted on
|
|
||||||
Nick Web Services may be affected by factors not controlled by us such as
|
|
||||||
bad optimization or buggy software.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{% call scope::uptime_table(svc_uptime_infos) %}
|
|
|
@ -1,35 +0,0 @@
|
||||||
{% macro uptime_table(uptime_infos) %}
|
|
||||||
<table style="width: 100%;">
|
|
||||||
<tr>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Uptime YTD</th>
|
|
||||||
<th>Response Time 24h</th>
|
|
||||||
<th>Current Status</th>
|
|
||||||
</tr>
|
|
||||||
{% for uptime_info in uptime_infos %}
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
{% if let Some(click_url) = uptime_info.url %}
|
|
||||||
<a href="{{click_url}}">
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{{uptime_info.name}}
|
|
||||||
|
|
||||||
{% if let Some(click_url) = uptime_info.url %}
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>{{uptime_info.uptime}}</td>
|
|
||||||
<td>{{uptime_info.response_time}}</td>
|
|
||||||
<td
|
|
||||||
{% if uptime_info.status != "Up" %}
|
|
||||||
style="color: red;"
|
|
||||||
{% endif %}
|
|
||||||
>
|
|
||||||
{{uptime_info.status}}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
<p style="margin-top: 0px;"><i>Data current as of {{last_updated}}</i></p>
|
|
||||||
{% endmacro %}
|
|
26
tsconfig.json
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src"
|
||||||
|
]
|
||||||
|
}
|