diff --git a/.gitignore b/.gitignore index aad40b8..f0c2c7d 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ # testing /coverage +cypress.env.json # next.js /.next/ @@ -39,4 +40,5 @@ next-env.d.ts package-lock.json # -.env \ No newline at end of file +.env.local +.env diff --git a/app/Providers.tsx b/app/Providers.tsx new file mode 100644 index 0000000..625346b --- /dev/null +++ b/app/Providers.tsx @@ -0,0 +1,8 @@ +"use client" + +import React from 'react' +import { SessionProvider } from "next-auth/react" + +export function AuthProvider({children}: {children: React.ReactNode}) { + return {children} +} \ No newline at end of file diff --git a/app/_components/AddMemberClient.tsx b/app/_components/AddMemberClient.tsx new file mode 100644 index 0000000..9e75119 --- /dev/null +++ b/app/_components/AddMemberClient.tsx @@ -0,0 +1,39 @@ +"use client" +import { FormEvent } from 'react'; +import { useRouter } from 'next/navigation'; +import React from 'react'; + + + +export default function AddMemberClient() { + + const router = useRouter(); + + async function addMember(event: FormEvent) { + event?.preventDefault(); + const formData = new FormData(event.currentTarget); + let body:{email?: string} = {} + + body.email = formData.get('email')?.toString(); + + const response = await fetch('/api/addMember/send-invite', { + method: 'POST', + body: JSON.stringify(body), + }) + const res = await response.json(); + console.log(res); + alert(`Invitation sent to ${body.email}`); + window.location.reload(); + } + + + return ( +
+

Invite other users to join this cluster:

+
+ + +
+
+ ) +} \ No newline at end of file diff --git a/app/_components/ConnectClusterPage/ConnectClusterModal.tsx b/app/_components/ConnectClusterPage/ConnectClusterModal.tsx new file mode 100644 index 0000000..eb24c29 --- /dev/null +++ b/app/_components/ConnectClusterPage/ConnectClusterModal.tsx @@ -0,0 +1,156 @@ +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import React from 'react'; + +export default function ConnectClusterModal({ + isModalVisible, + toggleModal, +}: any) { + const router = useRouter(); + const [clusterName, setClusterName] = useState(""); + const [clusterIp, setClusterIp] = useState(""); + const [error, setError] = useState(""); + + const resetFields = () => { + setClusterName(""); + setClusterIp(""); + setError(""); + }; + + // This function takes the passed in form data and sends it to the server + async function newCluster(e: React.FormEvent) { + e.preventDefault(); + + // Prepare the data to be sent + const clusterData = { + clusterName, + clusterIp, + }; + + // POST REQUEST TO ADD CLUSTER NAME AND CLUSTER IP TO DATABASE + try { + let res = await fetch("/api/connect-cluster", { + method: "POST", + body: JSON.stringify(clusterData), + headers: { "Content-Type": "application/json" }, + }); + const data = await res.json(); + if (!data.newCluster) { + setError(data.error); + } else { + router.push("/dashboard"); + toggleModal(); + resetFields(); + } + } catch (error) { + setError("An error occurred while adding your cluster. Please try again"); + console.error("Error sending data:", error); + } + } + + const handleClose = () => { + toggleModal(); + resetFields(); + }; + + return ( +
+
+ {/* Modal content here */} + +
+ ); +} diff --git a/app/_components/ConnectClusterPage/EditCluster.tsx b/app/_components/ConnectClusterPage/EditCluster.tsx new file mode 100644 index 0000000..9569f5e --- /dev/null +++ b/app/_components/ConnectClusterPage/EditCluster.tsx @@ -0,0 +1,57 @@ +"use client" + +import { FormEvent } from 'react'; +import React from 'react'; + +export default function EditCluster (props: {clusterName:string, clusterIp:string, cluster_id?:string, change_cluster: Function, change_edit: Function}) { + + + async function editCluster(event: FormEvent) { + event?.preventDefault(); + const formData = new FormData(event.currentTarget); + let body:{cluster_name?: string, cluster_ip?: string, cluster_id?:string} = {} + + body.cluster_name = formData.get('cluster_name')?.toString(); + body.cluster_ip = formData.get('cluster_ip')?.toString(); + body.cluster_id = props.cluster_id + + if (body !== undefined) { + const response = await fetch('/api/updateCluster/edit', { + method: 'POST', + body: JSON.stringify(body), + }) + let res = await response.json() + console.log(res) + } + + + props.change_cluster(body.cluster_name, body.cluster_ip); + + alert('Changes saved') + + props.change_edit(); + +} + + return ( + +
+
+
+
+

Cluster Name:

+ +

+

+

Cluster IP:

+ +

+ +
+
+
+
+ + ) + +} \ No newline at end of file diff --git a/app/_components/ConnectClusterPage/UserClusters.tsx b/app/_components/ConnectClusterPage/UserClusters.tsx new file mode 100644 index 0000000..2e76514 --- /dev/null +++ b/app/_components/ConnectClusterPage/UserClusters.tsx @@ -0,0 +1,40 @@ +"use client" +import React from 'react'; + +export default function UserClusters(props: { owner?: string; clusterName: string; clusterIp: string; edit: string }) { + + return ( +
+ + + + + + + + + + {/* Table Body Rendered with Users Corresponding Clusters */} + + + + + + + +
+ Cluster Name + + Cluster IP + + User Permissions +
+ {props.clusterName} + {props.clusterIp}{props.owner} +
+
+ ); +} diff --git a/app/_components/LoginPage.tsx b/app/_components/LoginPage.tsx new file mode 100644 index 0000000..9b04fcd --- /dev/null +++ b/app/_components/LoginPage.tsx @@ -0,0 +1,143 @@ +"use client"; + +import React from 'react' +import Link from "next/link"; +import Image from "next/image"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import LogInImage from "../../public/assets/NotiKubeLogin.svg"; +import { SignInResponse, signIn } from "next-auth/react"; +import Logo from "../../public/assets/logo.svg" +import githubLogo from "../../public/assets/github-mark.svg" +import Snackbar from '@mui/material/Snackbar'; +import Alert from '@mui/material/Alert'; + +const LoginPage = () => { + const router = useRouter(); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [showFieldsError, setShowFieldsError] = useState(false) + const [showUserNotFound, setShowUserNotFound] = useState(false) + + async function submit(e: React.MouseEvent) { + e.preventDefault(); + if (email == "" || password == "") { + setShowFieldsError(true) + return; + } + const params = { + username: email, + password: password, + }; + + try { + const res: any = await signIn("credentials", { + email: email, + password: password, + redirect: false, + }); + + if (res.error) { + console.log(res.error) + setShowUserNotFound(true) + return; + } + router.replace('/dashboard') + } + catch(e) { + console.log(e) + } + } + + const handleGithubLogin = (e: React.MouseEvent) => { + e.preventDefault(); + signIn('github') + } + + return ( + <> + setShowFieldsError(false)} anchorOrigin={{vertical: 'top', horizontal: 'center'}}> + setShowFieldsError(false)} + severity="error" + variant="filled" + sx={{ width: '100%' }} + > + Please fill in all of the fields! + + + + setShowUserNotFound(false)} anchorOrigin={{vertical: 'top', horizontal: 'center'}}> + setShowUserNotFound(false)} + severity="error" + variant="filled" + sx={{ width: '100%' }} + > + User not found! + + + +
+
+
+
+ + logo + + NotiKube +
+
+

Login

+ +
+
+
+ + setEmail(e.target.value)} + /> +
+
+ + setPassword(e.target.value)} + /> +
+ submit(e)} + value="Submit" + /> +

+ Don't have an account?{" "} + + Create an account here + +

+
+
+
+ Login +
+
+ + ); +}; + +export default LoginPage; diff --git a/app/_components/Sidebar.tsx b/app/_components/Sidebar.tsx deleted file mode 100644 index 5c126ad..0000000 --- a/app/_components/Sidebar.tsx +++ /dev/null @@ -1,169 +0,0 @@ -'use client'; - -// import { Link } from "react-router-dom"; -import Link from "next/link"; -import { usePathname } from "next/navigation"; - -export default function Sidebar() { - - const pathname = usePathname(); - - return ( -
- {/* Main HTML tag where Sidebar component is */} - -
- ); -} diff --git a/app/_components/Sidebar/NavLinks.tsx b/app/_components/Sidebar/NavLinks.tsx new file mode 100644 index 0000000..c7dda6c --- /dev/null +++ b/app/_components/Sidebar/NavLinks.tsx @@ -0,0 +1,29 @@ +import DashboardSideBar from "./SideBarList/DashboardSidebar"; +import IncidentsSideBar from "./SideBarList/IncidentsSidebar"; +import ConnectClusterSideBar from "./SideBarList/ConnectClusterSidebar"; +// import ConfigurationsSidebar from "./SideBarList/ConfigurationsSidebar"; + +export default function NavLinks() { + return ( + <> + + + ); +} diff --git a/app/_components/Sidebar/SideBarList/ConfigurationsSidebar.tsx b/app/_components/Sidebar/SideBarList/ConfigurationsSidebar.tsx new file mode 100644 index 0000000..7022ff3 --- /dev/null +++ b/app/_components/Sidebar/SideBarList/ConfigurationsSidebar.tsx @@ -0,0 +1,40 @@ +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +export default function ConfigurationsSidebar() { + const pathname = usePathname(); + const isConfigurationsPage = pathname === "/dashboard/configurations"; + + return ( + <> + + + Configurations + + + ); +} diff --git a/app/_components/Sidebar/SideBarList/ConnectClusterSidebar.tsx b/app/_components/Sidebar/SideBarList/ConnectClusterSidebar.tsx new file mode 100644 index 0000000..8950c3d --- /dev/null +++ b/app/_components/Sidebar/SideBarList/ConnectClusterSidebar.tsx @@ -0,0 +1,39 @@ +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +export default function ConnectClusterSideBar() { + const pathname = usePathname(); + const isConnectClusterPage = pathname === "/dashboard/connect-cluster" + + return ( + <> + + + + + + + Connect Cluster + + + ); +} diff --git a/app/_components/Sidebar/SideBarList/DashboardSidebar.tsx b/app/_components/Sidebar/SideBarList/DashboardSidebar.tsx new file mode 100644 index 0000000..d92a1be --- /dev/null +++ b/app/_components/Sidebar/SideBarList/DashboardSidebar.tsx @@ -0,0 +1,40 @@ +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +export default function DashboardSideBar() { + const pathname = usePathname(); + const isDashboardPage = pathname === "/dashboard"; + + return ( + <> + + + Dashboard + + + ); +} diff --git a/app/_components/Sidebar/SideBarList/IncidentsSidebar.tsx b/app/_components/Sidebar/SideBarList/IncidentsSidebar.tsx new file mode 100644 index 0000000..7f7d1af --- /dev/null +++ b/app/_components/Sidebar/SideBarList/IncidentsSidebar.tsx @@ -0,0 +1,41 @@ +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +export default function IncidentsSideBar() { + const pathname = usePathname(); + const isIncidentsPage = pathname === "/dashboard/incidents"; + + return ( + <> + + + + + + + Incidents + + + ); +} diff --git a/app/_components/Sidebar/SideBarList/NotiKubeLogo.tsx b/app/_components/Sidebar/SideBarList/NotiKubeLogo.tsx new file mode 100644 index 0000000..15de93a --- /dev/null +++ b/app/_components/Sidebar/SideBarList/NotiKubeLogo.tsx @@ -0,0 +1,21 @@ +import Link from "next/link"; +import Image from "next/image"; + +export default function NotiKubeLogo() { + return ( +
+ + NotiKube Logo + + NotiKube + + +
+ ); +} diff --git a/app/_components/Sidebar/SideBarList/ProfileSidebar.tsx b/app/_components/Sidebar/SideBarList/ProfileSidebar.tsx new file mode 100644 index 0000000..309fdc2 --- /dev/null +++ b/app/_components/Sidebar/SideBarList/ProfileSidebar.tsx @@ -0,0 +1,37 @@ +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +export default function ProfileSideBar() { + const pathname = usePathname(); + const isProfilePage = pathname === "/dashboard/profile"; + + return ( + <> + + + + + + + Profile + + + ); +} diff --git a/app/_components/Sidebar/SideBarList/SignoutSidebar.tsx b/app/_components/Sidebar/SideBarList/SignoutSidebar.tsx new file mode 100644 index 0000000..0485b76 --- /dev/null +++ b/app/_components/Sidebar/SideBarList/SignoutSidebar.tsx @@ -0,0 +1,32 @@ +import { signOut } from "next-auth/react"; +import Link from "next/link"; + +export default function SignoutSideBar() { + return ( +
+ signOut()} + className="flex items-center p-2 px-12 text-base font-normal text-gray-900 rounded-lg dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700 group" + > + + Sign Out + +
+ ); +} diff --git a/app/_components/Sidebar/Sidebar.tsx b/app/_components/Sidebar/Sidebar.tsx new file mode 100644 index 0000000..bb6d1ed --- /dev/null +++ b/app/_components/Sidebar/Sidebar.tsx @@ -0,0 +1,68 @@ +"use client"; + +import NotiKubeLogo from "./SideBarList/NotiKubeLogo"; +import NavLinks from "./NavLinks"; +import SignoutSideBar from "./SideBarList/SignoutSidebar"; +import { useSession } from "next-auth/react"; +import Image from "next/image"; +import Link from "next/link"; + +export default function Sidebar() { + const { data: session, status } = useSession(); + const username = session?.user.name; + const email = session?.user.email; + + return ( + <> + {/* Main HTML tag where Sidebar component is */} + + + ); +} diff --git a/app/_components/SignUpPage.tsx b/app/_components/SignUpPage.tsx new file mode 100644 index 0000000..d291f43 --- /dev/null +++ b/app/_components/SignUpPage.tsx @@ -0,0 +1,162 @@ +"use client"; + +import React from 'react'; +import Link from 'next/link'; +import Image from 'next/image'; +import { useRouter } from 'next/navigation';; +import { useState, MouseEvent } from 'react'; +// import { useNavigate } from 'react-router-dom'; +import SignUpImage from '../../public/assets/NotiKubeSignUp.svg' +import Logo from '../../public/assets/logo.svg' +import { Toast } from 'flowbite-react'; +import { HiExclamation} from 'react-icons/hi'; +import Snackbar from '@mui/material/Snackbar'; +import Alert from '@mui/material/Alert'; + +const Signup = () => { + const [fullName, setFullName] = useState(''); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [showFieldsError, setShowFieldsError] = useState(false) + const [showUserExistsError, setShowUserExistsError] = useState(false) + const [showUserCreated, setShowUserCreated] = useState(false) + + const router = useRouter() + + async function submit(e: React.MouseEvent) { + e.preventDefault(); + if (fullName == "" || email == "" || password == "") { + setShowFieldsError(true) + return + } + const params = { + fullName: fullName, + email: email, + password: password, + }; + + let res = await fetch("/api/register", { + method: "POST", + headers: { + "Content-type": "application/json", + }, + body: JSON.stringify(params), + }); + + const data = await res.json(); + if (data.newUser) { + setShowUserCreated(true) + } + else { + setShowUserExistsError(true) + } + } + + const RouteToLogin = () => { + setShowUserCreated(false) + router.push('/auth/login') + } + + return ( + <> + + setShowFieldsError(false)} anchorOrigin={{vertical: 'top', horizontal: 'center'}}> + setShowFieldsError(false)} + severity="error" + variant="filled" + sx={{ width: '100%' }} + > + Please fill in all of the fields! + + + + setShowUserExistsError(false)} anchorOrigin={{vertical: 'top', horizontal: 'center'}}> + setShowUserExistsError(false)} + severity="error" + variant="filled" + sx={{ width: '100%' }} + > + User already exists! + + + + RouteToLogin()} anchorOrigin={{vertical: 'top', horizontal: 'center'}}> + RouteToLogin()} + severity="success" + variant="filled" + sx={{ width: '100%' }} + > + New user created! + + + +
+
+
+
+ + logo + + NotiKube +
+

Sign Up

