diff --git a/.github/workflows/sync-upstream-frontend.yml b/.github/workflows/sync-upstream-frontend.yml deleted file mode 100644 index 26a7ee723..000000000 --- a/.github/workflows/sync-upstream-frontend.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: Sync Upstream (Frontend) - -on: - workflow_dispatch: - -jobs: - sync-upstream: - runs-on: ubuntu-latest - steps: - - name: Checkout private repository - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - name: Get Current Date and Day - id: date - run: echo "BRANCH_NAME=shadow/$(date +%A-%Y-%m-%d)" >> $GITHUB_ENV - - - name: Add Upstream Remote and Fetch Changes - run: | - git remote add upstream https://github.com/bluewave-labs/checkmate.git || true - git fetch upstream develop --prune - if ! git show-ref --quiet refs/remotes/upstream/develop; then - echo "ERROR: upstream/develop not found" - exit 1 - fi - - - name: Ensure Local Develop is Up-to-Date - run: | - git checkout develop - git pull origin develop --prune - - - name: Create Shadow Branch - run: | - git checkout -b ${{ env.BRANCH_NAME }} develop - - - name: Identify New Commits - id: check_commits - run: | - NEW_COMMITS=$(git log develop..upstream/develop --pretty=format:"%H" | wc -l) - echo "New commits: $NEW_COMMITS" - if [ "$NEW_COMMITS" -eq 0 ]; then - echo "No new commits. Exiting." - exit 0 - fi - - - name: Cherry-pick New Commits - run: | - git cherry-pick develop..upstream/develop || (git cherry-pick --abort && exit 1) - git push origin ${{ env.BRANCH_NAME }} - - - name: Create Pull Request - uses: peter-evans/create-pull-request@v5 - with: - title: "Sync Upstream (Frontend) - ${{ env.BRANCH_NAME }}" - body: "Automated upstream sync for frontend repository. Please review and merge." - branch: ${{ env.BRANCH_NAME }} - base: develop - labels: "sync, upstream" - reviewers: "ridvankaratas, ajhollid" diff --git a/src/Components/Charts/DePINStatusPageBarChart/index.jsx b/src/Components/Charts/DePINStatusPageBarChart/index.jsx new file mode 100644 index 000000000..65c6278f1 --- /dev/null +++ b/src/Components/Charts/DePINStatusPageBarChart/index.jsx @@ -0,0 +1,181 @@ +// Components +import { Stack, Box, Tooltip, Typography } from "@mui/material"; + +// Utils +import { useTheme } from "@emotion/react"; +import { useState, useEffect } from "react"; +import PropTypes from "prop-types"; +import { formatDateWithTz } from "../../../Utils/timeUtils"; +import { useSelector } from "react-redux"; + +const PlaceholderCheck = ({ daysToShow }) => { + const theme = useTheme(); + + return ( + + ); +}; + +PlaceholderCheck.propTypes = { + daysToShow: PropTypes.number, +}; + +const Check = ({ check, daysToShow }) => { + const [animate, setAnimate] = useState(false); + + const theme = useTheme(); + useEffect(() => { + setAnimate(true); + }, []); + const uiTimezone = useSelector((state) => state.ui.timezone); + + return ( + + + {formatDateWithTz(check._id, "ddd, MMMM D, YYYY", uiTimezone)} + + + + + Uptime percentage + + + {check.upPercentage.toFixed(2)} + + {" "} + % + + + + + + } + placement="top" + key={`check-${check?._id}`} + slotProps={{ + popper: { + className: "bar-tooltip", + modifiers: [ + { + name: "offset", + options: { + offset: [0, -10], + }, + }, + ], + sx: { + "& .MuiTooltip-tooltip": { + backgroundColor: theme.palette.secondary.main, + border: 1, + borderColor: theme.palette.primary.lowContrast, + borderRadius: theme.shape.borderRadius, + boxShadow: theme.shape.boxShadow, + px: theme.spacing(4), + py: theme.spacing(3), + }, + "& .MuiTooltip-tooltip p": { + /* TODO Font size should point to theme */ + fontSize: 12, + color: theme.palette.secondary.contrastText, + fontWeight: 500, + }, + "& .MuiTooltip-tooltip span": { + /* TODO Font size should point to theme */ + fontSize: 11, + color: theme.palette.secondary.contrastText, + fontWeight: 600, + }, + }, + }, + }} + > + + + + + ); +}; + +Check.propTypes = { + check: PropTypes.object, + daysToShow: PropTypes.number, +}; + +const DePINStatusPageBarChart = ({ checks = [], daysToShow = 30 }) => { + if (checks.length !== daysToShow) { + const placeholders = Array(daysToShow - checks.length).fill("placeholder"); + checks = [...checks, ...placeholders]; + } + return ( + + {checks.map((check) => { + if (check === "placeholder") { + return ( + + ); + } + return ( + + ); + })} + + ); +}; + +DePINStatusPageBarChart.propTypes = { + checks: PropTypes.array, + daysToShow: PropTypes.number, +}; + +export default DePINStatusPageBarChart; diff --git a/src/Pages/DistributedUptimeStatus/Create/Hooks/useDUStatusPageFetch.jsx b/src/Pages/DistributedUptimeStatus/Create/Hooks/useDUStatusPageFetch.jsx new file mode 100644 index 000000000..eecb161a3 --- /dev/null +++ b/src/Pages/DistributedUptimeStatus/Create/Hooks/useDUStatusPageFetch.jsx @@ -0,0 +1,46 @@ +import { useEffect, useState, useCallback } from "react"; +import { networkService } from "../../../../main"; +import { useSelector } from "react-redux"; +import { createToast } from "../../../../Utils/toastUtils"; + +const useDUStatusPageFetch = (isCreate = false, url) => { + const [isLoading, setIsLoading] = useState(true); + const [networkError, setNetworkError] = useState(false); + const [statusPage, setStatusPage] = useState(undefined); + const { authToken } = useSelector((state) => state.auth); + const fetchStatusPage = useCallback(async () => { + try { + const response = await networkService.getStatusPageByUrl({ + authToken, + url, + type: "distributed", + }); + + if (!response?.data?.data) return; + + const statusPage = response.data.data; + setStatusPage(statusPage); + } catch (error) { + // If there is a 404, status page is not found + if (error?.response?.status === 404) { + setStatusPage(undefined); + return; + } + createToast({ body: error.message }); + setNetworkError(true); + } finally { + setIsLoading(false); + } + }, [authToken, url]); + + useEffect(() => { + if (isCreate === true) { + return; + } + fetchStatusPage(); + }, [isCreate, fetchStatusPage]); + + return [statusPage, isLoading, networkError, fetchStatusPage]; +}; + +export { useDUStatusPageFetch }; diff --git a/src/Pages/DistributedUptimeStatus/Create/index.jsx b/src/Pages/DistributedUptimeStatus/Create/index.jsx index 3aa16a074..09673b53e 100644 --- a/src/Pages/DistributedUptimeStatus/Create/index.jsx +++ b/src/Pages/DistributedUptimeStatus/Create/index.jsx @@ -7,26 +7,34 @@ import VisuallyHiddenInput from "./Components/VisuallyHiddenInput"; import Image from "../../../Components/Image"; import LogoPlaceholder from "../../../assets/Images/logo_placeholder.svg"; import Breadcrumbs from "../../../Components/Breadcrumbs"; +import Search from "../../../Components/Inputs/Search"; +import MonitorList from "../../StatusPage/Create/Components/MonitorList"; // Utils import { useTheme } from "@emotion/react"; import { useState, useEffect } from "react"; import { useParams } from "react-router-dom"; -import { useStatusPageFetch } from "../../StatusPage/Status/Hooks/useStatusPageFetch"; +import { useDUStatusPageFetch } from "./Hooks/useDUStatusPageFetch"; import { useCreateStatusPage } from "../../StatusPage/Create/Hooks/useCreateStatusPage"; import { statusPageValidation } from "../../../Validation/validation"; import { buildErrors } from "../../../Validation/error"; import { createToast } from "../../../Utils/toastUtils"; import { useNavigate } from "react-router-dom"; +import { useMonitorsFetch } from "../../StatusPage/Create/Hooks/useMonitorsFetch"; const CreateStatus = () => { const theme = useTheme(); const { monitorId, url } = useParams(); const navigate = useNavigate(); const isCreate = typeof url === "undefined"; + const [createStatusPage, isLoading, networkError] = useCreateStatusPage(isCreate); - const [statusPage, statusPageMonitors, statusPageIsLoading, statusPageNetworkError] = - useStatusPageFetch(isCreate, url); + const [statusPage, statusPageIsLoading, statusPageNetworkError] = useDUStatusPageFetch( + isCreate, + url + ); + + const [monitors, monitorsIsLoading, monitorsNetworkError] = useMonitorsFetch(); const BREADCRUMBS = [ { name: "distributed uptime", path: "/distributed-uptime" }, @@ -43,6 +51,8 @@ const CreateStatus = () => { monitors: [monitorId], }); const [errors, setErrors] = useState({}); + const [search, setSearch] = useState(""); + const [selectedMonitors, setSelectedMonitors] = useState([]); const handleFormChange = (e) => { const { name, value, checked, type } = e.target; @@ -62,6 +72,16 @@ const CreateStatus = () => { setForm({ ...form, [name]: value }); }; + const handleMonitorsChange = (selectedMonitors) => { + handleFormChange({ + target: { + name: "subMonitors", + value: selectedMonitors.map((monitor) => monitor._id), + }, + }); + setSelectedMonitors(selectedMonitors); + }; + const handleImageUpload = (e) => { const img = e.target?.files?.[0]; setForm((prev) => ({ @@ -73,19 +93,20 @@ const CreateStatus = () => { let logoToSubmit = undefined; // Handle image - if (typeof form.logo !== "undefined") { + if (typeof form.logo !== "undefined" && typeof form.logo.src === "undefined") { logoToSubmit = { src: URL.createObjectURL(form.logo), name: form.logo.name, type: form.logo.type, size: form.logo.size, }; + } else if (typeof form.logo !== "undefined") { + logoToSubmit = form.logo; } const formToSubmit = { ...form }; if (typeof logoToSubmit !== "undefined") { formToSubmit.logo = logoToSubmit; } - // Validate const { error } = statusPageValidation.validate(formToSubmit, { abortEarly: false }); if (typeof error === "undefined") { @@ -127,12 +148,20 @@ const CreateStatus = () => { companyName: statusPage?.companyName, isPublished: statusPage?.isPublished, timezone: statusPage?.timezone, - monitors: statusPageMonitors.map((monitor) => monitor._id), + monitors: statusPage?.monitors, + subMonitors: statusPage?.subMonitors.map((monitor) => monitor._id), color: statusPage?.color, logo: newLogo, }; }); - }, [isCreate, statusPage, statusPageMonitors]); + setSelectedMonitors(statusPage?.subMonitors); + }, [isCreate, statusPage]); + + const imgSrc = form?.logo?.src + ? form.logo.src + : form.logo + ? URL.createObjectURL(form.logo) + : undefined; return ( @@ -212,7 +241,7 @@ const CreateStatus = () => { alignItems="center" > Logo { + + + Standard Monitors + + Attach standard monitors to your status page. + + + + + + + { + const theme = useTheme(); + const { determineState } = useUtils(); + return ( + <> + {monitors?.map((monitor) => { + const status = determineState(monitor); + return ( + + + + + + + + + + + + ); + })} + + ); +}; + +MonitorsList.propTypes = { + monitors: PropTypes.array.isRequired, +}; + +export default MonitorsList; diff --git a/src/Pages/DistributedUptimeStatus/Status/Components/TimeframeHeader/index.jsx b/src/Pages/DistributedUptimeStatus/Status/Components/TimeframeHeader/index.jsx new file mode 100644 index 000000000..011f470aa --- /dev/null +++ b/src/Pages/DistributedUptimeStatus/Status/Components/TimeframeHeader/index.jsx @@ -0,0 +1,39 @@ +import { Stack, Button, ButtonGroup } from "@mui/material"; +import { RowContainer } from "../../../../../Components/StandardContainer"; +import { useTheme } from "@emotion/react"; + +const TimeFrameHeader = ({ timeFrame, setTimeFrame }) => { + const theme = useTheme(); + return ( + + + + + + + + ); +}; + +export default TimeFrameHeader; diff --git a/src/Pages/DistributedUptimeStatus/Status/Hooks/useStatusPageFetchByUrl.jsx b/src/Pages/DistributedUptimeStatus/Status/Hooks/useStatusPageFetchByUrl.jsx index 0ca6346a0..61c4f1b65 100644 --- a/src/Pages/DistributedUptimeStatus/Status/Hooks/useStatusPageFetchByUrl.jsx +++ b/src/Pages/DistributedUptimeStatus/Status/Hooks/useStatusPageFetchByUrl.jsx @@ -2,25 +2,41 @@ import { useState, useEffect } from "react"; import { networkService } from "../../../../main"; import { createToast } from "../../../../Utils/toastUtils"; import { useSelector } from "react-redux"; +import { useTheme } from "@emotion/react"; +import { useMonitorUtils } from "../../../../Hooks/useMonitorUtils"; -const useStatusPageFetchByUrl = ({ url }) => { +const useStatusPageFetchByUrl = ({ url, timeFrame }) => { const [isLoading, setIsLoading] = useState(true); const [networkError, setNetworkError] = useState(false); const [statusPage, setStatusPage] = useState(undefined); const [monitorId, setMonitorId] = useState(undefined); const [isPublished, setIsPublished] = useState(false); const { authToken } = useSelector((state) => state.auth); + const theme = useTheme(); + const { getMonitorWithPercentage } = useMonitorUtils(); + useEffect(() => { const fetchStatusPageByUrl = async () => { try { - const response = await networkService.getStatusPageByUrl({ + const response = await networkService.getDistributedStatusPageByUrl({ authToken, url, type: "distributed", + timeFrame, }); if (!response?.data?.data) return; const statusPage = response.data.data; - setStatusPage(statusPage); + + const monitorsWithPercentage = statusPage?.subMonitors.map((monitor) => + getMonitorWithPercentage(monitor, theme) + ); + + const statusPageWithSubmonitorPercentages = { + ...statusPage, + subMonitors: monitorsWithPercentage, + }; + setStatusPage(statusPageWithSubmonitorPercentages); + setMonitorId(statusPage?.monitors[0]); setIsPublished(statusPage?.isPublished); } catch (error) { @@ -33,7 +49,7 @@ const useStatusPageFetchByUrl = ({ url }) => { } }; fetchStatusPageByUrl(); - }, [authToken, url]); + }, [authToken, url, getMonitorWithPercentage, theme, timeFrame]); return [isLoading, networkError, statusPage, monitorId, isPublished]; }; diff --git a/src/Pages/DistributedUptimeStatus/Status/index.jsx b/src/Pages/DistributedUptimeStatus/Status/index.jsx index fc3043eda..68493ed81 100644 --- a/src/Pages/DistributedUptimeStatus/Status/index.jsx +++ b/src/Pages/DistributedUptimeStatus/Status/index.jsx @@ -16,6 +16,7 @@ import UptLogo from "../../../assets/icons/upt_logo.png"; import PeopleAltOutlinedIcon from "@mui/icons-material/PeopleAltOutlined"; import InfoBox from "../../../Components/InfoBox"; import StatusHeader from "../../DistributedUptime/Details/Components/StatusHeader"; +import MonitorsList from "./Components/MonitorsList"; //Utils import { useTheme } from "@mui/material/styles"; @@ -27,6 +28,8 @@ import { useStatusPageDelete } from "../../StatusPage/Status/Hooks/useStatusPage import { useNavigate } from "react-router-dom"; import { useLocation } from "react-router-dom"; +import TimeFrameHeader from "./Components/TimeframeHeader"; + import SubHeader from "../../DistributedUptime/Details/Components/Subheader"; const DistributedUptimeStatus = () => { const { url } = useParams(); @@ -37,9 +40,11 @@ const DistributedUptimeStatus = () => { // Local State const [dateRange, setDateRange] = useState("day"); const [isDeleteOpen, setIsDeleteOpen] = useState(false); + const [timeFrame, setTimeFrame] = useState(30); // Utils const theme = useTheme(); const navigate = useNavigate(); + const [ statusPageIsLoading, statusPageNetworkError, @@ -48,6 +53,7 @@ const DistributedUptimeStatus = () => { isPublished, ] = useStatusPageFetchByUrl({ url, + timeFrame, }); const [isLoading, networkError, connectionStatus, monitor, lastUpdateTrigger] = @@ -56,6 +62,7 @@ const DistributedUptimeStatus = () => { const [deleteStatusPage, isDeleting] = useStatusPageDelete(() => { navigate("/distributed-uptime"); }, url); + // Constants const BREADCRUMBS = [ { name: "Distributed Uptime", path: "/distributed-uptime" }, @@ -223,6 +230,14 @@ const DistributedUptimeStatus = () => { monitor={monitor} lastUpdateTrigger={lastUpdateTrigger} /> + +