diff --git a/pkg/app/web/src/components/application-detail-page/application-state-view/cloudrun-state-view/cloudrun-resource-detail/index.tsx b/pkg/app/web/src/components/application-detail-page/application-state-view/cloudrun-state-view/cloudrun-resource-detail/index.tsx new file mode 100644 index 0000000000..606fdecbc1 --- /dev/null +++ b/pkg/app/web/src/components/application-detail-page/application-state-view/cloudrun-state-view/cloudrun-resource-detail/index.tsx @@ -0,0 +1,97 @@ +import { IconButton, makeStyles, Paper, Typography } from "@material-ui/core"; +import CloseIcon from "@material-ui/icons/Close"; +import { FC } from "react"; + +const DETAIL_WIDTH = 400; + +const useStyles = makeStyles((theme) => ({ + root: { + width: DETAIL_WIDTH, + padding: "16px 24px", + height: "100%", + overflow: "auto", + position: "relative", + zIndex: 2, + }, + closeButton: { + position: "absolute", + right: theme.spacing(1), + top: theme.spacing(1), + color: theme.palette.grey[500], + }, + name: { + paddingRight: theme.spacing(4), + wordBreak: "break-all", + paddingBottom: theme.spacing(2), + }, + section: { + paddingTop: theme.spacing(1), + display: "flex", + alignItems: "center", + }, + sectionTitle: { + color: theme.palette.text.secondary, + minWidth: 120, + }, + sectionBody: { + flex: 1, + wordBreak: "break-all", + }, + multilineSection: { + paddingTop: theme.spacing(1), + }, +})); + +export interface CloudRunResourceDetailProps { + resource: { + name: string; + kind: string; + apiVersion: string; + healthDescription: string; + }; + onClose: () => void; +} + +export const CloudRunResourceDetail: FC = ({ + resource, + onClose, +}) => { + const classes = useStyles(); + return ( + + + + + + {resource.name} + + +
+ + Kind + + + {resource.kind} + +
+ +
+ + Api Version + + + {resource.apiVersion} + +
+ +
+ + Health Description + + + {resource.healthDescription || "Empty"} + +
+
+ ); +}; diff --git a/pkg/app/web/src/components/application-detail-page/application-state-view/cloudrun-state-view/cloudrun-resource/health-status-icon/index.tsx b/pkg/app/web/src/components/application-detail-page/application-state-view/cloudrun-state-view/cloudrun-resource/health-status-icon/index.tsx new file mode 100644 index 0000000000..468b5db111 --- /dev/null +++ b/pkg/app/web/src/components/application-detail-page/application-state-view/cloudrun-state-view/cloudrun-resource/health-status-icon/index.tsx @@ -0,0 +1,36 @@ +import { makeStyles } from "@material-ui/core"; +import UnknownIcon from "@material-ui/icons/ErrorOutline"; +import FavoriteIcon from "@material-ui/icons/Favorite"; +import OtherIcon from "@material-ui/icons/HelpOutline"; +import { FC, memo } from "react"; +import { HealthStatus } from "~/modules/applications-live-state"; + +const useStyles = makeStyles((theme) => ({ + healthy: { + color: theme.palette.success.main, + }, + unknown: { + color: theme.palette.warning.main, + }, + other: { + color: theme.palette.info.main, + }, +})); + +export interface CloudRunResourceHealthStatusIconProps { + health: HealthStatus; +} + +export const CloudRunResourceHealthStatusIcon: FC = memo( + function HealthStatusIcon({ health }) { + const classes = useStyles(); + switch (health) { + case HealthStatus.UNKNOWN: + return ; + case HealthStatus.HEALTHY: + return ; + case HealthStatus.OTHER: + return ; + } + } +); diff --git a/pkg/app/web/src/components/application-detail-page/application-state-view/cloudrun-state-view/cloudrun-resource/index.tsx b/pkg/app/web/src/components/application-detail-page/application-state-view/cloudrun-state-view/cloudrun-resource/index.tsx new file mode 100644 index 0000000000..cb9cd0b3f2 --- /dev/null +++ b/pkg/app/web/src/components/application-detail-page/application-state-view/cloudrun-state-view/cloudrun-resource/index.tsx @@ -0,0 +1,42 @@ +import { makeStyles, Paper, Typography } from "@material-ui/core"; +import { FC, memo } from "react"; +import { CloudRunResourceState } from "~/modules/applications-live-state"; +import { CloudRunResourceHealthStatusIcon } from "./health-status-icon"; + +const useStyles = makeStyles((theme) => ({ + root: { + display: "inline-flex", + flexDirection: "column", + padding: theme.spacing(2), + width: 300, + cursor: "pointer", + }, + nameLine: { + display: "flex", + }, + name: { + marginLeft: theme.spacing(0.5), + }, +})); + +export interface CloudRunResourceProps { + resource: CloudRunResourceState.AsObject; + onClick: (resource: CloudRunResourceState.AsObject) => void; +} + +export const CloudRunResource: FC = memo( + function CloudRunResource({ resource, onClick }) { + const classes = useStyles(); + return ( + onClick(resource)}> + {resource.kind} +
+ + + {resource.name} + +
+
+ ); + } +); diff --git a/pkg/app/web/src/components/application-detail-page/application-state-view/cloudrun-state-view/index.tsx b/pkg/app/web/src/components/application-detail-page/application-state-view/cloudrun-state-view/index.tsx new file mode 100644 index 0000000000..5d45b912cf --- /dev/null +++ b/pkg/app/web/src/components/application-detail-page/application-state-view/cloudrun-state-view/index.tsx @@ -0,0 +1,188 @@ +import { Box, makeStyles } from "@material-ui/core"; +import clsx from "clsx"; +import dagre from "dagre"; +import { FC, useState } from "react"; +import { CloudRunResourceState } from "~/modules/applications-live-state"; +import { theme } from "~/theme"; +import { uniqueArray } from "~/utils/unique-array"; +import { CloudRunResource } from "./cloudrun-resource"; +import { CloudRunResourceDetail } from "./cloudrun-resource-detail"; + +const useStyles = makeStyles((theme) => ({ + root: { + display: "flex", + flex: 1, + justifyContent: "center", + overflow: "hidden", + }, + stateViewWrapper: { + flex: 1, + display: "flex", + justifyContent: "center", + overflow: "hidden", + }, + stateView: { + position: "relative", + overflow: "auto", + }, + closeDetailButton: { + position: "absolute", + right: theme.spacing(1), + top: theme.spacing(1), + color: theme.palette.grey[500], + }, +})); + +export interface CloudRunStateViewProps { + resources: CloudRunResourceState.AsObject[]; +} + +const NODE_HEIGHT = 72; +const NODE_WIDTH = 300; +const STROKE_WIDTH = 2; +const SVG_RENDER_PADDING = STROKE_WIDTH * 2; + +function useGraph( + resources: CloudRunResourceState.AsObject[], + showKinds: string[] +): dagre.graphlib.Graph<{ + resource: CloudRunResourceState.AsObject; +}> { + const graph = new dagre.graphlib.Graph<{ + resource: CloudRunResourceState.AsObject; + }>(); + graph.setGraph({ rankdir: "LR", align: "UL" }); + graph.setDefaultEdgeLabel(() => ({})); + + const service = resources.find((r) => r.parentIdsList.length === 0); + resources.forEach((resource) => { + graph.setNode(resource.id, { + resource, + height: NODE_HEIGHT, + width: NODE_WIDTH, + }); + if (service && resource.parentIdsList.length > 0) { + graph.setEdge(service.id, resource.id); + } + }); + + // Update after change graph + dagre.layout(graph); + + return graph; +} + +export const CloudRunStateView: FC = ({ + resources, +}) => { + const classes = useStyles(); + const [ + selectedResource, + setSelectedResource, + ] = useState(null); + + const kinds: string[] = uniqueArray(resources.map((r) => r.kind)); + const [filterState] = useState>( + kinds.reduce>((prev, current) => { + prev[current] = true; + return prev; + }, {}) + ); + const graph = useGraph( + resources, + Object.keys(filterState).filter((key) => filterState[key]) + ); + const nodes = graph + .nodes() + .map((v) => graph.node(v)) + .filter(Boolean); + + const graphInstance = graph.graph(); + + return ( +
+
+
+ {nodes.map((node) => ( + + + + ))} + { + // render edges + graph.edges().map((v, i) => { + const edge = graph.edge(v); + let baseX = Infinity; + let baseY = Infinity; + let svgWidth = 0; + let svgHeight = 0; + edge.points.forEach((p) => { + baseX = Math.min(baseX, p.x); + baseY = Math.min(baseY, p.y); + svgWidth = Math.max(svgWidth, p.x); + svgHeight = Math.max(svgHeight, p.y); + }); + baseX = Math.round(baseX); + baseY = Math.round(baseY); + // NOTE: Add padding to SVG sizes for showing edges completely. + // If you use the same size as the polyline points, it may hide the some strokes. + svgWidth = Math.ceil(svgWidth - baseX) + SVG_RENDER_PADDING; + svgHeight = Math.ceil(svgHeight - baseY) + SVG_RENDER_PADDING; + return ( + + { + return ( + prev + + `${Math.round(current.x - baseX) + STROKE_WIDTH},${ + Math.round(current.y - baseY) + STROKE_WIDTH + } ` + ); + }, "")} + strokeWidth={STROKE_WIDTH} + stroke={theme.palette.divider} + fill="transparent" + /> + + ); + }) + } + {graphInstance && ( +
+ )} +
+
+ + {selectedResource && ( + setSelectedResource(null)} + /> + )} +
+ ); +}; diff --git a/pkg/app/web/src/components/application-detail-page/application-state-view/index.tsx b/pkg/app/web/src/components/application-detail-page/application-state-view/index.tsx index ca5a09c09a..463cb2f323 100644 --- a/pkg/app/web/src/components/application-detail-page/application-state-view/index.tsx +++ b/pkg/app/web/src/components/application-detail-page/application-state-view/index.tsx @@ -22,6 +22,14 @@ import { selectHasError, } from "~/modules/applications-live-state"; import { KubernetesStateView } from "./kubernetes-state-view"; +import { CloudRunStateView } from "./cloudrun-state-view"; + +const isDisplayLiveState = (app: Application.AsObject | undefined): boolean => { + return ( + app?.kind === ApplicationKind.KUBERNETES || + app?.kind === ApplicationKind.CLOUDRUN + ); +}; const FETCH_INTERVAL = 4000; @@ -62,22 +70,20 @@ export const ApplicationStateView: FC = memo( ]); useEffect(() => { - if (app?.kind === ApplicationKind.KUBERNETES) { + if (app && isDisplayLiveState(app)) { dispatch(fetchApplicationStateById(app.id)); } }, [app, dispatch]); useInterval( () => { - // Only fetch kubernetes application. - if (app?.kind === ApplicationKind.KUBERNETES) { + // Only fetch kubernetes or cloud run application. + if (app && isDisplayLiveState(app)) { dispatch(fetchApplicationStateById(app.id)); } }, - // Only fetch kubernetes application. - app?.kind === ApplicationKind.KUBERNETES && hasError === false - ? FETCH_INTERVAL - : null + // Only fetch kubernetes or cloud run application. + isDisplayLiveState(app) && hasError === false ? FETCH_INTERVAL : null ); if (app?.disabled) { @@ -114,7 +120,7 @@ export const ApplicationStateView: FC = memo( if (!liveState) { return ( <> - {app?.kind === ApplicationKind.KUBERNETES ? ( + {isDisplayLiveState(app) ? (
@@ -139,6 +145,10 @@ export const ApplicationStateView: FC = memo( const resources = liveState.kubernetes?.resourcesList || []; return ; } + case ApplicationKind.CLOUDRUN: { + const resources = liveState.cloudrun?.resourcesList || []; + return ; + } default: } diff --git a/pkg/app/web/src/components/application-form/index.tsx b/pkg/app/web/src/components/application-form/index.tsx index 94deb8db12..61c1706994 100644 --- a/pkg/app/web/src/components/application-form/index.tsx +++ b/pkg/app/web/src/components/application-form/index.tsx @@ -384,13 +384,10 @@ export const ApplicationForm: FC = memo( label="Kind" value={`${values.kind}`} items={Object.keys(APPLICATION_KIND_TEXT).map((key) => ({ - name: - APPLICATION_KIND_TEXT[(key as unknown) as ApplicationKind], + name: APPLICATION_KIND_TEXT[(key as unknown) as ApplicationKind], value: key, }))} - onChange={({ value }) => - setFieldValue("kind", parseInt(value, 10)) - } + onChange={({ value }) => setFieldValue("kind", parseInt(value, 10))} disabled={isSubmitting || disableApplicationInfo} /> diff --git a/pkg/app/web/src/components/login-page/index.tsx b/pkg/app/web/src/components/login-page/index.tsx index 53a09a3a56..76e4f8f8cc 100644 --- a/pkg/app/web/src/components/login-page/index.tsx +++ b/pkg/app/web/src/components/login-page/index.tsx @@ -101,7 +101,8 @@ export const LoginPage: FC = memo(function LoginPage() { /> {isPlayEnvironment && (
- Try with play name if you want to join the playground environment + Try with play name if you want to join the + playground environment
)}
diff --git a/pkg/app/web/src/modules/applications-live-state/index.ts b/pkg/app/web/src/modules/applications-live-state/index.ts index 0a01f7c153..0a46a129ee 100644 --- a/pkg/app/web/src/modules/applications-live-state/index.ts +++ b/pkg/app/web/src/modules/applications-live-state/index.ts @@ -85,4 +85,5 @@ export const applicationLiveStateSlice = createSlice({ export { ApplicationLiveStateSnapshot, KubernetesResourceState, + CloudRunResourceState, } from "pipe/pkg/app/web/model/application_live_state_pb";