+
+ + setFullName(e.target.value)} + /> +
+
+ + setEmail(e.target.value)} + /> +
+
+ + setPassword(e.target.value)} + /> +
+ submit(e)} + /> +

+ Already have an account?{' '} + + Log in + +

+
+
+
+ Sign Up +
+
+ + + ); +}; + +export default Signup; diff --git a/app/_components/Table.tsx b/app/_components/Table.tsx new file mode 100644 index 0000000..40763d9 --- /dev/null +++ b/app/_components/Table.tsx @@ -0,0 +1,299 @@ +'use client'; + +import * as React from 'react'; +import { useState, useEffect } from 'react'; +import { DataGrid, GridRowModel, GridColDef } from '@mui/x-data-grid'; +import { FormControl, InputLabel, MenuItem, Select, SelectChangeEvent,} from '@mui/material'; +import { useSession } from 'next-auth/react'; +import { useRouter } from 'next/navigation'; +import { Incident, UserName } from '../../types/definitions'; +import Backdrop from '@mui/material/Backdrop'; +import CircularProgress from '@mui/material/CircularProgress'; + + + +const Table = () => { + + const router = useRouter(); + + const session = useSession().data; + const userId = session?.user?.userid; + + const [incidentList, setIncidentList] = useState([]); + const [memberList, setMemberList] = useState([]); + const [loading, setLoading] = useState(true); + const [rowSort, setRowSort] = useState('none'); + + let redirectURL = ''; + process.env.NODE_ENV === 'development' ? redirectURL = 'http://localhost:3000/dashboard/connect-cluster' : redirectURL = 'http://www.notikube.com/dashboard/connect-cluster' + + async function fetchData(user_id: (string | undefined)) { + if (user_id !== undefined) { + let res = await fetch(`/api/incidents/getAlerts/${user_id}`, {cache: 'no-store'}) + const data: [Incident[], UserName[]] = await res.json(); + setIncidentList(data[0]); + setMemberList(data[1]); + setLoading(false); + } + } + + let memberArray: UserName[] = memberList; + let members: Array = []; + + for (let key in memberArray) { + members.push(memberArray[key].name) + } + + members.push(''); + + const updateTable = React.useCallback( + async (newRow: GridRowModel) => { + const updatedRow = { ...newRow }; + updatedRow.id = newRow.incident_id; + fetch(`/api/incidents/update`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(newRow) + }); + return updatedRow; + },[]); + + useEffect(() => { + fetchData(userId); + },[userId]); + + function handleRowColor(event:SelectChangeEvent) { + setRowSort(event.target.value) + } + + const columns: GridColDef[] = [ + { + field: 'incident_date', + headerName: '* Timestamp', + minWidth: 200, + type: 'string', + editable: false, + headerClassName: 'column-header', + }, + { + field: 'incident_type', + headerName: '* Type', + minWidth: 200, + type: 'string', + editable: false, + headerClassName: 'column-header' + }, + { + field: 'description', + headerName: 'Description', + minWidth: 250, + type: 'string', + editable: true, + headerClassName: 'column-header' + }, + { + field: 'priority_level', + headerName: 'Priority', + minWidth: 125, + editable: true , + type: 'singleSelect', + headerClassName: 'column-header', + align: 'left', + headerAlign: 'left', + valueOptions: ['critical', 'warning', 'error', 'info'], + }, + { + field: 'incident_title', + headerName: 'Title', + minWidth: 200, + editable: true , + type: 'string', + headerClassName: 'column-header', + align: 'left', + headerAlign: 'left', + }, + { + field: 'incident_status', + headerName: 'Status', + minWidth: 150, + editable: true , + type: 'singleSelect', + headerClassName: 'column-header', + align: 'left', + headerAlign: 'left', + valueOptions: ['Open', 'Closed', 'In Progress'], + }, + { + field: 'incident_assigned_to', + headerName: 'Assigned To', + minWidth: 150, + editable: true , + type: 'singleSelect', + headerClassName: 'column-header', + align: 'left', + headerAlign: 'left', + valueOptions: members, + } + ]; + + while (loading) { + return ( +
+ theme.zIndex.drawer + 1 }} + open={true} + > + + +
+ )} + + return ( +
+
+

{incidentList[0].cluster_name}

+

Cluster IP Address: {incidentList[0].cluster_ip}

+
+

+
+

Select a row to view/edit incident details

+

Double click a cell to edit

+
+ incidentList.incident_id} + rows={incidentList} + columns={columns} + processRowUpdate={updateTable} + onProcessRowUpdateError={(() => console.log('Error processing row update'))} + onRowEditStop={(params) => { + console.log('params', params); + }} + getRowClassName={(params) => { + if (rowSort === 'priority') { + if (params.row.priority_level === 'critical' || params.row.priority_level === 'error') { + return 'orange' + } else if (params.row.priority_level === 'warning') { + return 'darkblue' + } else { + return 'blue' + } + } else if (rowSort === 'status') { + if (params.row.incident_status === 'Open') { + return 'orange' + } else if (params.row.incident_status === 'In Progress') { + return 'darkblue' + } else { + return 'blue' + } + } else { + if (params.indexRelativeToCurrentPage % 2 === 0) { + return 'even' + } else { + return 'odd' + } + } + }} + onRowSelectionModelChange={(newSelection) => { + router.push(`/dashboard/incident-details/${newSelection}`) + }} + disableRowSelectionOnClick + checkboxSelection={true} + /> +
+ + Row color + + +

Go to router.push(redirectURL)} className='font-bold focus:outline-none text-blue-700 hover:text-primary-600'>Connect Cluster page to add additional members

+
+
+ ) + +}; + + + + + export default Table; \ No newline at end of file diff --git a/app/_components/homePage/clusterDetails.tsx b/app/_components/homePage/clusterDetails.tsx new file mode 100644 index 0000000..d143692 --- /dev/null +++ b/app/_components/homePage/clusterDetails.tsx @@ -0,0 +1,13 @@ +import { Divider } from "@tremor/react"; + +export default async function clusterDetails({ cluster_name, cluster_ip }: { cluster_name: string, cluster_ip: string }) { + + return ( +
+
+

{cluster_name}

+

Cluster IP Address: {cluster_ip}

+
+
+ ) +} diff --git a/app/_components/homePage/clusterMetrics.tsx b/app/_components/homePage/clusterMetrics.tsx new file mode 100644 index 0000000..2afca61 --- /dev/null +++ b/app/_components/homePage/clusterMetrics.tsx @@ -0,0 +1,169 @@ +import { Card, Text, Metric, Title, BarChart, Subtitle, ProgressCircle } from "@tremor/react"; +import { + numOfReadyNodes, + numByNamePods, + numOfUnhealthyNodes, + restartByNamePods, + cpuUtilByNode, + numOfReadyPods, + numOfUnhealthyPods, + clusterMemoryUsage, + clusterCpuUsage10mAvg +} from "../../lib/queries"; +import { NameSpacePods, CircleNode } from "../../../types/definitions"; +import NodeCircle from "./nodeCircle"; +import CPUMemCircle from "./cpuMemCircle"; + +// returns cpu utilization by node in tremor progress circles +export async function NodeCPUHealth({ cluster_ip }: { cluster_ip: string }) { + try { + const cpuUtilByNodeResult = await cpuUtilByNode(cluster_ip); + const nodeCircles = cpuUtilByNodeResult.map((n: CircleNode) => { return }) + + return ( +
+ CPU Utilization By Node +
+ {nodeCircles} +
+
+ ) + } + catch (e) { + console.log('Error - NodeCPUHealth:', e) + return ( +
+ Data Error +
+ ) + } +} + +// returns number of pods by namespace in tremor barchart +export async function PodHealth({ cluster_ip }: { cluster_ip: string }) { + try { + const numByNamePodsResult = await numByNamePods(cluster_ip); + const chartData1 = numByNamePodsResult.map((n: NameSpacePods) => { return { name: n.metric.namespace, 'Number of Pods': Number(n.value[1]) } }) + return ( +
+ + Number of Pods + By Namespace + + +
+ ) + } + catch (e) { + console.log('Error - PodHealth:', e) + return ( +
+ Data Error +
+ ) + } +} + +// returns number of pod restarts by namespace in tremor barchart +export async function PodRestartHealth({ cluster_ip }: { cluster_ip: string }) { + try { + const restartByNamePodsResult = await restartByNamePods(cluster_ip); + const chartData2 = restartByNamePodsResult.map((n: NameSpacePods) => { return { name: n.metric.namespace, 'Number of Restarted Pods': Number(n.value[1]) } }) + return ( +
+ + Number of restarted pods per namespace + By Namepace ~ 5 Minutes + + +
+ ) + } + catch (e) { + console.log('Error - PodRestartHealth:', e) + return ( +
+ Data Error +
+ ) + } +} + +// Returns number of ready/unavail nodes and ready/unavail pods in cluster in tremor metric cards +export async function ClusterHealth({ cluster_ip }: { cluster_ip: string }) { + try { + const numOfReadyNodesResult = await numOfReadyNodes(cluster_ip); + const numOfUnhealthyNodesResult = await numOfUnhealthyNodes(cluster_ip); + const numOfReadyPodsResult = await numOfReadyPods(cluster_ip); + const numOfUnhealthyPodsResult = await numOfUnhealthyPods(cluster_ip); + return ( +
+
+ + Ready Nodes + {numOfReadyNodesResult} + + + Unavailable Nodes + {numOfUnhealthyNodesResult} + + + Ready Pods + {numOfReadyPodsResult} + + + Unavailable Pods + {numOfUnhealthyPodsResult} + +
+
+ ) + } + catch (e) { + console.log('Error - ClusterHealth:', e) + return ( +
+ Data Error +
+ ) + } +} + +// returns cluster cpu and memory usage in two tremor progress circles +export async function ClusterCPUMem({ cluster_ip }: { cluster_ip: string }) { + try { + const memory = await clusterMemoryUsage(cluster_ip); + const cpu = await clusterCpuUsage10mAvg(cluster_ip); + + return ( +
+ +
+ ) + } + catch (e) { + console.log('Error - clusterCPUMem:', e) + return ( +
+ Data Error +
+ ) + } +} + diff --git a/app/_components/homePage/cpuMemCircle 2.tsx b/app/_components/homePage/cpuMemCircle 2.tsx new file mode 100644 index 0000000..c533e01 --- /dev/null +++ b/app/_components/homePage/cpuMemCircle 2.tsx @@ -0,0 +1,40 @@ +'use client' + +import React from 'react' +import { ProgressCircle, Card } from "@tremor/react"; +import { clusterMemoryUsage, clusterCpuUsage10mAvg } from '../../lib/queries'; + +export default function CPUMemCircle({ cpu, memory }: { cpu: number, memory: number }) { + + return ( +
+ +
+

Cluster Memory Usage

+ + {memory.toFixed(2)}% + +
+ +
+

Cluster CPU Usage

+ + {cpu.toFixed(2)}% + +
+
+
+ ) +} \ No newline at end of file diff --git a/app/_components/homePage/cpuMemCircle.tsx b/app/_components/homePage/cpuMemCircle.tsx new file mode 100644 index 0000000..fb83821 --- /dev/null +++ b/app/_components/homePage/cpuMemCircle.tsx @@ -0,0 +1,40 @@ +'use client' + +import React from 'react' +import { ProgressCircle, Card } from "@tremor/react"; +import { clusterMemoryUsage, clusterCpuUsage10mAvg } from '../../lib/queries'; + +export default function CPUMemCircle({ cpu, memory }: { cpu: number, memory: number }) { + + return ( +
+ +
+

Cluster Memory Usage

+ + {memory.toFixed(2)}% + +
+ +
+

Cluster CPU Usage

+ + {cpu.toFixed(2)}% + +
+
+
+ ) +} \ No newline at end of file diff --git a/app/_components/homePage/homeAlerts.tsx b/app/_components/homePage/homeAlerts.tsx new file mode 100644 index 0000000..0398124 --- /dev/null +++ b/app/_components/homePage/homeAlerts.tsx @@ -0,0 +1,36 @@ +import Link from 'next/link' +import { numProgressAlerts, numTotalAlerts, numCriticalAlerts, numOpenAlerts } from '../../lib/homePage/numOfAlerts'; + +export default async function homeAlerts({ cluster_ip }: { cluster_ip: string }) { + const totalAlerts = await numTotalAlerts(cluster_ip); + const totalOpenAlerts = await numOpenAlerts(cluster_ip); + const totalInProgressAlerts = await numProgressAlerts(cluster_ip); + const totalCriticalAlerts = await numCriticalAlerts(cluster_ip); + + return ( +
+
+
+ +
Total Incidents
+

{totalAlerts}

+ + +
Open Incidents
+

{totalOpenAlerts}

+ + +
In Progress Incidents
+

{totalInProgressAlerts}

+ + +
Critical Incidents
+

{totalCriticalAlerts}

+ +
+
+
+ ) +} + + diff --git a/app/_components/homePage/loadingSpinner.tsx b/app/_components/homePage/loadingSpinner.tsx new file mode 100644 index 0000000..f4c9915 --- /dev/null +++ b/app/_components/homePage/loadingSpinner.tsx @@ -0,0 +1,17 @@ +import React from 'react' + +export default function LoadingSpinner() { + return ( +
+
+
+ + Loading... +
+
+
+ ) +} diff --git a/app/_components/homePage/nodeCircle.tsx b/app/_components/homePage/nodeCircle.tsx new file mode 100644 index 0000000..8255b04 --- /dev/null +++ b/app/_components/homePage/nodeCircle.tsx @@ -0,0 +1,25 @@ +'use client' + +import React from 'react' +import { ProgressCircle, Card, Flex } from "@tremor/react"; + +export default function NodeCircle({ name, value }: { name: string, value: number }) { + return ( +
+ +

{name}

+ + + {value.toFixed(2)}% + + +
+
+ ) +} diff --git a/app/_components/incident-details/EditDetails.tsx b/app/_components/incident-details/EditDetails.tsx new file mode 100644 index 0000000..047d982 --- /dev/null +++ b/app/_components/incident-details/EditDetails.tsx @@ -0,0 +1,50 @@ +import * as React from 'react'; +import { EditDetailsType } from '../../../types/definitions' + + + +const EditDetails = (props: EditDetailsType) => { + + return ( +
+
+
+

+

Title:

+

{props.incident_title}

+

+

Assigned By:

+

{props.incident_assigned_by}

+

+

Due Date:

+

{props.incident_due_date}

+

+

Status:

+

{props.incident_status}

+

+

Notes:

+

{props.comment}

+

+
+
+

Priority:

+

{props.priority_level}

+

+

Assigned To:

+

{props.incident_assigned_to}

+

+

Assigned Date:

+

{props.incident_assigned_date}

+

+

Description:

+

{props.description}

+

+
+
+
+ ) + + +}; + +export default EditDetails; \ No newline at end of file diff --git a/app/_components/incident-details/EditForm.tsx b/app/_components/incident-details/EditForm.tsx new file mode 100644 index 0000000..6720dfc --- /dev/null +++ b/app/_components/incident-details/EditForm.tsx @@ -0,0 +1,174 @@ +import * as React from 'react'; +import { FormEvent } from 'react'; +import {useRouter} from 'next/navigation'; +import { Incident, UserName } from '../../../types/definitions' + + +const EditForm = (props: { + incident_title?: string, + priority_level?: string, + incident_status?: string, + description?: string, + comment?: string, + incident_assigned_to?: string, + incident_assigned_by?: string, + incident_assigned_date?: string, + incident_due_date?: string, + incident_type?: string, + cluster_name?: string, + incident_id?: string, + cluster_id?: string, + updateEdit?: Function, + incident_date?: string, + memberProps?: [{name: string, email: string}], +}) => { + + // create new empty array to hold members + let memberArray: Array = []; + + // function to push members from props to member array + function getMembers(array: Array) { + for (let i = 0; i < array.length; i++) { + memberArray.push(array[i].name) + } + }; + + // invoke function to push members from props to member array + if (props.memberProps !== undefined) { + getMembers(props.memberProps); + } + + const router = useRouter(); + + // function to handle submit when user saves edits + async function onSubmit(event: FormEvent) { + event.preventDefault(); + + const formData = new FormData(event.currentTarget); + + // save old state to body object + let body: Incident = JSON.parse(JSON.stringify(props)); + + // if incident_id, cluster_id, and incident_date on props have data, assign them to body object + if (props.incident_id !== undefined && props.cluster_id !== undefined && props.incident_date !== undefined) { + body.incident_id = props.incident_id; + body.cluster_id = props.cluster_id; + body.incident_date = props.incident_date; + } + + // declare newState object and assign it values of body (old state) + let newState: Incident = body; + + // iterate through body object, comparing values to those of the submitted form + // if keys match, but values don't, assign value of form to body + // assign matching key:value pairs from body to newState + for (let key in body) { + if (key !== 'members' && key !== 'updateEdit') { + for (let pair of formData.entries()) { + if (key === pair[0] && body[key as keyof typeof body] !== pair[1].toString() && pair[1].toString() !== 'mm/dd/yyyy') { + body[key as keyof typeof body] = pair[1].toString(); + } + newState[key as keyof typeof newState] = body[key as keyof typeof body] + } + } + } + + // update state on Table component with newState values (form values from edit page) + if (props.updateEdit !== undefined) { + props.updateEdit(newState); + }; + + // send post request with new values from form + const response = await fetch('/api/incidents/updateDetails',{ + method:'POST', + body: JSON.stringify(body), + }) + let res = await response.json(); + console.log(res); + + }; + + // create variables to hold due date and assigned date values + let incident_due_date: (string | undefined) = ''; + let incident_assigned_date: (string | undefined) = ''; + + // if props.incident_due_date or props.incident_assigned_date are empty, assign a placeholder value, otherwise, assign the props value from DB + if (props.incident_due_date === '') { + incident_due_date = 'mm/dd/yyyy' + } else { + incident_due_date = props.incident_due_date + } + + if (props.incident_assigned_date === '') { + incident_assigned_date = 'mm/dd/yyyy' + } else { + incident_assigned_date = props.incident_assigned_date + } + + + return ( +
+
+
+
+

Title:

+ +

+

Assigned By:

+ +

+

Due Date:

+ +

+

Status:

+ +

+

Notes:

+ +

+ +
+
+

Priority:

+ +

+

Assigned To:

+ +

+

Assigned Date:

+ +

+

Description:

+ +

+

+
+
+
+
+ ) + +}; + +export default EditForm; \ No newline at end of file diff --git a/app/_components/incident-details/PermanentDetails.tsx b/app/_components/incident-details/PermanentDetails.tsx new file mode 100644 index 0000000..eb5077f --- /dev/null +++ b/app/_components/incident-details/PermanentDetails.tsx @@ -0,0 +1,25 @@ +import * as React from 'react'; +import { PermanentDetailsType } from '../../../types/definitions'; + + +const PermanentDetails = (props: PermanentDetailsType) => { + + return ( +
+
+
+

Date of Incident:

+

{props.date}

+
+
+

Type of Incident:

+

{props.incident_type}

+
+
+
+ ) + + +}; + +export default PermanentDetails; \ No newline at end of file diff --git a/app/_components/incident-details/SnapshotData/SnapshotData.tsx b/app/_components/incident-details/SnapshotData/SnapshotData.tsx new file mode 100644 index 0000000..bc2e479 --- /dev/null +++ b/app/_components/incident-details/SnapshotData/SnapshotData.tsx @@ -0,0 +1,29 @@ +import { Accordion } from 'flowbite-react'; +import {CpuOrMemoryCircle, NodesAndPodsMetrics} from './VisualizingMetrics'; +import { SnapshotDataDefinition } from '../../../../types/definitions'; + +const SnapshotData = (props: {snapshotData: SnapshotDataDefinition}) => { + const metrics = props.snapshotData + const clusterMemoryUsage = Math.round(parseInt(metrics.cluster_memory_usage)) + const clusterCpuUsage = Math.round(parseInt(metrics.cluster_cpu_usage)) + const readyNodes = parseInt(metrics.ready_nodes) + const unhealthyNodes = parseInt(metrics.unhealthy_nodes) + const readyPods = parseInt(metrics.ready_pods) + const unhealthyPods = parseInt(metrics.unhealthy_pods) + + return ( +
+ + + Incident Cluster Metrics + + + + + + +
+ ) +}; + +export default SnapshotData \ No newline at end of file diff --git a/app/_components/incident-details/SnapshotData/VisualizingMetrics.tsx b/app/_components/incident-details/SnapshotData/VisualizingMetrics.tsx new file mode 100644 index 0000000..0d7a193 --- /dev/null +++ b/app/_components/incident-details/SnapshotData/VisualizingMetrics.tsx @@ -0,0 +1,68 @@ +import React from "react"; +import { Card, ProgressCircle, Text, Metric} from '@tremor/react'; + +function CpuOrMemoryCircle(props: {memory: number, cpu: number}) { + + const {memory, cpu} = props; + + return ( +
+ +
+

Cluster Memory Usage

+ + {memory}% + +
+
+

Cluster CPU Usage

+ + {cpu}% + +
+
+
+ ) +}; + +function NodesAndPodsMetrics(props: {readyNodes: number, unhealthyNodes: number, readyPods: number, unhealthyPods: number}) { + + const {readyNodes, unhealthyNodes, readyPods, unhealthyPods} = props; + + return ( +
+
+ + Ready Nodes + {readyNodes} + + + Unavailable Nodes + {unhealthyNodes} + + + Ready Pods + {readyPods} + + + Unavailable Pods + {unhealthyPods} + +
+
+ ) +}; + +export {CpuOrMemoryCircle, NodesAndPodsMetrics}; diff --git a/app/_components/landingPage/TeamMember.tsx b/app/_components/landingPage/TeamMember.tsx index d450289..007f286 100644 --- a/app/_components/landingPage/TeamMember.tsx +++ b/app/_components/landingPage/TeamMember.tsx @@ -10,7 +10,13 @@ interface TeamMemberProps { role: string; } -const TeamMember: React.FC = ({ name, avatarSrc, githubLink, linkedinLink, role }) => { +const TeamMember: React.FC = ({ + name, + avatarSrc, + githubLink, + linkedinLink, + role, +}) => { return (
+
+

+ Core Technologies Used +

+
+ {[ + { logo: "kubernetes.svg", link: "https://kubernetes.io/" }, + { logo: "docker.svg", link: "https://www.docker.com/" }, + { logo: "prometheus.svg", link: "https://prometheus.io/" }, + { logo: "nextjs.svg", link: "https://nextjs.org/" }, + ].map((tech, index) => ( + + {`${tech.logo.replace(".svg", + + ))} +
+
+
+ ); +} diff --git a/app/_components/landingPage/firstinfoSection.tsx b/app/_components/landingPage/firstinfoSection.tsx new file mode 100644 index 0000000..a073550 --- /dev/null +++ b/app/_components/landingPage/firstinfoSection.tsx @@ -0,0 +1,36 @@ +import Image from "next/image"; + +export default function FirstInfoSection() { + return ( +
+
+
+
+
+

+ Monitor Your Incidents Alongside Cluster Health +

+

+ See the most critical incident metrics pulled directly from + Prometheus Alert Manager +

+

+ Check your clusters health directly from the homepage ensuring + high availability and constant uptime +

+
+
+ dashboard +
+
+
+
+
+ ); +} diff --git a/app/_components/landingPage/footerSection.tsx b/app/_components/landingPage/footerSection.tsx new file mode 100644 index 0000000..b7b1f9a --- /dev/null +++ b/app/_components/landingPage/footerSection.tsx @@ -0,0 +1,91 @@ +import Link from "next/link"; +import Image from "next/image"; + +export default function Footer() { + return ( + <> +
+
+
+
+ + NotiKube Logo + + NotiKube + + +
+
+
+ {" "} + {/* Adjust column span for wider desktop view */} +

+ Follow us +

+ +
+
+
+
+
+ + © 2023{" "} + + NotiKube™ + + . All Rights Reserved. + +
+ {/* Github Logo */} + + GitHub Logo + + + LinkedIn Logo + +
+
+
+
+ + ); +} diff --git a/app/_components/landingPage/mediumSection.tsx b/app/_components/landingPage/mediumSection.tsx new file mode 100644 index 0000000..567bfb5 --- /dev/null +++ b/app/_components/landingPage/mediumSection.tsx @@ -0,0 +1,43 @@ +import Link from "next/link"; + +export default function MediumArticleSection() { + return ( +
+
+
+

+ Managing Incidents for Modern-Day DevOps Teams +

+

+ NotiKube is a lightweight incident management platform that utilizes + Prometheus Alert Manager and combines traditional incident + management tools like OpsGenie and ServiceNow into a centralized + platform +

+

+ Read our Medium article to learn more about the problem we're + tackling and how we landed on the idea +

+ + Learn more + + + + +
+
+
+ ); +} diff --git a/app/_components/landingPage/navbar.tsx b/app/_components/landingPage/navbar.tsx new file mode 100644 index 0000000..8b88535 --- /dev/null +++ b/app/_components/landingPage/navbar.tsx @@ -0,0 +1,41 @@ +import Link from "next/link"; +import Image from "next/image"; + +export default function Navbar() { + return ( + <> +
+ +
+ + ); +} diff --git a/app/_components/landingPage/secondInfoSection.tsx b/app/_components/landingPage/secondInfoSection.tsx new file mode 100644 index 0000000..93a234b --- /dev/null +++ b/app/_components/landingPage/secondInfoSection.tsx @@ -0,0 +1,32 @@ +import Image from "next/image"; + +export default function SecondInfoSection() { + return ( +
+
+
+ incidents +
+
+

+ Track Your Incidents +

+

+ View details for each incident through our intuitive dashboard +

+

+ Our application uses the Prometheus API to provide real-time + alerting and incident management. Track your most critical alerts + and see what priorities they are +

+
+
+
+ ); +} diff --git a/app/_components/landingPage/teamInfoSection.tsx b/app/_components/landingPage/teamInfoSection.tsx new file mode 100644 index 0000000..8d541da --- /dev/null +++ b/app/_components/landingPage/teamInfoSection.tsx @@ -0,0 +1,66 @@ +import TeamMember from "./TeamMember"; + +export default function TeamInfoSection() { + const teamMembers = [ + { + name: "Jesse Chou", + avatarSrc: + "https://ca.slack-edge.com/T04VCTELHPX-U05BZ4FFHHV-e6b25299e14c-512", + githubLink: "https://github.com/jesse-chou/", + linkedinLink: "https://www.linkedin.com/in/jesse-chou/", + role: "Software Engineer", + }, + { + name: "Derek Coughlan", + avatarSrc: + "https://ca.slack-edge.com/T04VCTELHPX-U057QU0L9LG-10e0ff54b26c-512", + githubLink: "https://github.com/derekcoughlan", + linkedinLink: "https://www.linkedin.com/in/derekcoughlan/", + role: "Software Engineer", + }, + { + name: "Emmanuel Ikhalea", + avatarSrc: + "https://ca.slack-edge.com/T04VCTELHPX-U05AZ9KR9A9-8ffcc9718ee7-512", + githubLink: "https://github.com/DeveloperIkhalea", + linkedinLink: "https://www.linkedin.com/in/emmanuel-ikhalea-222781178/", + role: "Software Engineer", + }, + { + name: "Apiraam Selvabaskaran", + avatarSrc: + "https://ca.slack-edge.com/T04VCTELHPX-U050F0D9T71-9da884f90254-512", + githubLink: "https://github.com/apiraam96", + linkedinLink: + "https://www.linkedin.com/in/apiraam-selvabaskaran/", + role: "Software Engineer", + }, + { + name: "Dane Smith", + avatarSrc: + "https://ca.slack-edge.com/T04VCTELHPX-U05C5P6M935-aa90bb8223ce-512", + githubLink: "https://github.com/dudemandane", + linkedinLink: "https://www.linkedin.com/in/danealexandersmith/", + role: "Software Engineer", + }, + ]; + return ( +
+
+
+

+ Our Team +

+

+ NotiKube is a distributed team with engineers from around the world +

+
+
+ {teamMembers.map((member, index) => ( + + ))} +
+
+
+ ); +} diff --git a/app/_components/landingPage/titleSection.tsx b/app/_components/landingPage/titleSection.tsx new file mode 100644 index 0000000..1b93152 --- /dev/null +++ b/app/_components/landingPage/titleSection.tsx @@ -0,0 +1,48 @@ +import Link from "next/link"; +import Image from "next/image"; + +export default function TitleSection() { + return ( +
+
+
+
+

+ Incident Management for Kubernetes Clusters +

+

+ Stay on top of critical alerts with NotiKube's intuitive + dashboard for complete incident lifecycle management. +

+ + Get Started + + + + +
+
+ mockup +
+
+
+
+ ); +} diff --git a/app/_components/userPreferences/DeleteAccount.tsx b/app/_components/userPreferences/DeleteAccount.tsx new file mode 100644 index 0000000..d4363b3 --- /dev/null +++ b/app/_components/userPreferences/DeleteAccount.tsx @@ -0,0 +1,34 @@ +'use client' + +import * as React from 'react'; +import { useRouter } from 'next/navigation'; +import { signOut } from "next-auth/react"; + +const DeleteAccount = (props: {user_id: (string | undefined)}) => { + + const router = useRouter(); + + function handleClick() { + if (confirm('Deleting user account will remove all data associated with this account. This action cannot be undone. Do you wish to continue deleting your account?')) { + fetch('/api/updateUser/removeAccount', { + method: 'POST', + body: JSON.stringify({user_id: props.user_id}) + }) + .then((res) => { + return res.json(); + }) + .then((res) => console.log(res)) + signOut(); + } + } + + + return ( + + ) + + + +} + +export default DeleteAccount; \ No newline at end of file diff --git a/app/_components/userPreferences/EmailSwitch.tsx b/app/_components/userPreferences/EmailSwitch.tsx new file mode 100644 index 0000000..19e0bce --- /dev/null +++ b/app/_components/userPreferences/EmailSwitch.tsx @@ -0,0 +1,54 @@ +'use client' + +import * as React from 'react'; +import Switch from '@mui/material/Switch'; +import { useState } from 'react'; +import { FormControlLabel, Typography } from '@mui/material'; +import { alpha, styled } from '@mui/material/styles'; +import { red } from '@mui/material/colors'; + + + +const EmailSwitch = (props: {user_id: (string | undefined), status: (boolean | undefined)}) => { + + const [checked, setChecked] = useState<(boolean | undefined)>(props.status); + + const handleChange = (event: React.ChangeEvent) => { + setChecked(event.target.checked) + fetch('/api/updateUser/email', { + method:'POST', + body: JSON.stringify({user_id: props.user_id, status: checked}) + }) + .then((res) => { + return res.json() + }) + .then((res) => console.log(res)) + }; + + const RedSwitch = styled(Switch)(({ theme }) => ({ + '& .MuiSwitch-switchBase.Mui-checked': { + color: red[600], + '&:hover': { + backgroundColor: alpha(red[600], theme.palette.action.hoverOpacity), + }, + }, + '& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': { + backgroundColor: red[600], + }, + })); + + + + return ( + } + label={Email Notifications} + /> + ) +} + +export default EmailSwitch; \ No newline at end of file diff --git a/app/api/addMember/[urlParams]/route.ts b/app/api/addMember/[urlParams]/route.ts new file mode 100644 index 0000000..a3f599b --- /dev/null +++ b/app/api/addMember/[urlParams]/route.ts @@ -0,0 +1,45 @@ +import { NextRequest, NextResponse } from "next/server"; +import sql from '../../../utils/db'; +import { User, Incident } from '../../../../types/definitions'; + +const CryptoJS = require('crypto-js'); + + +export async function GET(request: NextRequest, {params}: {params: {urlParams: any}}) { + + let urlStart = ''; + process.env.NODE_ENV === 'development' ? urlStart = 'http://localhost:3000' : urlStart = 'http://www.notikube.com' + + let { urlParams } = params; + let cipherText: string = urlParams.replaceAll('notikube', '/') + let bytes = CryptoJS.AES.decrypt(cipherText, process.env.CIPHER_KEY); + let decrypt = bytes.toString(CryptoJS.enc.Utf8); + + let idValues: Array = decrypt.split('$') + + try { + + const user: User[] = await sql ` + select * from users where email=${idValues[1]}; + ` + + if (user[0] !== undefined) { + await sql ` + update users set cluster_id=${idValues[0]}, cluster_owner=FALSE where email=${idValues[1]}; + ` + return NextResponse.redirect(urlStart + '/auth/login'); + } else { + const redirectURL = urlStart + '/auth/signup' + return NextResponse.json({ + Error: `Cannot find NotiKube user account for ${idValues[1]}. Please register a NotiKube user account at ${redirectURL} and try again.` + }); + }; + } catch(err) { + const redirectURL = urlStart + '/auth/login' + console.log('error', err) + return NextResponse.json({ + message: `Error adding user to cluster. Please ensure you have a valid user account and log in here: ${redirectURL}` + }); + } + +}; \ No newline at end of file diff --git a/app/api/addMember/send-invite/route.ts b/app/api/addMember/send-invite/route.ts new file mode 100644 index 0000000..efc8956 --- /dev/null +++ b/app/api/addMember/send-invite/route.ts @@ -0,0 +1,61 @@ +import { NextRequest, NextResponse } from "next/server"; +import sql from '../../../utils/db'; +import { User } from '../../../../types/definitions'; +import { sendMail } from '../../../../service/mailService'; +import { getServerSession } from "next-auth"; + + +const CryptoJS = require('crypto-js'); + + +export async function POST(request: NextRequest, response: NextResponse) { + + const data: {email: string} = await request.json(); + + const session = await getServerSession(); + const userEmail: (string | undefined | null) = session?.user.email; + + try { + + if (userEmail !== undefined) { + + const user: User[] = await sql` + select name, cluster_id, cluster_name from users join clusters using (cluster_id) where email=${userEmail} +` + const urlParams: string = user[0].cluster_id + '$' + data.email; + let cipherText: any = CryptoJS.AES.encrypt(urlParams, process.env.CIPHER_KEY).toString(); + const encodedURL: string = cipherText.replaceAll('/', 'notikube'); + + let urlStart = ''; + process.env.NODE_ENV === 'development' ? urlStart = 'http://localhost:3000' : urlStart = 'http://www.notikube.com' + let redirectURL = urlStart + '/auth/signup'; + let addUserURL = urlStart + `/api/addMember/${encodedURL}` + + + if (data.email) { + console.log('sending email to: ', data.email) + sendMail( + data.email, + `NotiKube: Invitation to Join ${user[0].name}\'s Cluster`, + `You have been invited to join ${user[0].name}\'s NotiKube team: ${user[0].cluster_name}.

+ + If you already have a NotiKube user account, click here to connect to ${user[0].cluster_name}.

+ + Important Note: NotiKube users can only be associated with one cluster. If you are already associated with a NotiKube cluster, clicking the link above will sever your ability to view and manage incidents for your current cluster. Only click the confirmation link if you wish to change your NotiKube cluster permissions to view and manage ${user[0].cluster_name}.

+ + If you\'re not already a registered user, click here to create a NotiKube account, then click the link above to connect to ${user[0].cluster_name}.` + + ) + } + } + + return NextResponse.json({message: 'successfullly invited user'}); + +} catch(err) { + console.log('error', err) + return NextResponse.json({ + message: `Error inviting new user to cluster. Please ensure you entered a valid email address and try again.` + }); +} + +}; diff --git a/app/api/alertmanager/route 2.ts b/app/api/alertmanager/route 2.ts new file mode 100644 index 0000000..61b1c44 --- /dev/null +++ b/app/api/alertmanager/route 2.ts @@ -0,0 +1,56 @@ +import { NextRequest, NextResponse } from "next/server"; +import sql from "../../utils/db"; +import { Incident } from "../../../types/definitions"; +import { numOfReadyNodes, numOfUnhealthyNodes, numOfReadyPods, numOfUnhealthyPods, clusterMemoryUsage, clusterCpuUsage10mAvg } from "../../lib/queries"; + +export async function POST(req: NextRequest) { + try { + + // Retrieving email from webhook URL query + const searchParams = req.nextUrl.searchParams + const email = searchParams.get("email") + console.log("email: ", email) + const alertObject = await req.json() + console.log(alertObject) + + // Search and grab clusterID from the db based on the user email + const clusterIdQueryResult = await sql`SELECT cluster_id FROM users WHERE email=${email};` + const clusterId = clusterIdQueryResult[0]['cluster_id'] + console.log(clusterId) + + // Retrieving relevant incident details + const incident_title = alertObject['commonLabels']['alertname'] + const priority_level = alertObject['commonLabels']['severity'] + const incident_type = alertObject['commonAnnotations']['summary'] + const incident_description = alertObject['commonAnnotations']['description'] + const incident_status = 'Open' + + // Insert new incident into the db + const newIncident = await sql`INSERT INTO incidents (cluster_id, incident_type, description, priority_level, incident_title, incident_status) VALUES (${clusterId}, ${incident_type}, ${incident_description}, ${priority_level}, ${incident_title}, ${incident_status}) RETURNING *;` + console.log('newIncident:', newIncident) + const newIncidentId = newIncident[0]['incident_id'] + + // Retrieving IP Address from db for PromQL queries for the metric snapshot + const clusterInformation = await sql`SELECT * FROM clusters WHERE cluster_id=${clusterId}` + const ipAddress = clusterInformation[0]['cluster_ip'] + + // Grabbing metrics snapshots with PromQL queries + const numOfReadyNodesResult = await numOfReadyNodes(ipAddress) + const numOfUnhealthyNodesResult = await numOfUnhealthyNodes(ipAddress) + const numOfReadyPodsResult = await numOfReadyPods(ipAddress) + const numOfUnhealthyPodsResult = await numOfUnhealthyPods(ipAddress) + const clusterMemoryUsageResult = await clusterMemoryUsage(ipAddress) + const clusterCpuUsage10mAvgResult = await clusterCpuUsage10mAvg(ipAddress) + + //Adding the metrics to the db + await sql`INSERT INTO metric_data (incident_id, ready_nodes, unhealthy_nodes, ready_pods, unhealthy_pods, cluster_memory_usage, cluster_cpu_usage) VALUES (${newIncidentId}, ${numOfReadyNodesResult}, ${numOfUnhealthyNodesResult}, ${numOfReadyPodsResult}, ${numOfUnhealthyPodsResult}, ${clusterMemoryUsageResult}, ${clusterCpuUsage10mAvgResult})` + + return NextResponse.json({status: 200}) + + } + + catch(e) { + return NextResponse.json({message: 'Error occured while storing alert in db: ', e}, {status: 500}) + } + +} \ No newline at end of file diff --git a/app/api/alertmanager/route.ts b/app/api/alertmanager/route.ts new file mode 100644 index 0000000..61b1c44 --- /dev/null +++ b/app/api/alertmanager/route.ts @@ -0,0 +1,56 @@ +import { NextRequest, NextResponse } from "next/server"; +import sql from "../../utils/db"; +import { Incident } from "../../../types/definitions"; +import { numOfReadyNodes, numOfUnhealthyNodes, numOfReadyPods, numOfUnhealthyPods, clusterMemoryUsage, clusterCpuUsage10mAvg } from "../../lib/queries"; + +export async function POST(req: NextRequest) { + try { + + // Retrieving email from webhook URL query + const searchParams = req.nextUrl.searchParams + const email = searchParams.get("email") + console.log("email: ", email) + const alertObject = await req.json() + console.log(alertObject) + + // Search and grab clusterID from the db based on the user email + const clusterIdQueryResult = await sql`SELECT cluster_id FROM users WHERE email=${email};` + const clusterId = clusterIdQueryResult[0]['cluster_id'] + console.log(clusterId) + + // Retrieving relevant incident details + const incident_title = alertObject['commonLabels']['alertname'] + const priority_level = alertObject['commonLabels']['severity'] + const incident_type = alertObject['commonAnnotations']['summary'] + const incident_description = alertObject['commonAnnotations']['description'] + const incident_status = 'Open' + + // Insert new incident into the db + const newIncident = await sql`INSERT INTO incidents (cluster_id, incident_type, description, priority_level, incident_title, incident_status) VALUES (${clusterId}, ${incident_type}, ${incident_description}, ${priority_level}, ${incident_title}, ${incident_status}) RETURNING *;` + console.log('newIncident:', newIncident) + const newIncidentId = newIncident[0]['incident_id'] + + // Retrieving IP Address from db for PromQL queries for the metric snapshot + const clusterInformation = await sql`SELECT * FROM clusters WHERE cluster_id=${clusterId}` + const ipAddress = clusterInformation[0]['cluster_ip'] + + // Grabbing metrics snapshots with PromQL queries + const numOfReadyNodesResult = await numOfReadyNodes(ipAddress) + const numOfUnhealthyNodesResult = await numOfUnhealthyNodes(ipAddress) + const numOfReadyPodsResult = await numOfReadyPods(ipAddress) + const numOfUnhealthyPodsResult = await numOfUnhealthyPods(ipAddress) + const clusterMemoryUsageResult = await clusterMemoryUsage(ipAddress) + const clusterCpuUsage10mAvgResult = await clusterCpuUsage10mAvg(ipAddress) + + //Adding the metrics to the db + await sql`INSERT INTO metric_data (incident_id, ready_nodes, unhealthy_nodes, ready_pods, unhealthy_pods, cluster_memory_usage, cluster_cpu_usage) VALUES (${newIncidentId}, ${numOfReadyNodesResult}, ${numOfUnhealthyNodesResult}, ${numOfReadyPodsResult}, ${numOfUnhealthyPodsResult}, ${clusterMemoryUsageResult}, ${clusterCpuUsage10mAvgResult})` + + return NextResponse.json({status: 200}) + + } + + catch(e) { + return NextResponse.json({message: 'Error occured while storing alert in db: ', e}, {status: 500}) + } + +} \ No newline at end of file diff --git a/app/api/auth/[...nextauth]/authOptions.ts b/app/api/auth/[...nextauth]/authOptions.ts new file mode 100644 index 0000000..4561c3f --- /dev/null +++ b/app/api/auth/[...nextauth]/authOptions.ts @@ -0,0 +1,91 @@ +import { NextAuthOptions } from 'next-auth' +import CredentialsProvider from 'next-auth/providers/credentials' +import GithubProvider from 'next-auth/providers/github' +import sql from '../../../utils/db' +import bcrypt from 'bcrypt' + +export const authOptions: NextAuthOptions = { + + providers: [ + GithubProvider({ + clientId: process.env.GITHUB_ID as string, + clientSecret: process.env.GITHUB_SECRET as string + }), + + CredentialsProvider({ + name: 'credentials', + credentials: {}, + + async authorize(credentials): Promise { + const {email, password} = credentials as {email: string, password: string} + + try { + //Checking to see if the user exists + let res = await sql`SELECT * FROM users WHERE email=${email}` + if (!res.length) { + return null + } + //Checking to see if the password is correct + const passwordsMatch = await bcrypt.compare(password, res[0].password) + if (!passwordsMatch) { + return null + } + return res[0] + } + catch(e) { + console.log(e) + return null + } + }, + }) + ], + session: { + strategy: 'jwt', + maxAge: 2 * 60 * 60 + }, + secret: process.env.NEXTAUTH_SECRET, + pages: { + signIn:'/auth/login' + }, + callbacks: { + async session({session, user}): Promise { + if (!session) return + try { + //Identifying the userid in the db to put in the session.user object + let res = await sql`SELECT * FROM users WHERE email=${session.user.email!}` + + session.user.userid = res[0].user_id + return session + } + catch(e) { + console.log(e) + return null + } + }, + + async signIn({profile, account}): Promise { + //Checking to see if the user is using the Github oauth or local auth + if (account?.provider == 'credentials') { + return true + } + console.log(profile) + + try { + //Checking to see if the user exists + //Typescript note: 'profile?.email!' has an explanation mark to ensure that this value will not be null + let res = await sql`SELECT * FROM users WHERE email=${profile?.email!}` + if (!res.length) { + //Inputing user into the db + await sql`INSERT INTO users (name, email) VALUES (${profile?.name as string}, ${profile?.email as string});` + } + + return true + } + catch(e) { + console.log(e) + return + } + + } + } +} \ No newline at end of file diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..86f2a5c --- /dev/null +++ b/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,6 @@ +import { authOptions } from "./authOptions" +import NextAuth from "next-auth" + +const handler = NextAuth(authOptions) + +export {handler as GET, handler as POST} \ No newline at end of file diff --git a/app/api/connect-cluster/route.ts b/app/api/connect-cluster/route.ts new file mode 100644 index 0000000..5620908 --- /dev/null +++ b/app/api/connect-cluster/route.ts @@ -0,0 +1,60 @@ +import { NextResponse} from 'next/server'; +import sql from '../../utils/db'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '../auth/[...nextauth]/authOptions' +import { activeCluster } from '../../lib/queries'; + +export async function POST(req: any) { + + // This grabs the user id from the current logged in user + const session = await getServerSession(authOptions); + const user_id = session?.user.userid + + // This is the cluster name and cluster ip that is sent from the request modal + const { clusterName, clusterIp } = await req.json() + + try { + // This checks whether the logged in user has an associated cluster_id + if (user_id !== undefined) { + if (user_id !== undefined) { + const verifyUserCluster = await sql`SELECT cluster_id FROM users WHERE user_id = ${user_id}` + + if (verifyUserCluster[0].cluster_id !== null) { + return NextResponse.json({ error: 'Error: You already have a cluster associated with your account!' }, { status: 400 }) + } + } + } + + // This checks whether the submitted cluster_ip from the popup modal already exists + const verifyClusterIp = await sql`SELECT cluster_ip from clusters WHERE cluster_ip = ${clusterIp}` + + if (verifyClusterIp.length !== 0) { + console.log('clusterip already exists!') + return NextResponse.json({ error: 'Error: This clusterIp already exists!' }, { status: 400 }) + } + + // This checks whether the provided clusterip points to an active cluster + const activeClusterResults = await activeCluster(clusterIp) + if (!activeClusterResults) { + return NextResponse.json({ error: 'Error: This cluster is not active!' }, { status: 400 }) + } + + // This grabs the cluster_id and sends it back to the user so that cluster_id is associated with the user + const grabClusterId = await sql` + INSERT INTO clusters (cluster_name, cluster_ip) VALUES (${clusterName}, ${clusterIp}) RETURNING cluster_id` + + const clusterId = grabClusterId[0].cluster_id + + // This inserts into users cluster_ip + if (user_id !== undefined) { + const addClusterToUser = await sql` + UPDATE users SET cluster_id=${clusterId}, cluster_owner=TRUE WHERE user_id=${user_id}` + } + + // If all the checks have passed, return true boolean + return NextResponse.json({newCluster: true}); + } catch (err) { + console.error(`Error inserting data:`, err) + return NextResponse.json({newCluster: false}) + } +} \ No newline at end of file diff --git a/app/api/get-cluster/route.ts b/app/api/get-cluster/route.ts new file mode 100644 index 0000000..035ad9e --- /dev/null +++ b/app/api/get-cluster/route.ts @@ -0,0 +1,28 @@ +import { NextResponse } from "next/server"; +import sql from "../../utils/db"; +import { getServerSession } from "next-auth"; +import { authOptions } from "../auth/[...nextauth]/authOptions"; + +export async function GET(req: any) { + // This grabs the user id from the current logged in user + const session = await getServerSession(authOptions); + const user_id = session?.user.userid; + + try { + if (user_id !== undefined) { + // Select cluster_name and cluster_up based on the users_id + let clusterData = await sql`SELECT cluster_name, cluster_ip + FROM clusters + JOIN users ON users.cluster_id = clusters.cluster_id + WHERE users.user_id = ${user_id};` + if (clusterData.length === 0) { + return NextResponse.json({error: 'User Data Not Found'}, {status: 400}) + } + const { cluster_ip, cluster_name }= clusterData[0] + // console.log(cluster_ip, cluster_name) + return NextResponse.json({cluster_ip, cluster_name}) + } + } catch (err) { + console.error("Error. Not able to get user's clusters", err); + } +} diff --git a/app/api/getUser/[userID]/route.ts b/app/api/getUser/[userID]/route.ts new file mode 100644 index 0000000..2864ec5 --- /dev/null +++ b/app/api/getUser/[userID]/route.ts @@ -0,0 +1,23 @@ +import { NextRequest, NextResponse} from 'next/server'; +import sql from '../../../utils/db'; +import { User } from '../../../../types/definitions'; + +export async function GET(request: NextRequest, {params}: {params: {userID: string}}) { + + const { userID } = params; + + try { + + const userData: User[] = await sql` + select * from users where user_id=${userID} + ` + + return NextResponse.json(userData[0]); + + } catch(err) { + console.log('error', err) + return NextResponse.json({ + message: `Error getting user data.` + }); + } +}; \ No newline at end of file diff --git a/app/api/incidents/getAlerts/[user_id]/route.ts b/app/api/incidents/getAlerts/[user_id]/route.ts new file mode 100644 index 0000000..efa7762 --- /dev/null +++ b/app/api/incidents/getAlerts/[user_id]/route.ts @@ -0,0 +1,34 @@ +import { NextRequest, NextResponse} from 'next/server'; +import sql from '../../../../utils/db'; +import { TableData, ClusterRes } from '../../../../../types/definitions'; + + + +export async function GET(request: NextRequest, {params}: {params: {user_id: string}}) { + + console.log('envorionment', process.env.NODE_ENV) + + const { user_id } = params; + + try { + + const cluster_id: ClusterRes[] = await sql` + select cluster_id from users where user_id=${user_id} + ` + const incidents: TableData[] = await sql` + select * from incidents left join clusters using (cluster_id) where cluster_id=${cluster_id[0].cluster_id} + ` + const members: Array = await sql` + select name, email from users where cluster_id=${cluster_id[0].cluster_id} + ` + + return NextResponse.json([incidents, members]); + + } catch(err) { + console.log('error', err) + return NextResponse.json({ + message: `Error retrieving alerts.` + }); +} + +}; diff --git a/app/api/incidents/incidentDetails/[incident_id]/route.ts b/app/api/incidents/incidentDetails/[incident_id]/route.ts new file mode 100644 index 0000000..94c432f --- /dev/null +++ b/app/api/incidents/incidentDetails/[incident_id]/route.ts @@ -0,0 +1,35 @@ +import { NextRequest, NextResponse} from 'next/server'; +import sql from '../../../../utils/db'; +import { Incident } from '../../../../../types/definitions'; + + +export async function GET(request: NextRequest, {params}: {params: {incident_id: string}}) { + + const { incident_id } = params; + + try { + + const incidentDetails: Incident[] = await sql` + select * from incidents where incident_id=${incident_id} + ` + const clusterName: Incident[] = await sql` + select cluster_name, cluster_ip from clusters where cluster_id=${incidentDetails[0].cluster_id} + ` + const members: [{name:string, email:string}] = await sql` + select name, email from users where cluster_id=${incidentDetails[0].cluster_id} + ` + incidentDetails[0].cluster_name = clusterName[0].cluster_name; + incidentDetails[0].cluster_ip = clusterName[0].cluster_ip; + + const snapshotDataRow = await sql`SELECT * FROM metric_data WHERE incident_id=${incident_id}` + const snapshotData = snapshotDataRow[0] + + return NextResponse.json({incidentDetails: incidentDetails, snapshotData: snapshotData, memberProps: members}); + + } catch(err) { + console.log('error', err) + return NextResponse.json({ + message: `Error retrieving incident details.` + }); + } +}; diff --git a/app/api/incidents/update/route.ts b/app/api/incidents/update/route.ts new file mode 100644 index 0000000..55e4989 --- /dev/null +++ b/app/api/incidents/update/route.ts @@ -0,0 +1,56 @@ +import { NextRequest, NextResponse} from 'next/server'; +import sql from '../../../utils/db'; +import {sendMail} from '../../../../service/mailService'; +import { Incident, Email } from '../../../../types/definitions'; + + +export async function POST(req: NextRequest) { + + let redirectURL = ''; + process.env.NODE_ENV === 'development' ? redirectURL = 'http://localhost:3000/auth/login' : + redirectURL = 'http://www.notikube.com/auth/login' + + const incident: Incident = await req.json(); + + try { + + const assignedTo: Incident[] = await sql` + select incident_assigned_to from incidents where incident_id=${incident.incident_id} + ` + const email: Email[] = await sql` + select email, email_status from users where name=${incident.incident_assigned_to} AND cluster_id=${incident.cluster_id} + ` + if (assignedTo[0].incident_assigned_to !== incident.incident_assigned_to) { + + if (email[0].email && email[0].email_status) { + + console.log('sending mail to:', email[0].email) + + sendMail( + email[0].email, + 'NotiKube: You have been assigned a new incident', + `You have been assigned a new NotiKube incident: ${incident.incident_title}. +

+ Please log in to your NotiKube account and navigate to the Incidents page for more details.` + ) + } +} + + async function updateUser(incident: Incident) { + await sql` + update incidents SET incident_type=${incident.incident_type}, description=${incident.description}, priority_level=${incident.priority_level}, incident_title=${incident.incident_title}, incident_status=${incident.incident_status}, comment=${incident.comment}, incident_assigned_to=${incident.incident_assigned_to} where incident_id=${incident.incident_id} + ` + } + + updateUser(incident); + + return NextResponse.json(incident); + +} catch(err) { + console.log('error', err) + return NextResponse.json({ + message: `Error updating incident.` + }); +} + +}; \ No newline at end of file diff --git a/app/api/incidents/updateDetails/route.ts b/app/api/incidents/updateDetails/route.ts new file mode 100644 index 0000000..519a377 --- /dev/null +++ b/app/api/incidents/updateDetails/route.ts @@ -0,0 +1,57 @@ +import { NextRequest, NextResponse} from 'next/server'; +import sql from '../../../utils/db'; +import {sendMail} from '../../../../service/mailService'; +import { Incident, Email } from '../../../../types/definitions'; + +export async function POST(req: NextRequest, res: NextResponse) { + + let redirectURL = ''; + process.env.NODE_ENV === 'development' ? redirectURL = 'http://localhost:3000/auth/login' : + redirectURL = 'http://www.notikube.com/auth/login' + + const data = await req.json(); + + try { + + // look up who the incident was previously assigned to + const assignedTo: Incident[] = await sql` + select incident_assigned_to from incidents where incident_id=${data.incident_id} + ` + // check to see if incident has been reassigned + if (assignedTo[0].incident_assigned_to !== data.incident_assigned_to) { + + const email: Email[] = await sql` + select email, email_status from users where name=${data.incident_assigned_to} AND cluster_id=${data.cluster_id} + ` + + if (data.incident_title === undefined) data.incident_title = 'Unnamed Cluster'; + + if (email[0].email && email[0].email_status ) { + + console.log('sending mail to:', email[0].email) + + sendMail( + email[0].email, + 'NotiKube: You have been assigned a new incident', + `You have been assigned a new NotiKube incident: ${data.incident_title}. +

+ Please log in to your NotiKube account and navigate to the Incidents page for more details.` + ) + } + } + + // update incident row in database + await sql` + update incidents SET description=${data.description}, priority_level=${data.priority_level}, incident_title=${data.incident_title}, incident_status=${data.incident_status}, comment=${data.comment}, incident_assigned_to=${data.incident_assigned_to}, incident_assigned_by=${data.incident_assigned_by}, incident_due_date=${data.incident_due_date}, incident_assigned_date=${data.incident_assigned_date} where incident_id=${data.incident_id} + ` + + return NextResponse.json({response: 'successfully updated incident'}); + + } catch(err) { + console.log('error', err) + return NextResponse.json({ + message: `Error updating incident details.` + }); +} + +}; \ No newline at end of file diff --git a/app/api/register/route.ts b/app/api/register/route.ts new file mode 100644 index 0000000..dd8478d --- /dev/null +++ b/app/api/register/route.ts @@ -0,0 +1,31 @@ +import { NextResponse } from "next/server" +import bcrypt from 'bcrypt' +import sql from '../../utils/db' +const SALT_FACTOR = 10 + +export async function POST(req: any) { + try { + const {fullName, email, password} = await req.json() + console.log('full name: ', fullName) + console.log('password: ', password) + console.log('email: ', email) + + const result = await sql`SELECT * FROM users WHERE email=${email}`; + console.log('result', result) + + if (!result.length) { + const hashedPassword = await bcrypt.hash(password, SALT_FACTOR) + await sql`INSERT INTO users (name, email, password) VALUES (${fullName}, ${email}, ${hashedPassword})` + console.log('User created!'); + return NextResponse.json({newUser: true}); + } + + else { + console.log('User already exists!'); + return NextResponse.json({ newUser: false }); + } + } + catch(e) { + return NextResponse.json({message: 'Error occured while registering the user: ', e}, {status: 500}) + } +} \ No newline at end of file diff --git a/app/api/updateCluster/delete/[id]/route.ts b/app/api/updateCluster/delete/[id]/route.ts new file mode 100644 index 0000000..8182fd7 --- /dev/null +++ b/app/api/updateCluster/delete/[id]/route.ts @@ -0,0 +1,32 @@ +import { NextRequest, NextResponse } from "next/server"; +import sql from "../../../../utils/db"; + +export async function GET( + request: NextRequest, + { params }: { params: { id: string } } +) { + const { id } = params; + + try { + await sql` + update users set cluster_id=NULL where cluster_id=${id} + `; + await sql` + delete from incidents where cluster_id=${id} +`; + await sql` + delete from clusters where cluster_id=${id} + `; + // TO-DO: BUILD LOGIC - IF INCIDENT IS DELETE, ALSO DELETE METRIC DATA +// await sql` +// delete from metric_data_id where cluster_id=${id} +// `; + + return NextResponse.json({ message: "success" }); + } catch (err) { + console.log("error", err); + return NextResponse.json({ + message: `Error deleting cluster.`, + }); + } +} diff --git a/app/api/updateCluster/edit/route.ts b/app/api/updateCluster/edit/route.ts new file mode 100644 index 0000000..98362ec --- /dev/null +++ b/app/api/updateCluster/edit/route.ts @@ -0,0 +1,22 @@ +import { NextRequest, NextResponse} from 'next/server'; +import sql from '../../../utils/db'; + +export async function POST(req: NextRequest, res: NextResponse) { + + const data = await req.json(); + + try { + + await sql` + update clusters set cluster_name=${data.cluster_name}, cluster_ip=${data.cluster_ip} where cluster_id=${data.cluster_id} + ` + return NextResponse.json({message: 'successfully updated cluster information'}) + + } catch(err) { + console.log('error', err) + return NextResponse.json({ + message: `Error updating cluster details.` + }); +} + +} \ No newline at end of file diff --git a/app/api/updateUser/email/route.ts b/app/api/updateUser/email/route.ts new file mode 100644 index 0000000..26a17fc --- /dev/null +++ b/app/api/updateUser/email/route.ts @@ -0,0 +1,32 @@ +import { NextRequest, NextResponse } from 'next/server'; +import sql from '../../../utils/db'; + + +export async function POST(req: NextRequest, res: NextResponse) { + + let data = await req.json(); + + let emailStatus: (string | undefined) = undefined; + + if (data.status === true) { + data.status = false; + } else if (data.status === false) { + data.status = true; + } + + try { + + await sql` + update users set email_status=${data.status} where user_id=${data.user_id} + ` + + return NextResponse.json({message: 'successfully updated email preferences'}) + + } catch(err) { + console.log('error', err) + return NextResponse.json({ + message: `Error updating email preferences.` + }); +} + +}; \ No newline at end of file diff --git a/app/api/updateUser/removeAccount/route.ts b/app/api/updateUser/removeAccount/route.ts new file mode 100644 index 0000000..ba343cf --- /dev/null +++ b/app/api/updateUser/removeAccount/route.ts @@ -0,0 +1,30 @@ +import { NextRequest, NextResponse } from 'next/server'; +import sql from '../../../utils/db'; + + +export async function POST(req: NextRequest, res: NextResponse) { + + const data = await req.json(); + + try { + + const user = await sql` + delete from users where user_id=${data.user_id} returning users.name + ` + await sql` + update incidents set incident_assigned_to='' where incident_assigned_to=${user[0].name} + ` + await sql` + update incidents set incident_assigned_by='' where incident_assigned_by=${user[0].name} + ` + + return NextResponse.json({message: 'success'}) + + } catch(err) { + console.log('error', err) + return NextResponse.json({ + message: `Error removing user account.` + }); +} + +}; \ No newline at end of file diff --git a/app/api/updateUser/removeCluster/[id]/route.ts b/app/api/updateUser/removeCluster/[id]/route.ts new file mode 100644 index 0000000..253c1df --- /dev/null +++ b/app/api/updateUser/removeCluster/[id]/route.ts @@ -0,0 +1,26 @@ +import { NextRequest, NextResponse} from 'next/server'; +import sql from '../../../../utils/db'; + +export async function GET(request: NextRequest, {params}: {params: {id: string}}) { + + const {id} = params; + + try { + + const user = await sql` + update users set cluster_id=NULL where user_id=${id} returning users.name + ` + await sql` + update incidents set incident_assigned_to='' where incident_assigned_to=${user[0].name} + ` + + return NextResponse.json({status: 'successfully removed cluster from user table'}) + + } catch(err) { + console.log('error', err) + return NextResponse.json({ + message: `Error deleting cluster.` + }); +} + +}; diff --git a/app/auth/login/page.tsx b/app/auth/login/page.tsx index 7a5337b..27db632 100644 --- a/app/auth/login/page.tsx +++ b/app/auth/login/page.tsx @@ -1,96 +1,14 @@ -'use client'; - -import Link from "next/link"; -import { useRouter } from "next/router"; -import { useState } from "react"; -import LogInImage from '../../../public/NotiKubeLogin.svg'; -import Logo from "../../../public/logo.svg"; - -const Login = () => { - // const router = useRouter(); - const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); - - async function submit(e: Event) { - e.preventDefault(); - //Use the alert snackbar here - if (email == "" || password == "") { - return; - } - const params = { - username: email, - password: password, - }; - - const result = await fetch("/api/auth/login", { - method: "POST", - headers: { - "Content-type": "application/json", - }, - body: JSON.stringify(params), - credentials: "include", - }); - - if (result.status == 401) { - console.log("unAuthorized"); - alert("Incorrect Email/Password"); - } else { - await result.json(); - console.log(result); - "/dashboard"; - } +import LoginPage from "../../_components/LoginPage"; +import { getServerSession } from "next-auth"; +import {redirect} from "next/navigation"; +import {authOptions} from "../../api/auth/[...nextauth]/authOptions"; +import React from 'react'; + +export default async function Login() { + const session = await getServerSession(authOptions) + if (session) { + //console.log('User ID: ', session.user.userid) + redirect('/dashboard') } - - return ( -
-
-
-
- logo - NotiKube -
-

Login

- -
-
- - setEmail(e.target.value)} - /> -
-
- - setPassword(e.target.value)} - /> -
- submit(e)} - value="Submit" - /> -

- Don&apost have an account yet?{" "} - - Create an account - -

-
-
-
- -
-
- ); -}; - -export default Login; + return +} \ No newline at end of file diff --git a/app/auth/signup/page.tsx b/app/auth/signup/page.tsx index 45d1ee6..4ff300b 100644 --- a/app/auth/signup/page.tsx +++ b/app/auth/signup/page.tsx @@ -1,80 +1,14 @@ -'use client'; +import SignUpPage from "../../_components/SignUpPage"; +import { getServerSession } from "next-auth"; +import {redirect} from "next/navigation" +import {authOptions} from "../../api/auth/[...nextauth]/authOptions" -import Link from 'next/link'; -import { useState } from 'react'; -// import { useNavigate } from 'react-router-dom'; -import SignUpImage from '../../../public/NotiKubeSignUp.svg' -import Logo from '../../../public/logo.svg' - -const Signup = () => { - - // const navigate = useNavigate(); - - const [fullName, setFullName] = useState(''); - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - - function submit(e: Event) { - e.preventDefault(); - if (fullName == '' || email == '' || password == '') { - return; - } - const params = { - fullName: fullName, - email: email, - password: password - }; - - fetch('/api/auth/signup', { - method: 'POST', - headers: { - 'Content-type': 'application/json' - }, - body: JSON.stringify(params) - }) - .then(res => res.json()) - .then(data => { - if (data.newUser) { - alert('Account created!'); - // navigate('/login'); - } - else { - alert('User already exists'); - } - }); +export default async function SignUp() { + const session = await getServerSession(authOptions) + if (session) { + redirect('/dashboard/incidents'); } - - return ( -
-
-
-
- - NotiKube -
-

Sign Up

-
- - setFullName(e.target.value)} /> -
-
- - setEmail(e.target.value)} /> -
-
- - setPassword(e.target.value)} /> -
- {/* submit(e)} /> */} -

Already have an account? Log in

-
-
-
- -
-
- ); -}; - -export default Signup; \ No newline at end of file + + return +} \ No newline at end of file diff --git a/app/dashboard/_configurations/page.tsx b/app/dashboard/_configurations/page.tsx new file mode 100644 index 0000000..5484e20 --- /dev/null +++ b/app/dashboard/_configurations/page.tsx @@ -0,0 +1,13 @@ +'use client' + +import React from 'react'; + +export default function Configurations() { + return ( + <> +

+ This is the Configurations Page +

+ + ) +} \ No newline at end of file diff --git a/app/dashboard/connect-cluster/page.tsx b/app/dashboard/connect-cluster/page.tsx new file mode 100644 index 0000000..3d114f7 --- /dev/null +++ b/app/dashboard/connect-cluster/page.tsx @@ -0,0 +1,253 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import {useRouter, redirect} from 'next/navigation'; +import { useSession } from 'next-auth/react'; +// UserClusters is the table component that populates the associated cluster with the user +import UserClusters from "../../_components/ConnectClusterPage/UserClusters"; +// ConnectClusterModal is the popup modal that appears when you press 'Add New Cluster' +import ConnectClusterModal from "../../_components/ConnectClusterPage/ConnectClusterModal"; +import { User } from '../../../types/definitions'; +import EditCluster from '../../_components/ConnectClusterPage/EditCluster'; +import Backdrop from '@mui/material/Backdrop'; +import CircularProgress from '@mui/material/CircularProgress'; +import AddMemberClient from "../../_components/AddMemberClient"; + +export default function ConnectCluster() { + + const router = useRouter(); + + const [isModalVisible, setModalVisible] = useState(false); + const [userData, setUserData] = useState(); + const [loading, setLoading] = useState(true); + const [clusterIp, setClusterIp] = useState("") + const [clusterName, setClusterName] = useState("") + const [userRole, setUserRole] = useState() + const [edit, setEdit] = useState('false') + + // FETCH REQUEST TO GRAB CLUSTER ASSOCIATED WITH USER + async function getClusters() { + try { + const response = await fetch("/api/get-cluster"); + if (!response.ok) { + throw new Error("Failed to fetch clusters"); + } + const data = await response.json(); + setClusterName(data.cluster_name) + setClusterIp(data.cluster_ip) + } catch (err) { + console.log("Error fetching user's cluster", err); + } + } + + // grab user_id from session token + const session = useSession().data; + const userId = session?.user?.userid; + + // fetch user data and set it in state + async function fetchUser(userId: (string | undefined)) { + if (userId !== undefined) { + let res = await fetch(`/api/getUser/${userId}`) + const data: User = await res.json(); + setUserData(data); + setLoading(false); + } + } + + // get cluster data and user role + useEffect(() => { + if (userId != undefined) { + getClusters(); + fetchUser(userId) + if (userData?.cluster_owner === true) { + setUserRole('Owner'); + } else if (userData?.cluster_owner === false) { + setUserRole('Member') + } else { + setUserRole('') + } + }}, [userId, userData?.cluster_owner]) + + + // function to toggleModal with a warning message (if user with existing cluster clicks add cluster) + const initiateAdd = () => { + if (confirm('Adding a new cluster will remove the existing cluster. All cluster members will lose access to the existing cluster and must be added to the new cluster.')) { + setModalVisible(!isModalVisible); + } + }; + + // function to toggleModal without warning message (if user doesn't have existing cluster) + const toggleModal = () => { + setModalVisible(!isModalVisible); + }; + + // function to switch between edit component and display component + const changeEdit = () => { + if (edit === 'true') { + setEdit('false') + } else { + setEdit('true') + } + }; + + // function to update the state of cluster details when they're edited + const changeCluster = (name: string, ip: string) => { + setClusterName(name); + setClusterIp(ip); + } + + // function to handle the delete cluster button + const deleteCluster = () => { + if (confirm('Deleting this cluster will remove all previous cluster incidents and all members will lose access to the cluster. Are you sure you want to delete this cluster?')) { + fetch(`/api/updateCluster/delete/${userData?.cluster_id}`) + let newUserData = JSON.parse(JSON.stringify(userData)) + newUserData.cluster_id = null; + setUserData(newUserData) + } +} + + const removeCluster = () => { + + if (confirm('Removing this cluster will revoke your access to all cluster incidents and details. To regain access to incidents and cluster details, you must be invited to rejoin the cluster by the cluster owner. Are you sure you want to remove this cluster?')) { + fetch(`/api/updateUser/removeCluster/${userId}`) + alert('Cluster removed') + window.location.reload(); + } + } + + while (loading) { + return ( +
+ theme.zIndex.drawer + 1 }} + open={true} + > + + +
+ )} + +// if the user is not associated with a cluster, render instructions and the add cluster form + if (userData?.cluster_id === null) { + + return ( +
+

+ Add cluster details to start using NotiKube: +

+ {isModalVisible && ( +
+ )} + + {/* Modal That Appears When you Click + Add New Cluster */} + + +
+ ) + + // if the user is an owner, and they haven't clicked the edit button, render cluster details with edit fucntionality + } else if (userRole === 'Owner' && edit === 'false') { + + return ( +
+
+

+ Your Cluster +

+ +
+

+

+ + {/* If the Modal is Visible, grey out the background */} + {isModalVisible && ( +
+ )} + + {/* Modal That Appears When you Click + Add New Cluster */} + + + {/* Table for Clusters associated with Users */} + +
+ + +
+ +
+ ); + +// if the user is an owner and they have clicked edit, render the edit form +} else if (userRole === 'Owner' && edit) { + + return ( +
+
+

+ Your Cluster +

+
+ + +
+ ) + +// if the user is a member, render the cluster details with no edit buttons +} else if (userRole === 'Member') { + return ( + +
+
+

+ Your Cluster +

+ +
+

+ +
+

* Cluster members cannot make changes to cluster name or cluster IP. Only owners can edit cluster details. To be removed from this cluster click here.

+

+
+
+ + ) +} + +} + diff --git a/app/dashboard/connectCluster/page.tsx b/app/dashboard/connectCluster/page.tsx deleted file mode 100644 index 901f91c..0000000 --- a/app/dashboard/connectCluster/page.tsx +++ /dev/null @@ -1,156 +0,0 @@ -'use client'; - -import React, { useState } from 'react'; -import Sidebar from '../../_components/Sidebar'; -// import { useNavigate } from 'react-router-dom'; -// import '../App.css'; - -export default function ConnectCluster() { - // const navigate = useNavigate(); - - const [isModalVisible, setModalVisible] = useState(false); - - const toggleModal = () => { - setModalVisible(!isModalVisible); - }; - - // This function takes the passed in form data and sends it to the server - async function newCluster(e: React.FormEvent) { - e.preventDefault(); - - // Get the values from the form fields - const clusterNameElement = document.getElementById('clusterName') as HTMLInputElement | null; - const clusterIPElement = document.getElementById('clusterIP') as HTMLInputElement | null; - - if (clusterNameElement && clusterIPElement) { - const clusterName = clusterNameElement.value; - const clusterIP = clusterIPElement.value; - - // Prepare the data to be sent - const data = { - clusterName, - clusterIP, - }; - - try { - await fetch('/api/newClusterConnection', { - method: 'POST', - body: JSON.stringify(data), - headers: { 'Content-Type': 'application/json' }, - }); - console.log('Data sent successfully'); - // navigate('/dashboard', {newIpAddress: clusterIP, newClusterName: clusterName }); - } catch (error) { - console.error('Error sending data:', error); - } - - // Close the modal after form submission - toggleModal(); - } else { - console.error('Cluster name or IP element is null.'); - } - } - - return ( -
- -
-

- Your Clusters -

- -
- {/* Modal toggle */} - - {/* Main modal */} - -
-
-
- ); -} diff --git a/app/dashboard/incident-details/[incident_id]/page.tsx b/app/dashboard/incident-details/[incident_id]/page.tsx new file mode 100644 index 0000000..536190c --- /dev/null +++ b/app/dashboard/incident-details/[incident_id]/page.tsx @@ -0,0 +1,127 @@ +'use client' + +import * as React from 'react'; +import { useState, useEffect } from 'react'; +import { Incident } from '../../../../types/definitions'; +import PermanentDetails from '../../../_components/incident-details/PermanentDetails'; +import EditDetails from '../../../_components/incident-details/EditDetails'; +import EditForm from '../../../_components/incident-details/EditForm'; +import SnapshotData from '../../../_components/incident-details/SnapshotData/SnapshotData'; +import { SnapshotDataDefinition } from '../../../../types/definitions'; +import Backdrop from '@mui/material/Backdrop'; +import CircularProgress from '@mui/material/CircularProgress'; + + +export default function IncidentDetails({params}: {params: {incident_id: any}}) { + + const {incident_id} = params; + const [incidentDetails, setIncidentDetails] = useState(); + const [snapshotData, setSnapshotData] = useState(); + const [edit, setEdit] = useState(false); + const [loading, setLoading] = useState(true); + const [members, setMembers] = useState<[{name:string, email:string}]>(); + + async function fetchIncident() { + if (incident_id !== undefined) { + let res = await fetch(`/api/incidents/incidentDetails/${incident_id}`) + if (!res.ok) { + throw new Error("Failed to fetch incident details"); + } + const data: {incidentDetails: Incident[], snapshotData: SnapshotDataDefinition, memberProps: [{name:string, email:string}]} = await res.json(); + setIncidentDetails(data.incidentDetails[0]); + setMembers(data.memberProps) + setSnapshotData(data.snapshotData); + setLoading(false); + } + }; + + useEffect(() => { + fetchIncident(); + },[]); + + //const memberArray: Array = []; + + function updateEdit(body: Incident) { + + if (incidentDetails) { + body.cluster_ip = incidentDetails.cluster_ip; + body.cluster_name = incidentDetails.cluster_name; + body.metric_data_id = incidentDetails?.metric_data_id; + } + + setIncidentDetails(body) + setEdit(false); + alert('Changes Saved'); + + }; + + while (loading) { + return ( +
+ theme.zIndex.drawer + 1 }} + open={true} + > + + +
+ )} + + while (edit === false) { + + + // if snapshot === null and edit === false, return details without snapshot at top + if (snapshotData == null) { + return ( +
+

Incident Details

+
+

{incidentDetails?.cluster_name}

+

Cluster IP Address: {incidentDetails?.cluster_ip}

+
+

+

+ + +

+ +
+ ) + } + + // if snapshot !== null and edit === false, return details with snapshot data + return ( +
+

Incident Details

+
+

{incidentDetails?.cluster_name}

+

Cluster IP Address: {incidentDetails?.cluster_ip}

+
+ +

+

+ + + +
+ ) + } + + // if edit === true, return edit form + return ( +
+

Incident Details

+
+

{incidentDetails?.cluster_name}

+

Cluster IP Address: {incidentDetails?.cluster_ip}

+
+

+

+ +

+ + +
+ ) + +}; \ No newline at end of file diff --git a/app/dashboard/incidentDetails/layout.tsx b/app/dashboard/incident-details/layout.tsx similarity index 100% rename from app/dashboard/incidentDetails/layout.tsx rename to app/dashboard/incident-details/layout.tsx diff --git a/app/dashboard/incidentDetails/[id]/page.tsx b/app/dashboard/incidentDetails/[id]/page.tsx deleted file mode 100644 index aa85dd2..0000000 --- a/app/dashboard/incidentDetails/[id]/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -export default function IncidentDetails() { - return ( -

This is the Incident Details page

- ) -} \ No newline at end of file diff --git a/app/dashboard/incidents/page.tsx b/app/dashboard/incidents/page.tsx index 1962ffe..159d0d1 100644 --- a/app/dashboard/incidents/page.tsx +++ b/app/dashboard/incidents/page.tsx @@ -1,133 +1,37 @@ -'use client'; import * as React from 'react'; -// import { -// DataGrid, -// GridCellModes, - -// } from '@mui/x-data-grid'; -import { useState, useEffect, useCallback } from 'react'; -// import Sidebar from '../../components/Sidebar'; +import Table from '../../_components/Table' +import {redirect} from 'next/navigation'; +import sql from '../../utils/db'; +import {getServerSession} from 'next-auth'; +import { authOptions } from "../../api/auth/[...nextauth]/authOptions"; -export default function Incidents() { - const [ count, setCount ] = useState(0); - const [ data, setData ] = useState([]); - const [ loading, setLoading ] = useState(false); +export default async function Incidents() { - const getData = useCallback(() => { - setLoading(true); - const URL = '/api/tableData'; - fetch(URL) - .then((res) => { - return res.json(); - }) - .then((json) => { - console.log('json', json); - setData(json); - }) - .then((json) => console.log('data', data)); - - setLoading(false); - },[data]); - useEffect(() => { - getData(); - },[]); - + const session = await getServerSession(authOptions); + const user_id = session?.user.userid; - // const updateTable = React.useCallback( - // async (newRow) => { - // const updatedRow = { ...newRow }; - // updatedRow.id = newRow.timestamp; - // fetch('/api/updateAlerts', { - // method: 'POST', - // headers: { - // 'Content-Type': 'application/json' - // }, - // body: JSON.stringify(newRow) - // }); - // console.log('updated row', updatedRow); - // return updatedRow; - // },[]); - + if (user_id !== undefined) { + const cluster_id = await sql` + select cluster_id from users where user_id=${user_id} + ` + if (cluster_id[0].cluster_id === null) redirect('/dashboard/connect-cluster') + } return ( - -
- {/* */} -
-

Incident History

-

-

- {/* data.timestamp} - rows={data} - columns={columns} - processRowUpdate={updateTable} - onProcessRowUpdateError={(() => console.log('Error processing row update'))} - onRowEditStop={(params) => { - console.log(params); - }} */} -
-
- ); -} - -const columns = [ - { - field: 'timestamp', - headerName: 'Timestamp', - width: 225, - editable: false , - headerClassName: 'column-header', - }, - { - field: 'type', - headerName: 'Type', - type: 'string', - editable: true, - align: 'left', - headerAlign: 'left', - width: 150, - headerClassName: 'column-header', - }, - { - field: 'description', - headerName: 'Description', - type: 'string', - width: 450, - editable: true, - headerClassName: 'column-header', - }, - { - field: 'priority', - headerName: 'Priority', - type: 'singleSelect', - width: 125, - editable: true, - headerClassName: 'column-header', - valueOptions: ['High', 'Med', 'Low'] - }, - { - field: 'status', - headerName: 'Status', - type: 'singleSelect', - width: 125, - editable: true, - headerClassName: 'column-header', - valueOptions: ['Open', 'Closed', 'Reassigned', 'In Progress'] - }, -]; +
+
+
+

Incident History

+
+
+
+
+ + + ) + } \ No newline at end of file diff --git a/app/dashboard/layout.tsx b/app/dashboard/layout.tsx index 761f9e9..9f98c92 100644 --- a/app/dashboard/layout.tsx +++ b/app/dashboard/layout.tsx @@ -1,15 +1,16 @@ // Components -import Sidebar from "../_components/Sidebar" +import Sidebar from "../_components/Sidebar/Sidebar"; +import React from 'react'; + +export default function Layout ({ children } : {children : React.ReactNode }) { -export default function DashboardLayout({ - children, // will be a page or nested layout -}: { - children: React.ReactNode -}) { return ( -
- - {children} -
+
+
+ +
+
{children}
+
) + } \ No newline at end of file diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index c8b9190..0afdafa 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -1,97 +1,92 @@ -'use client'; -import { useState, useEffect } from 'react'; -import { useRouter } from 'next/router'; -import Sidebar from '../_components/Sidebar'; +import HomeAlerts from '../_components/homePage/homeAlerts'; +import { ClusterHealth, NodeCPUHealth, PodHealth, PodRestartHealth, ClusterCPUMem } from '../_components/homePage/clusterMetrics'; +import ClusterDetails from '../_components/homePage/clusterDetails'; +import { clusterInfo } from '../lib/homePage/clusterInfo'; +import LoadingSpinner from '../_components/homePage/loadingSpinner'; +import { Suspense } from 'react' +import type { Metadata } from "next"; +import { redirect } from "next/navigation" +import { authOptions } from '../api/auth/[...nextauth]/authOptions'; +import { getServerSession } from 'next-auth'; +import { Tab, TabList, TabGroup, TabPanel, TabPanels, Divider } from "@tremor/react"; +import React from 'react'; -import '../globals.css' - -function Dashboard() { - // const router = useRouter(); - const [ipAddress, setIpAddress] = useState('127.0.0.1:56657'); - const [clusterName, setClusterName] = useState('Test1'); - const [name, setName] = useState('Jesse'); - const [numOfAlerts, setNumOfAlerts] = useState(12); - const [inProgress, setInProgress] = useState(3); - const [isLoaded, setIsLoaded] = useState(false); +export const metadata: Metadata = { + title: 'Dashboard', + description: 'Cluster dashboard current alerts and health visualizations' +} - // const newIpAddress = location.newIpAddress; - // const newClusterName = location.newClusterName; +export default async function Dashboard() { + const session = await getServerSession(authOptions); + let currentUserID = session?.user.userid === undefined ? null : session.user.userid; - // useEffect(() => { - // setIpAddress(newIpAddress); - // setClusterName(newClusterName); - // }, [newIpAddress, newClusterName]); + try { + const { cluster_name, cluster_ip } = await clusterInfo(currentUserID) + return ( +
+
- fetch('/api/auth/checkauth') - .then(res => res.json()) - .then(data => { - if (data.user) { - setName(data.name); - setIsLoaded(true); - } - // --LEAVE THIS COMMENTED OUT UNTIL WE ARE READY TO GO LIVE-- - // else { - // navigate('/'); - // } - }); + {/* Header */} +

Dashboard

+ }> + + +
- async function logout() { - const result = await fetch('/api/auth/logout'); - if (result.status == 200) { - // router.push('/'); - } - else { - alert('There was a problem with logging out'); - } - } + {/* Alerts */} + Alerts + }> + + - // --LEAVE THIS COMMENTED OUT UNTIL WE ARE READY TO GO LIVE-- - // if (isLoaded) { - return ( -
- -
-

Dashboard

-

{clusterName}

-

Current Grafana IP Address: {ipAddress}

-
-
- -
-

Cluster

-
-
- - -
-
- - -
-
-

Nodes

-
- - -
+ {/* Metrics */} + Metrics + +
+ + Node CPU + Cluster CPU/Mem + Pod By NameSpace + Pod Restarts + Cluster Summary + +
+ + + }> + + + + + }> + + + + + }> + + + + + }> + + + + + }> + + + + +
+
-
); - // } - // else { - // return; - // } + } + catch (error) { + console.log('Error fetching user cluster, redirecting to connect cluster:', error); + redirect('/dashboard/connect-cluster') + } } - -export default Dashboard; \ No newline at end of file diff --git a/app/dashboard/profile/page.tsx b/app/dashboard/profile/page.tsx index bbd39d5..260597d 100644 --- a/app/dashboard/profile/page.tsx +++ b/app/dashboard/profile/page.tsx @@ -1,19 +1,114 @@ -import Link from 'next/link'; -import sql from '../../utils/db'; +"use client"; +import EmailSwitch from "../../_components/userPreferences/EmailSwitch"; +import DeleteAcount from "../../_components/userPreferences/DeleteAccount"; +import { useSession } from "next-auth/react"; +import { useState, useEffect } from "react"; +import { User } from "../../../types/definitions"; +import Backdrop from "@mui/material/Backdrop"; +import CircularProgress from "@mui/material/CircularProgress"; +import React from "react"; export default function Profile() { + // get userid from session + const session = useSession().data; + const userId = session?.user?.userid; + // set state of email slider + const [checked, setChecked] = useState(); + // set loading state + const [loading, setLoading] = useState(true); + // set user details in state + const [user, setUser] = useState(); + // set state for default profile picture + const [imageUrl, setImageUrl] = useState( + "https://png2.cleanpng.com/sh/00663d74b8b97254f9a0df3226bae67f/L0KzQYm3V8IzN5R2kJH0aYP2gLBuTgV0baMyiOR4ZnnvdX65UME5NZpzReVyZ3j3Pcb6hgIua5Dzftd7ZX7mdX7smQBwNWZnTac9Y0C8SYjqgBUzNmY5TqUANUW0QYa6UsMyPmc9Sag7MUixgLBu/kisspng-user-profile-2018-in-sight-user-conference-expo-5b554c0997cce2.5463555115323166816218.png" + ); + + // if user has uploaded a profile picture, overwrite default url in state + // if (user?.profile_picture_url) setImageUrl(user.profile_picture_url) + + // read month, day, year from timestamp of account creation and then reformat it to more readable version mm//dd/yyy + let timeStamp = user?.account_created.slice(0, 10); + let month = user?.account_created.slice(5, 7); + let day = user?.account_created.slice(8, 10); + let year = user?.account_created.slice(0, 4); + timeStamp = month + "/" + day + "/" + year; + + // function to fetch user data and store it in state + async function getStatus(user_id: string | undefined) { + if (userId !== undefined) { + let res = await fetch(`/api/getUser/${userId}`); + const data: User = await res.json(); + setChecked(data.email_status); + setUser(data); + setLoading(false); + } + } + // invoke function to get user data + useEffect(() => { + if (userId !== undefined) { + getStatus(userId); + } + }, [userId]); - const name = 'derek' - const email = 'derek@test.com' + // Emmanuel, if you add your upload photo function here, then use the commented out setImageUrl function to set the response image url in state, that's all you need to do on this page + function addImage(event: React.MouseEvent) { + event.preventDefault(); + console.log("add image"); + // setImageUrl(urlFromResponse) + } - const users = sql` - insert into users (name, email) values (${name}, ${email}) - ` + // while page is loading, display gray screen with loading circle + while (loading) { + return ( +
+ theme.zIndex.drawer + 1 }} + open={true} + > + + +
+ ); + } + // when page finishes loading, return profile page with email switch and delete button components return ( <> -

This is the Profile Page

- {JSON.stringify(users)} - Go Home +
+
+
+
+ default profile image + {/*
Upload Photo
*/} +
+
+
+

+ {user?.name} +

+
+
+

+ {user?.email} +

+
+
+

+ Account created: {timeStamp} +

+
+
+
+

+
+ +
+
+ +
); -} \ No newline at end of file +} diff --git a/app/globals.css b/app/globals.css index cc7a41e..fc129e5 100644 --- a/app/globals.css +++ b/app/globals.css @@ -19,7 +19,7 @@ body { margin: 0; - display: flex; + /* display: flex; */ place-items: center; min-width: 320px; min-height: 100vh; diff --git a/app/layout.tsx b/app/layout.tsx index 79bbb21..3ea613f 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,6 +1,8 @@ import type { Metadata } from "next"; import { Inter } from "next/font/google"; -import './globals.css' +import "./globals.css"; +import { AuthProvider } from "./Providers"; +import React from 'react' const inter = Inter({ subsets: ["latin"] }); @@ -18,10 +20,10 @@ export default function RootLayout({ return ( - + - {children} + {children} ); diff --git a/app/lib/homePage/clusterInfo.tsx b/app/lib/homePage/clusterInfo.tsx new file mode 100644 index 0000000..e3d2c26 --- /dev/null +++ b/app/lib/homePage/clusterInfo.tsx @@ -0,0 +1,17 @@ +import sql from "../../utils/db"; +//Opting out of static rending for dynamic (no caching) @https://nextjs.org/docs/app/api-reference/functions/unstable_noStore +import { unstable_noStore as noStore } from 'next/cache'; +import { Cluster } from '../../../types/definitions' + +//return cluster info +export async function clusterInfo(userid: string | null) { + noStore(); + try{ + const result = await sql`SELECT clusters.cluster_name, clusters.cluster_ip FROM clusters JOIN users ON clusters.cluster_id=users.cluster_id WHERE users.user_id=${userid};`; + return result[0] + } + catch (error) { + console.error('Database Error:', error); + throw new Error('Failed to fetch clusterInfo.'); + } +} diff --git a/app/lib/homePage/numOfAlerts.tsx b/app/lib/homePage/numOfAlerts.tsx new file mode 100644 index 0000000..2845e4c --- /dev/null +++ b/app/lib/homePage/numOfAlerts.tsx @@ -0,0 +1,56 @@ +import sql from "../../utils/db"; +//Opting out of static rending for dynamic (no caching) @https://nextjs.org/docs/app/api-reference/functions/unstable_noStore +import { unstable_noStore as noStore } from 'next/cache'; +import { Count } from "../../../types/definitions"; + +//returns total number of alerts +export async function numTotalAlerts(clusterip: string) { + noStore(); + try { + const result = await sql`SELECT COUNT(*) FROM incidents JOIN clusters ON clusters.cluster_id=incidents.cluster_id WHERE clusters.cluster_ip=${clusterip}`; + return result[0].count + } + catch (error) { + console.error('Database Error:', error); + throw new Error('Failed to fetch total number of alerts.'); + } +} + +//return number of Open status alerts +export async function numOpenAlerts(clusterip: string) { + noStore(); + try { + const result = await sql`SELECT COUNT(*) FROM incidents JOIN clusters ON clusters.cluster_id=incidents.cluster_id WHERE incidents.incident_status='Open' AND clusters.cluster_ip=${clusterip}`; + return result[0].count + } + catch (error) { + console.error('Database Error:', error); + throw new Error('Failed to fetch number of Open alerts.'); + } +} + +//return number of In Progress status alerts +export async function numProgressAlerts(clusterip: string) { + noStore(); + try { + const result = await sql`SELECT COUNT(*) FROM incidents JOIN clusters ON clusters.cluster_id=incidents.cluster_id WHERE incidents.incident_status='In Progress' AND clusters.cluster_ip=${clusterip}`; + return result[0].count + } + catch (error) { + console.error('Database Error:', error); + throw new Error('Failed to fetch number of In Progress alerts.'); + } +} + +//return number of In Progress status alerts +export async function numCriticalAlerts(cluster_ip: string) { + noStore(); + try { + const result = await sql`SELECT COUNT(*) FROM incidents JOIN clusters ON clusters.cluster_id=incidents.cluster_id WHERE incidents.priority_level='Critical' AND clusters.cluster_ip=${cluster_ip}`; + return result[0].count + } + catch (error) { + console.error('Database Error:', error); + throw new Error('Failed to fetch number of Critical alerts.'); + } +} \ No newline at end of file diff --git a/app/lib/queries.tsx b/app/lib/queries.tsx new file mode 100644 index 0000000..3d92a22 --- /dev/null +++ b/app/lib/queries.tsx @@ -0,0 +1,255 @@ +import { unstable_noStore as noStore } from 'next/cache'; + +// Returns number of nodes in the ready condition for the cluster as a number +export async function numOfReadyNodes(ip: string) { + noStore(); + try { + // Define the Prometheus API query + const prometheusQuery = "sum(kube_node_status_condition{condition='Ready', status='true'}==1)"; + const prometheusServerIP = `${ip}`; + const prometheusEndpoint = `http://${prometheusServerIP}/api/v1/query?query=${encodeURIComponent(prometheusQuery)}`; + // Make the fetch request to Prometheus API + const response = await fetch(prometheusEndpoint); + if (!response.ok) { + throw new Error(`Failed to fetch data from Prometheus. Status: ${response.status}`); + } + const responseData = await response.json() + const result = responseData.data.result[0].value[1] + return result; + } + catch (error) { + console.error('Error:', error); + return 'error' + } +} + +// Return the number of nodes that have expereinced a 'not ready' condition in the last 15m as a number +export async function numOfUnhealthyNodes(ip: string) { + noStore(); + try { + // Define the Prometheus API query + const prometheusQuery = "sum(changes(kube_node_status_condition{status='true',condition='Ready'}[15m])) by (node) > 2"; + const prometheusServerIP = `${ip}`; + const prometheusEndpoint = `http://${prometheusServerIP}/api/v1/query?query=${encodeURIComponent(prometheusQuery)}`; + // Make the fetch request to Prometheus API + const response = await fetch(prometheusEndpoint); + if (!response.ok) { + throw new Error(`Failed to fetch data from Prometheus. Status: ${response.status}`); + } + const responseData = await response.json() + const result = responseData.data.result.length === 0 ? 0 : responseData.data.result[1]; + return result; + } + catch (error) { + console.error('Error:', error); + return 'error' + } +} + +// Returns the total number of pods in the cluster as a number +export async function numOfReadyPods(ip: string) { + noStore(); + try { + // Define the Prometheus API query + const prometheusQuery = "count(kube_pod_info)"; + const prometheusServerIP = `${ip}`; + const prometheusEndpoint = `http://${prometheusServerIP}/api/v1/query?query=${encodeURIComponent(prometheusQuery)}`; + // Make the fetch request to Prometheus API + const response = await fetch(prometheusEndpoint); + if (!response.ok) { + throw new Error(`Failed to fetch data from Prometheus. Status: ${response.status}`); + } + const responseData = await response.json() + const result = responseData.data.result[0].value.length <= 0 ? 0 : responseData.data.result[0].value[1]; + return result; + } + catch (error) { + console.error('Error:', error); + return 'error' + } +} + +// Returns the total number of pods in the cluster that are not in the 'ready' condition as a number +export async function numOfUnhealthyPods(ip: string) { + noStore(); + try { + // Define the Prometheus API query + const prometheusQuery = "sum by (namespace) (kube_pod_status_ready{condition='false'})"; + const prometheusServerIP = `${ip}`; + const prometheusEndpoint = `http://${prometheusServerIP}/api/v1/query?query=${encodeURIComponent(prometheusQuery)}`; + // Make the fetch request to Prometheus API + const response = await fetch(prometheusEndpoint); + if (!response.ok) { + throw new Error(`Failed to fetch data from Prometheus. Status: ${response.status}`); + } + const responseData = await response.json() + const result = responseData.data.result[0].value.length <= 0 ? 0 : responseData.data.result[0].value[1]; + return result; + } + catch (error) { + console.error('Error:', error); + return 'error' + } +} + +// Returns the number of pods per namepsace as an object with two properties +export async function numByNamePods(ip: string) { + noStore(); + try { + // Define the Prometheus API query + const prometheusQuery = "sum by (namespace) (kube_pod_info)"; + const prometheusServerIP = `${ip}`; + const prometheusEndpoint = `http://${prometheusServerIP}/api/v1/query?query=${encodeURIComponent(prometheusQuery)}`; + // Make the fetch request to Prometheus API + const response = await fetch(prometheusEndpoint); + if (!response.ok) { + throw new Error(`Failed to fetch data from Prometheus. Status: ${response.status}`); + } + const responseData = await response.json() + const result = responseData.data.result; + return result; + } + catch (error) { + console.error('Error:', error); + return 'error' + } +} + +// Returns number of pods that didn't have a ready status in the last five minutes by namespace as an object. +export async function restartByNamePods(ip: string) { + noStore(); + try { + // Define the Prometheus API query + const prometheusQuery = "sum by (namespace)(changes(kube_pod_status_ready{condition='true'}[5m]))"; + const prometheusServerIP = `${ip}`; + const prometheusEndpoint = `http://${prometheusServerIP}/api/v1/query?query=${encodeURIComponent(prometheusQuery)}`; + // Make the fetch request to Prometheus API + const response = await fetch(prometheusEndpoint); + if (!response.ok) { + throw new Error(`Failed to fetch data from Prometheus. Status: ${response.status}`); + } + const responseData = await response.json() + const result = responseData.data.result; + return result; + } + catch (error) { + console.error('Error:', error); + return 'error' + } +} + +// Return CPU Utilization By Node - as object +export async function cpuUtilByNode(ip: string) { + noStore(); + try { + // Define the Prometheus API query + const prometheusQuery = "100 - (avg by (instance) (irate(node_cpu_seconds_total{mode='idle'}[10m]) * 100) * on(instance) group_left(nodename) (node_uname_info))"; + const prometheusServerIP = `${ip}`; + const prometheusEndpoint = `http://${prometheusServerIP}/api/v1/query?query=${encodeURIComponent(prometheusQuery)}`; + // Make the fetch request to Prometheus API + const response = await fetch(prometheusEndpoint); + if (!response.ok) { + throw new Error(`Failed to fetch data from Prometheus. Status: ${response.status}`); + } + const responseData = await response.json() + const result = responseData.data.result; + return result; + } + catch (error) { + console.error('Error:', error); + return 'error' + } +} + +// Returning Cluster Memory Usage - returns string value +export async function clusterMemoryUsage(ip : string) { + try { + const prometheusQuery = 'sum (container_memory_working_set_bytes{id="/",kubernetes_io_hostname=~"^.*$"}) / sum (machine_memory_bytes{kubernetes_io_hostname=~"^.*$"}) * 100' + const prometheusServerIP = `${ip}`; + const prometheusEndpoint = `http://${prometheusServerIP}/api/v1/query?query=${encodeURIComponent(prometheusQuery)}`; + // Make the fetch request to Prometheus API + const response = await fetch(prometheusEndpoint); + if (!response.ok) { + throw new Error(`Failed to fetch data from Prometheus. Status: ${response.status}`); + } + const responseData = await response.json() + const result = responseData.data.result[0].value[1] + return result; + + } + catch(e) { + console.log('Error: ', e) + return e + } +} + +// Returning Cluster CPU Usage at a 10m average - returns string value +export async function clusterCpuUsage10mAvg(ip : string) { + try { + const prometheusQuery = 'sum (rate (container_cpu_usage_seconds_total{id="/",kubernetes_io_hostname=~"^.*$"}[10m])) / sum (machine_cpu_cores{kubernetes_io_hostname=~"^.*$"}) * 100' + const prometheusServerIP = `${ip}`; + const prometheusEndpoint = `http://${prometheusServerIP}/api/v1/query?query=${encodeURIComponent(prometheusQuery)}`; + // Make the fetch request to Prometheus API + const response = await fetch(prometheusEndpoint); + if (!response.ok) { + throw new Error(`Failed to fetch data from Prometheus. Status: ${response.status}`); + } + const responseData = await response.json() + const result = responseData.data.result[0].value[1] + return result; + } + catch(e) { + console.log('Error: ', e) + return e + } +} + +// Checks if Cluster is Active and returns boolean +export async function activeCluster(ip: string) { + noStore(); + try { + // Define the Prometheus API query + const prometheusQuery = "up==1"; + const prometheusServerIP = `${ip}`; + const prometheusEndpoint = `http://${prometheusServerIP}/api/v1/query?query=${encodeURIComponent(prometheusQuery)}`; + // Make the fetch request to Prometheus API + const response = await fetch(prometheusEndpoint); + if (!response.ok) { + throw new Error(`Failed to fetch data from Prometheus. Status: ${response.status}`); + } + const responseData = await response.json() + const result = responseData.data.result; + return result.length > 0; + } + catch (error) { + console.error('Error:', error); + return false + } +} + +//Returns memory available by Node as object - **WORKS NEED TO DEFINE WHAT 'MEMORY' IS BEING RETURNED** +// export async function memAvailByNode(){ +// noStore(); +// try{ +// // Define the Prometheus API query +// const prometheusQuery = "node_memory_MemAvailable_bytes * on(instance) group_left(nodename) (node_uname_info) / 2^30"; +// // Replace with the actual IP address of your Prometheus server (parameter) +// const prometheusServerIP = '34.168.131.121:80'; +// const prometheusEndpoint = `http://${prometheusServerIP}/api/v1/query?query=${encodeURIComponent(prometheusQuery)}`; +// // Make the fetch request to Prometheus API +// const response = await fetch(prometheusEndpoint); +// if (!response.ok) { +// throw new Error(`Failed to fetch data from Prometheus. Status: ${response.status}`); +// } +// const responseData = await response.json() +// console.log('cpu overcommit result:', responseData.data.result) +// const result = responseData.data.result; +// return result; +// } +// catch(error){ +// console.error('Error:', error); +// //We don't want the user to get this error - send some sort of error value? +// // throw new Error('Failed to fetch cpuUtilByNode.'); +// return 'error' +// } +// } \ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx index 755962a..96ed5a6 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,358 +1,25 @@ -import Image from "next/image"; -import Link from "next/link"; -import TeamMember from "./_components/landingPage/TeamMember"; +// import Navbar from "./_components/LandingPage/Navbar"; +import Navbar from "./_components/LandingPage/navbar"; +import TitleSection from "./_components/LandingPage/titleSection"; +import CoreTechSection from "./_components/LandingPage/coreTechSection"; +import FirstInfoSection from "./_components/LandingPage/firstinfoSection"; +import SecondInfoSection from "./_components/LandingPage/secondInfoSection"; +import TeamInfoSection from "./_components/LandingPage/teamInfoSection"; +import MediumArticleSection from "./_components/LandingPage/mediumSection"; +import Footer from "./_components/LandingPage/footerSection"; +import React from "react"; export default function LandingPage() { - - const teamMembers = [ - { - name: "Jesse Chou", - avatarSrc: - "https://ca.slack-edge.com/T04VCTELHPX-U05BZ4FFHHV-e6b25299e14c-512", - githubLink: "https://github.com/jesse-chou/", - linkedinLink: "https://www.linkedin.com/in/jesse-chou/", - role: "Software Engineer", - }, - { - name: "Derek Coughlan", - avatarSrc: - "https://ca.slack-edge.com/T04VCTELHPX-U057QU0L9LG-10e0ff54b26c-512", - githubLink: "https://github.com/derekcoughlan", - linkedinLink: "https://www.linkedin.com/in/derekcoughlan/", - role: "Software Engineer", - }, - { - name: "Emmanuel Ikhalea", - avatarSrc: - "https://ca.slack-edge.com/T04VCTELHPX-U05AZ9KR9A9-8ffcc9718ee7-512", - githubLink: "#", - linkedinLink: "https://www.linkedin.com/in/emmanuel-ikhalea-222781178/", - role: "Software Engineer", - }, - { - name: "Apiraam Selvabaskaran", - avatarSrc: - "https://ca.slack-edge.com/T04VCTELHPX-U050F0D9T71-9da884f90254-512", - githubLink: "https://github.com/apiraam96", - linkedinLink: - "https://www.linkedin.com/in/apiraam-selvabaskaran-2427b8162/", - role: "Software Engineer", - }, - { - name: "Dane Smith", - avatarSrc: - "https://ca.slack-edge.com/T04VCTELHPX-U05C5P6M935-aa90bb8223ce-512", - githubLink: "https://github.com/dudemandane", - linkedinLink: "https://www.linkedin.com/in/danealexandersmith/", - role: "Software Engineer", - }, - ]; - return ( -
- {/* Navbar */} -
- -
- - {/* ATF (Above The Fold) Section */} -
-
-
{/* Added pr-8 (padding-right) */} -

- Incident management for Kubernetes clusters -

-

- Stay on top of critical alerts with NotiKube's intuitive dashboard for complete incident lifecycle management. -

-
- - Get Started - - - - -
-
-
- -
-
-
- - {/* Core Technologies Section */} -
-

- Core Technologies Used -

-
- {[ - { logo: "kubernetes.svg", link: "https://kubernetes.io/" }, - { logo: "docker.svg", link: "https://www.docker.com/" }, - { logo: "prometheus.svg", link: "https://prometheus.io/" }, - { logo: "grafana.svg", link: "https://grafana.com/" }, - ].map((tech, index) => ( - - - - ))} -
-
- - {/* First Informational Section - Dashboard View */} -
-
-
-
-

- Monitor Your Alerts Alongside Cluster Health -

-

- See the most critical incident metrics pulled directly from - Prometheus Alert Manager -

-

- Check your clusters health directly from the homepage ensuring - high availability and constant uptime -

-
-
- -
-
-
-
- - {/* Second Informational Section - Incidents View */} -
-
-
- -
-
-

- Track Your Incidents -

-

- View details for each incident through our intuitive dashboard -

-

- Our application uses the Prometheus API to provide real-time - alerting and incident management. Track your most critical alerts - and see what priorities they are -

-
-
-
- - {/* Our Team Section */} -
-
-
-

- Our Team -

-

- NotiKube is a distributed team with engineers from around the world -

-
-
- {teamMembers.map((member, index) => ( - - ))} -
-
-
- - {/* Medium Article Section */} -
-
-
-

- Managing Incidents for Modern Day DevOps Teams -

-

- NotiKube is a lightweight incident management tool that utilizes - Prometheus Alert Manager and combines traditional incident - management tools like OpsGenie and ServiceNow into a centralized - platform -

-

- Read our Medium article to learn more about the problem we're - tackling and how we landed on the idea -

- - Learn more - - - - -
-
-
- - {/* Footer Section */} -
-
-
-
- - - - NotiKube - - -
-
-
-

- Follow us -

-
    -
  • - - Github - -
  • -
  • - - LinkedIn - -
  • -
-
-
-
-
-
- - © 2023{" "} - - NotiKube™ - - . All Rights Reserved. - -
- {/* Github Logo */} - - - - - - -
-
-
-
- +
+ + + + + + + +
); } diff --git a/cypress.config 2.ts b/cypress.config 2.ts new file mode 100644 index 0000000..818e1ed --- /dev/null +++ b/cypress.config 2.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "cypress"; + +export default defineConfig({ + e2e: { + setupNodeEvents(on, config) { + // implement node event listeners here + }, + baseUrl: 'http://localhost:3000' + }, + +}); diff --git a/cypress.config.ts b/cypress.config.ts new file mode 100644 index 0000000..818e1ed --- /dev/null +++ b/cypress.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "cypress"; + +export default defineConfig({ + e2e: { + setupNodeEvents(on, config) { + // implement node event listeners here + }, + baseUrl: 'http://localhost:3000' + }, + +}); diff --git a/cypress/e2e/dashboard/dashboard_spec.cy.ts b/cypress/e2e/dashboard/dashboard_spec.cy.ts new file mode 100644 index 0000000..f29075f --- /dev/null +++ b/cypress/e2e/dashboard/dashboard_spec.cy.ts @@ -0,0 +1,59 @@ +describe('Dashboard', () => { + beforeEach(() => { + cy.login(Cypress.env('username'), Cypress.env('password')) + cy.visit('/dashboard') + cy.viewport(1366, 768) + }) + + context("Sections", () => { + it('loads dashboard sections successfully', () => { + cy.contains('Alerts') + cy.contains('Metrics') + }) + }) + + context("Headers", () => { + it('loads dashboard headers successfully', () => { + cy.get('h1').should('contain', 'Dashboard') + }) + it('contains cluster name and ip info', () => { + cy.get('[data-cy="header-cluster-name"]').should('be.visible') + cy.get('[data-cy="header-cluster-ip"]').should('contain', 'Cluster IP Address:') + }) + }) + + context("Alert links", () => { + it('links to the incidents table via total alerts', () => { + cy.get('[data-cy="total-alerts"]').click() + cy.url().should('contain', '/dashboard/incidents') + }) + it('links to the incidents table via total open alerts', () => { + cy.get('[data-cy="total-open-alerts"]').click() + cy.url().should('contain', '/dashboard/incidents') + }) + }) + + context('Metrics', () => { + it('shows node cpu metrics tab', () => { + cy.get('[data-cy="node-cpu-tab"]').click() + cy.get('[data-cy="node-cpu"]').should('be.visible') + }) + it('shows cluster cpu/mem metrics tab', () => { + cy.get('[data-cy="cluster-cpu-tab"]').click() + cy.get('[data-cy="cluster-cpu"]').should('be.visible') + }) + it('shows pod by namespace metrics tab', () => { + cy.get('[data-cy="pod-name-tab"]').click() + cy.get('[data-cy="pod-name"]').should('be.visible') + }) + it('shows pod restart metrics tab', () => { + cy.get('[data-cy="pod-restart-tab"]').click() + cy.get('[data-cy="pod-restart"]').should('be.visible') + }) + it('shows cluster summary metrics tab', () => { + cy.get('[data-cy="cluster-summary-tab"]').click() + cy.get('[data-cy="cluster-summary"]').should('be.visible') + }) + }) + + }) \ No newline at end of file diff --git a/cypress/fixtures/example.json b/cypress/fixtures/example.json new file mode 100644 index 0000000..02e4254 --- /dev/null +++ b/cypress/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} diff --git a/cypress/support/1-getting-started/todo.cy.ts b/cypress/support/1-getting-started/todo.cy.ts new file mode 100644 index 0000000..4768ff9 --- /dev/null +++ b/cypress/support/1-getting-started/todo.cy.ts @@ -0,0 +1,143 @@ +/// + +// Welcome to Cypress! +// +// This spec file contains a variety of sample tests +// for a todo list app that are designed to demonstrate +// the power of writing tests in Cypress. +// +// To learn more about how Cypress works and +// what makes it such an awesome testing tool, +// please read our getting started guide: +// https://on.cypress.io/introduction-to-cypress + +describe('example to-do app', () => { + beforeEach(() => { + // Cypress starts out with a blank slate for each test + // so we must tell it to visit our website with the `cy.visit()` command. + // Since we want to visit the same URL at the start of all our tests, + // we include it in our beforeEach function so that it runs before each test + cy.visit('https://example.cypress.io/todo') + }) + + it('displays two todo items by default', () => { + // We use the `cy.get()` command to get all elements that match the selector. + // Then, we use `should` to assert that there are two matched items, + // which are the two default items. + cy.get('.todo-list li').should('have.length', 2) + + // We can go even further and check that the default todos each contain + // the correct text. We use the `first` and `last` functions + // to get just the first and last matched elements individually, + // and then perform an assertion with `should`. + cy.get('.todo-list li').first().should('have.text', 'Pay electric bill') + cy.get('.todo-list li').last().should('have.text', 'Walk the dog') + }) + + it('can add new todo items', () => { + // We'll store our item text in a variable so we can reuse it + const newItem = 'Feed the cat' + + // Let's get the input element and use the `type` command to + // input our new list item. After typing the content of our item, + // we need to type the enter key as well in order to submit the input. + // This input has a data-test attribute so we'll use that to select the + // element in accordance with best practices: + // https://on.cypress.io/selecting-elements + cy.get('[data-test=new-todo]').type(`${newItem}{enter}`) + + // Now that we've typed our new item, let's check that it actually was added to the list. + // Since it's the newest item, it should exist as the last element in the list. + // In addition, with the two default items, we should have a total of 3 elements in the list. + // Since assertions yield the element that was asserted on, + // we can chain both of these assertions together into a single statement. + cy.get('.todo-list li') + .should('have.length', 3) + .last() + .should('have.text', newItem) + }) + + it('can check off an item as completed', () => { + // In addition to using the `get` command to get an element by selector, + // we can also use the `contains` command to get an element by its contents. + // However, this will yield the