This commit is contained in:
Manabu Mccloskey 2023-07-03 17:24:10 -07:00
parent 7c155abf1a
commit 0e5a38664c
21 changed files with 982 additions and 0 deletions

View file

@ -0,0 +1,3 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, {
extends: ['prettier'],
});

View file

@ -0,0 +1,2 @@
build
coverage

View file

@ -0,0 +1,13 @@
# apache-spark
Welcome to the apache-spark plugin!
_This plugin was created through the Backstage CLI_
## Getting started
Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/apache-spark](http://localhost:3000/apache-spark).
You can also serve the plugin in isolation by running `yarn start` in the plugin directory.
This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads.
It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory.

View file

@ -0,0 +1,12 @@
import React from 'react';
import { createDevApp } from '@backstage/dev-utils';
import { apacheSparkPlugin, ApacheSparkPage } from '../src/plugin';
createDevApp()
.registerPlugin(apacheSparkPlugin)
.addPage({
element: <ApacheSparkPage />,
title: 'Root Page',
path: '/apache-spark'
})
.render();

View file

@ -0,0 +1,55 @@
{
"name": "@internal/plugin-apache-spark",
"version": "0.1.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"private": true,
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"backstage": {
"role": "frontend-plugin"
},
"scripts": {
"start": "backstage-cli package start",
"build": "backstage-cli package build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
},
"dependencies": {
"@backstage/core-components": "^0.13.1",
"@backstage/core-plugin-api": "^1.5.1",
"@backstage/plugin-catalog-react": "^1.7.0",
"@backstage/plugin-kubernetes": "^0.9.2",
"@backstage/theme": "^0.3.0",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.61",
"react-use": "^17.2.4",
"yaml": "^2.3.1"
},
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.22.7",
"@backstage/core-app-api": "^1.8.0",
"@backstage/dev-utils": "^1.0.15",
"@backstage/test-utils": "^1.3.1",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^12.1.3",
"@testing-library/user-event": "^14.0.0",
"@types/node": "*",
"cross-fetch": "^3.1.5",
"msw": "^1.0.0"
},
"files": [
"dist"
]
}

View file

@ -0,0 +1,57 @@
#
# Copyright 2017 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
apiVersion: "sparkoperator.k8s.io/v1beta2"
kind: SparkApplication
metadata:
# name: spark-pi
generateName: spark-pi
namespace: default
spec:
type: Python
pythonVersion: "3"
mode: cluster
image: "public.ecr.aws/m8u6z8z4/manabu-test:test-spark"
imagePullPolicy: Always
mainClass: org.apache.spark.examples.SparkPi
mainApplicationFile: "local:///opt/spark/examples/src/main/python/pi.ps"
sparkVersion: "3.1.1"
restartPolicy:
type: Never
volumes:
- name: "test-volume"
hostPath:
path: "/tmp"
type: Directory
driver:
cores: 1
coreLimit: "1200m"
memory: "512m"
labels:
version: 3.1.1
serviceAccount: spark
volumeMounts:
- name: "test-volume"
mountPath: "/tmp"
executor:
cores: 1
instances: 1
memory: "512m"
labels:
version: 3.1.1
volumeMounts:
- name: "test-volume"
mountPath: "/tmp"

View file

@ -0,0 +1,35 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: spark
namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: default
name: spark-role
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["*"]
- apiGroups: [""]
resources: ["services"]
verbs: ["*"]
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["*"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: spark-role-binding
namespace: default
subjects:
- kind: ServiceAccount
name: spark
namespace: default
roleRef:
kind: Role
name: spark-role
apiGroup: rbac.authorization.k8s.io

View file

@ -0,0 +1,103 @@
import { createApiRef } from '@backstage/core-plugin-api';
import { ApacheSpark, ApacheSparkList } from './model';
import { KubernetesApi } from '@backstage/plugin-kubernetes';
export const apacheSparkApiRef = createApiRef<ApacheSparkApi>({
id: 'plugin.apachespark',
});
const API_VERSION = 'sparkoperator.k8s.io/v1beta2';
const SPARK_APP_PLURAL = 'sparkapplications';
const K8s_API_TIMEOUT = 'timeoutSeconds';
export interface ApacheSparkApi {
getSparkApps(
clusterName: string | undefined,
namespace: string | undefined,
labels: string | undefined,
): Promise<ApacheSparkList>;
getSparkApp(
clusterName: string | undefined,
namespace: string | undefined,
name: string,
): Promise<ApacheSpark>;
}
export class ApacheSparkClient implements ApacheSparkApi {
private kubernetesApi: KubernetesApi;
constructor(kubernetesApi: KubernetesApi) {
this.kubernetesApi = kubernetesApi;
}
async getSparkApps(
clusterName: string | undefined,
namespace: string | undefined,
labels: string,
): Promise<ApacheSparkList> {
const ns = namespace !== undefined ? namespace : 'default';
const path = `/apis/${API_VERSION}/namespaces/${ns}/${SPARK_APP_PLURAL}`;
const query = new URLSearchParams({
[K8s_API_TIMEOUT]: '30',
// labelSelector: labels,
});
const resp = await this.kubernetesApi.proxy({
clusterName:
clusterName !== undefined ? clusterName : await this.getFirstCluster(),
path: `${path}?${query.toString()}`,
});
if (!resp.ok) {
return Promise.reject(
`failed to fetch resources: ${resp.status}, ${
resp.statusText
}, ${await resp.text()}`,
);
}
const out = JSON.parse(await resp.text());
this.removeManagedField(out);
return out;
}
async getSparkApp(
clusterName: string | undefined,
namespace: string | undefined,
name: string,
): Promise<ApacheSpark> {
const ns = namespace !== undefined ? namespace : 'default';
const path = `/apis/${API_VERSION}/namespaces/${ns}/${SPARK_APP_PLURAL}/${name}`;
const resp = await this.kubernetesApi.proxy({
clusterName:
clusterName !== undefined ? clusterName : await this.getFirstCluster(),
path: `${path}`,
});
if (!resp.ok) {
return Promise.reject(
`failed to fetch resources: ${resp.status}, ${
resp.statusText
}, ${await resp.text()}`,
);
}
const out = JSON.parse(await resp.text());
this.removeManagedField(out);
return out;
}
async getFirstCluster(): Promise<string> {
const clusters = await this.kubernetesApi.getClusters();
if (clusters.length > 0) {
return Promise.resolve(clusters[0].name);
}
return Promise.reject('no clusters found in configuration');
}
removeManagedField(spark: any) {
if (spark.metadata?.hasOwnProperty('managedFields')) {
delete spark.metadata.managedFields;
}
if (spark.items) {
for (const i of spark.items) {
this.removeManagedField(i);
}
}
}
}

View file

@ -0,0 +1,84 @@
export type Metadata = {
name: string;
namespace?: string;
labels: Record<string, string>;
annotations: Record<string, string>;
creationTimestamp: string;
managedFields?: any;
};
export type Spec = {
arguments: string[];
batchScheduler: string;
driver: {
coreLimit: string;
coreRequest: string;
cores: number;
gpu: {
name: string;
quantity: number;
};
labels: Record<string, string>;
memory: string;
memoryOverhead: string;
podName: string;
schedulerName: string;
serviceAccount: string;
};
executor: {
coreLimit: string;
coreRequest: string;
cores: number;
gpu: {
name: string;
quantity: number;
};
instances: number;
labels: Record<string, string>;
memory: string;
memoryOverhead: string;
schedulerName: string;
serviceAccount: string;
};
image: string;
mainClass: string;
mode: string;
pythonVersion: string;
type: string;
};
export type Status = {
applicationState: {
errorMessage: string;
state: string;
};
driverInfo: {
podName: string;
webUIAddress: string;
webUIIngressAddress: string;
webUIIngressName: string;
webUIPort: string;
webUIServiceName: string;
};
executionAttempts: number;
executorState: Record<string, string>;
lastSubmissionAttemptTime: string;
sparkApplicationId: string;
submissionAttempts: number;
submissionID: string;
terminationTime: string;
};
export type ApacheSpark = {
apiVersion: string;
kind: string;
metadata: Metadata;
spec: Spec;
status: Status;
};
export type ApacheSparkList = {
apiVersion: string;
kind: string;
items?: ApacheSpark[];
};

View file

@ -0,0 +1,148 @@
import {
Progress,
StatusError,
StatusOK,
StatusPending,
StatusRunning,
Table,
TableColumn,
} from '@backstage/core-components';
import { useEntity } from '@backstage/plugin-catalog-react';
import { useApi } from '@backstage/core-plugin-api';
import { apacheSparkApiRef } from '../../api';
import React, { useEffect, useState } from 'react';
import { getAnnotationValues } from '../utils';
import useAsync from 'react-use/lib/useAsync';
import { ApacheSpark, ApacheSparkList } from '../../api/model';
import Alert from '@material-ui/lab/Alert';
import { createStyles, Drawer, makeStyles, Theme } from '@material-ui/core';
import { DrawerContent } from '../DetailedDrawer/DetailedDrawer';
type TableData = {
id: string;
name: string;
namespace: string;
applicationState?: string;
startedAt?: string;
finishedAt?: string;
raw: ApacheSpark;
};
const columns: TableColumn[] = [
{
title: 'Name',
field: 'name',
},
{ title: 'Namespace', field: 'namespace', type: 'string' },
{
title: 'Application State',
field: 'applicationState',
},
{
title: 'StartTime',
field: 'startedAt',
type: 'datetime',
defaultSort: 'desc',
},
{ title: 'EndTime', field: 'finishedAt', type: 'datetime' },
];
const useDrawerStyles = makeStyles((theme: Theme) =>
createStyles({
paper: {
width: '50%',
justifyContent: 'space-between',
padding: theme.spacing(2.5),
},
}),
);
export const ApacheSparkOverviewTable = () => {
// const { entity } = useEntity();
const apiClient = useApi(apacheSparkApiRef);
const [columnData, setColumnData] = useState([] as TableData[]);
const [isOpen, toggleDrawer] = useState(false);
const [drawerData, setDrawerData] = useState({} as ApacheSpark);
const classes = useDrawerStyles();
// const { ns, clusterName, labelSelector } = getAnnotationValues(entity);
const { value, loading, error } = useAsync(
async (): Promise<ApacheSparkList> => {
return await apiClient.getSparkApps(
'cnoe-packaging-2',
'default',
undefined,
);
},
);
useEffect(() => {
const data = value?.items?.map(val => {
let state = {};
switch (val.status?.applicationState.state) {
case 'RUNNING':
state = <StatusRunning>Running</StatusRunning>;
break;
case 'COMPLETED':
state = <StatusOK>COMPLETED</StatusOK>;
break;
case 'FAILED':
state = <StatusError>FAILED</StatusError>;
break;
default:
state = (
<StatusPending>
'${val.status.applicationState.state}'
</StatusPending>
);
break;
}
return {
id: `${val.metadata.namespace}/${val.metadata.name}`,
raw: val,
name: val.metadata.name,
namespace: val.metadata.namespace,
applicationState: state,
startedAt: val.metadata.creationTimestamp,
finishedAt: val.status?.terminationTime,
} as TableData;
});
if (data && data.length > 0) {
setColumnData(data);
}
}, [value]);
if (loading) {
return <Progress />;
} else if (error) {
return <Alert severity="error">{`${error}`}</Alert>;
}
return (
<>
<Table
options={{
padding: 'dense',
paging: true,
search: true,
sorting: true,
}}
onRowClick={(_event, rowData: TableData | undefined) => {
setDrawerData(rowData?.raw!);
toggleDrawer(true);
}}
columns={columns}
data={columnData}
/>
<Drawer
classes={{
paper: classes.paper,
}}
anchor="right"
open={isOpen}
onClose={() => toggleDrawer(false)}
>
<DrawerContent toggleDrawer={toggleDrawer} apacheSpark={drawerData} />
</Drawer>
</>
);
};

View file

@ -0,0 +1,91 @@
import { ApacheSpark } from '../../api/model';
import {
Button,
createStyles,
IconButton,
makeStyles,
Theme,
Typography,
} from '@material-ui/core';
import Close from '@material-ui/icons/Close';
import React, { PropsWithChildren } from 'react';
import { stringify } from 'yaml';
import { CopyTextButton, TabbedLayout } from '@backstage/core-components';
import { MemoryRouter, Route, Routes } from 'react-router-dom';
const useDrawerContentStyles = makeStyles((theme: Theme) =>
createStyles({
header: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
},
icon: {
fontSize: 20,
},
content: {
height: '80%',
backgroundColor: '#EEEEEE',
overflow: 'scroll',
display: 'flex',
flexDirection: 'row',
},
secondaryAction: {
marginLeft: theme.spacing(2.5),
},
}),
);
export const DrawerContent = ({
toggleDrawer,
apacheSpark,
}: {
toggleDrawer: (isOpen: boolean) => void;
apacheSpark: ApacheSpark;
}) => {
const classes = useDrawerContentStyles();
const yamlString = stringify(apacheSpark);
return (
<TabbedLayout>
<TabbedLayout.Route path="/" title="Manifest">
<>
<div className={classes.header}>
<Typography variant="h6">{apacheSpark.metadata.name}</Typography>
<IconButton
key="dismiss"
title="Close"
onClick={() => toggleDrawer(false)}
color="inherit"
>
<Close className={classes.icon} />
</IconButton>
</div>
<div className={classes.content}>
<CopyTextButton text={yamlString} tooltipText="Copy" />
<pre>{yamlString}</pre>
</div>
<div>
<Button
variant="contained"
color="primary"
onClick={() => toggleDrawer(false)}
>
Primary Action
</Button>
<Button
className={classes.secondaryAction}
variant="outlined"
color="primary"
onClick={() => toggleDrawer(false)}
>
Secondary Action
</Button>
</div>
</>
</TabbedLayout.Route>
<TabbedLayout.Route path="/logs" title="logs">
<div>logs!</div>
</TabbedLayout.Route>
</TabbedLayout>
);
};

View file

@ -0,0 +1,14 @@
import { Content, Header, HeaderLabel, Page } from '@backstage/core-components';
import { ApacheSparkOverviewTable } from '../ApacheSparkOverviewTable/ApacheSparkOverviewTable';
import React from 'react';
export const ApacheSparkOverviewPage = () => (
<Page themeId="tool">
<Header title="Apache Spark">
<HeaderLabel label="Lifecycle" value="Alpha" />
</Header>
<Content>
<ApacheSparkOverviewTable />
</Content>
</Page>
);

View file

@ -0,0 +1 @@
export * from './Overview';

View file

@ -0,0 +1,31 @@
import { Entity } from '@backstage/catalog-model';
import {
APACHE_SPARK_LABEL_SELECTOR_ANNOTATION,
CLUSTER_NAME_ANNOTATION,
K8S_LABEL_SELECTOR_ANNOTATION,
K8S_NAMESPACE_ANNOTATION,
} from '../plugin';
export type getAnnotationValuesOutput = {
ns: string;
clusterName?: string;
labelSelector?: string;
};
export function getAnnotationValues(entity: Entity): getAnnotationValuesOutput {
const ns =
entity.metadata.annotations?.[K8S_NAMESPACE_ANNOTATION] !== undefined
? entity.metadata.annotations?.[K8S_NAMESPACE_ANNOTATION]
: 'default';
const clusterName = entity.metadata.annotations?.[CLUSTER_NAME_ANNOTATION];
const labelSelector =
entity.metadata?.annotations?.[APACHE_SPARK_LABEL_SELECTOR_ANNOTATION] !==
undefined
? entity.metadata?.annotations?.[APACHE_SPARK_LABEL_SELECTOR_ANNOTATION]
: entity.metadata.annotations?.[K8S_LABEL_SELECTOR_ANNOTATION];
return {
ns: ns,
clusterName: clusterName,
labelSelector: labelSelector,
};
}

View file

@ -0,0 +1 @@
export { apacheSparkPlugin, ApacheSparkPage } from './plugin';

View file

@ -0,0 +1,7 @@
import { apacheSparkPlugin } from './plugin';
describe('apache-spark', () => {
it('should export plugin', () => {
expect(apacheSparkPlugin).toBeDefined();
});
});

View file

@ -0,0 +1,42 @@
import {
createApiFactory,
createPlugin,
createRoutableExtension,
discoveryApiRef,
fetchApiRef,
} from '@backstage/core-plugin-api';
import { rootRouteRef } from './routes';
import { apacheSparkApiRef, ApacheSparkClient } from './api';
import { kubernetesApiRef } from '@backstage/plugin-kubernetes';
export const APACHE_SPARK_LABEL_SELECTOR_ANNOTATION =
'apache-spark/label-selector';
export const CLUSTER_NAME_ANNOTATION = 'apache-spark/cluster-name';
export const K8S_LABEL_SELECTOR_ANNOTATION =
'backstage.io/kubernetes-label-selector';
export const K8S_NAMESPACE_ANNOTATION = 'backstage.io/kubernetes-namespace';
export const apacheSparkPlugin = createPlugin({
id: 'apache-spark',
routes: {
root: rootRouteRef,
},
apis: [
createApiFactory({
api: apacheSparkApiRef,
deps: {
kubernetesApi: kubernetesApiRef,
},
factory: ({ kubernetesApi }) => new ApacheSparkClient(kubernetesApi),
}),
],
});
export const ApacheSparkPage = apacheSparkPlugin.provide(
createRoutableExtension({
name: 'ApacheSparkPage',
component: () =>
import('./components/Overvew').then(m => m.ApacheSparkOverviewPage),
mountPoint: rootRouteRef,
}),
);

View file

@ -0,0 +1,5 @@
import { createRouteRef } from '@backstage/core-plugin-api';
export const rootRouteRef = createRouteRef({
id: 'apache-spark',
});

View file

@ -0,0 +1,2 @@
import '@testing-library/jest-dom';
import 'cross-fetch/polyfill';

276
plugins/apache-spark/test/t Normal file
View file

@ -0,0 +1,276 @@
<!DOCTYPE html><html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/static/bootstrap.min.css" type="text/css"/><link rel="stylesheet" href="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/static/vis-timeline-graph2d.min.css" type="text/css"/><link rel="stylesheet" href="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/static/webui.css" type="text/css"/><link rel="stylesheet" href="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/static/timeline-view.css" type="text/css"/><script src="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/static/sorttable.js"></script><script src="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/static/jquery-3.5.1.min.js"></script><script src="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/static/vis-timeline-graph2d.min.js"></script><script src="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/static/bootstrap.bundle.min.js"></script><script src="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/static/initialize-tooltips.js"></script><script src="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/static/table.js"></script><script src="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/static/timeline-view.js"></script><script src="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/static/log-view.js"></script><script src="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/static/webui.js"></script><script>setUIRoot('')</script>
<script>setAppBasePath('')</script>
<link rel="shortcut icon" href="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/static/spark-logo-77x50px-hd.png"></link>
<title>PythonPi - Spark Jobs</title>
</head>
<body>
<nav class="navbar navbar-expand-md navbar-light bg-light mb-4">
<div class="navbar-header">
<div class="navbar-brand">
<a href="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/">
<img src="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/static/spark-logo-77x50px-hd.png"/>
<span class="version">3.2.1</span>
</a>
</div>
</div>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarCollapse">
<ul class="navbar-nav mr-auto"><li class="nav-item active">
<a class="nav-link" href="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/jobs/">Jobs</a>
</li><li class="nav-item">
<a class="nav-link" href="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/stages/">Stages</a>
</li><li class="nav-item">
<a class="nav-link" href="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/storage/">Storage</a>
</li><li class="nav-item">
<a class="nav-link" href="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/environment/">Environment</a>
</li><li class="nav-item">
<a class="nav-link" href="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/executors/">Executors</a>
</li><li class="nav-item">
<a class="nav-link" href="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/SQL/">SQL</a>
</li></ul>
<span class="navbar-text navbar-right d-none d-md-block">
<strong title="PythonPi" class="text-nowrap">PythonPi</strong>
<span class="text-nowrap">application UI</span>
</span>
</div>
</nav>
<div class="container-fluid">
<div class="row">
<div class="col-12">
<h3 style="vertical-align: bottom; white-space: nowrap; overflow: hidden;
text-overflow: ellipsis;">
Spark Jobs
<sup>
(<a data-toggle="tooltip" data-placement="top" title="A job is triggered by an action, like count() or saveAsTextFile(). Click on a job to see information about the stages of tasks inside it.">?</a>)
</sup>
</h3>
</div>
</div>
<div class="row">
<div class="col-12">
<div>
<ul class="list-unstyled">
<li>
<strong>User:</strong>
root
</li>
<li>
<strong>Total Uptime:</strong>
55 min
</li>
<li>
<strong>Scheduling Mode: </strong>
FIFO
</li>
<li id="completed-summary">
<a href="#completed"><strong>Completed Jobs:</strong></a>
1
</li>
</ul>
</div><span class="expand-application-timeline">
<span class="expand-application-timeline-arrow arrow-closed"></span>
<a data-toggle="tooltip" title="Shows when jobs started and ended and when executors joined or left. Drag to scroll.
Click Enable Zooming and use mouse wheel to zoom in/out." data-placement="top">
Event Timeline
</a>
</span><div id="application-timeline" class="collapsed">
<div class="control-panel">
<div id="application-timeline-zoom-lock">
<input type="checkbox"></input>
<span>Enable zooming</span>
</div>
</div>
</div><script type="text/javascript">
drawApplicationTimeline(
[
{
'id': 'executors',
'content': '<div>Executors</div><div class="legend-area"><svg width="150px" height="55px"> <rect class="executor-added-legend" x="5px" y="5px" width="20px" height="15px" rx="2px" ry="2px"></rect> <text x="35px" y="17px">Added</text> <rect class="executor-removed-legend" x="5px" y="30px" width="20px" height="15px" rx="2px" ry="2px"></rect> <text x="35px" y="42px">Removed</text> </svg></div>',
},
{
'id': 'jobs',
'content': '<div>Jobs</div><div class="legend-area"><svg width="150px" height="85px"> <rect class="succeeded-job-legend" x="5px" y="5px" width="20px" height="15px" rx="2px" ry="2px"></rect> <text x="35px" y="17px">Succeeded</text> <rect class="failed-job-legend" x="5px" y="30px" width="20px" height="15px" rx="2px" ry="2px"></rect> <text x="35px" y="42px">Failed</text> <rect class="running-job-legend" x="5px" y="55px" width="20px" height="15px" rx="2px" ry="2px"></rect> <text x="35px" y="67px">Running</text> </svg></div>',
}
]
,[
{
'className': 'job application-timeline-object succeeded',
'group': 'jobs',
'start': new Date(1688165973401),
'end': new Date(1688165987811),
'content': '<div class="application-timeline-content"' +
'data-html="true" data-placement="top" data-toggle="tooltip"' +
'data-title="reduce at \/opt\/spark\/examples\/src\/main\/python\/pi.py:43 (Job 0)<br>' +
'Status: SUCCEEDED<br>' +
'Submitted: 2023/06/30 22:59:33' +
'<br>Completed: 2023/06/30 22:59:47">' +
'reduce at \/opt\/spark\/examples\/src\/main\/python\/pi.py:43 (Job 0)</div>'
}
,
{
'className': 'executor added',
'group': 'executors',
'start': new Date(1688165940466),
'content': '<div class="executor-event-content"' +
'data-toggle="tooltip" data-placement="top"' +
'data-title="Executor driver<br>' +
'Added at 2023/06/30 22:59:00"' +
'data-html="true">Executor driver added</div>'
}
,
{
'className': 'executor added',
'group': 'executors',
'start': new Date(1688165986135),
'content': '<div class="executor-event-content"' +
'data-toggle="tooltip" data-placement="top"' +
'data-title="Executor 1<br>' +
'Added at 2023/06/30 22:59:46"' +
'data-html="true">Executor 1 added</div>'
}
], 1688165935501, 0);
</script><span id="completed" class="collapse-aggregated-completedJobs collapse-table" onclick="collapseTable(&#39;collapse-aggregated-completedJobs&#39;,&#39;aggregated-completedJobs&#39;)">
<h4>
<span class="collapse-table-arrow arrow-open"></span>
<a>Completed Jobs (1)</a>
</h4>
</span><div class="aggregated-completedJobs collapsible-table">
<div>
<div>
<div>
<form id="form-completedJob-table-top-page" method="get" action="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/jobs/?&amp;completedJob.sort=Job+Id&amp;completedJob.desc=true#completed" class="form-inline float-right justify-content-end" style="margin-bottom: 0px;">
<input type="hidden" name="completedJob.sort" value="Job Id"/><input type="hidden" name="completedJob.desc" value="true"/>
<label>1 Pages. Jump to</label>
<input type="text" name="completedJob.page" id="form-completedJob-table-top-page-no" value="1" class="col-1 form-control"/>
<label>. Show </label>
<input type="text" id="form-completedJob-table-top-page-size" name="completedJob.pageSize" value="100" class="col-1 form-control"/>
<label>items in a page.</label>
<button type="submit" class="btn btn-spark">Go</button>
</form>
</div>
<div>
<span style="float: left; padding-top: 4px; padding-right: 4px;">Page: </span>
<ul class="pagination">
<li class="page-item disabled"><a href="" class="page-link">1</a></li>
</ul>
</div>
</div>
<table class="table table-bordered table-sm table-striped table-head-clickable table-cell-width-limited" id="completedJob-table">
<thead>
<tr><th>
<a href="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/jobs/?&amp;completedJob.sort=Job+Id&amp;completedJob.desc=false&amp;completedJob.pageSize=100#completed">
<span data-toggle="tooltip" data-placement="top" title="">
Job Id&nbsp;&#x25BE;
</span>
</a>
</th><th>
<a href="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/jobs/?&amp;completedJob.sort=Description&amp;completedJob.pageSize=100#completed">
<span data-toggle="tooltip" data-placement="top" title="">
Description
</span>
</a>
</th><th>
<a href="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/jobs/?&amp;completedJob.sort=Submitted&amp;completedJob.pageSize=100#completed">
<span data-toggle="tooltip" data-placement="top" title="">
Submitted
</span>
</a>
</th><th>
<a href="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/jobs/?&amp;completedJob.sort=Duration&amp;completedJob.pageSize=100#completed">
<span data-toggle="tooltip" data-placement="top" title="Elapsed time since the job was submitted until execution completion of all its stages.">
Duration
</span>
</a>
</th><th>
<span data-toggle="tooltip" data-placement="top" title="">
Stages: Succeeded/Total
</span>
</th><th>
<span data-toggle="tooltip" data-placement="top" title="">
Tasks (for all stages): Succeeded/Total
</span>
</th></tr>
</thead>
<tbody>
<tr id="job-0">
<td>
0
</td>
<td>
<span class="description-input">reduce at /opt/spark/examples/src/main/python/pi.py:43</span>
<a href="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/jobs/job/?id=0" class="name-link">reduce at /opt/spark/examples/src/main/python/pi.py:43</a>
</td>
<td>
2023/06/30 22:59:33
</td>
<td>14 s</td>
<td class="stage-progress-cell">
1/1
</td>
<td class="progress-cell">
<div class="progress">
<span style="text-align:center; position:absolute; width:100%;">
2/2
</span>
<div class="progress-bar progress-completed" style="width: 100.0%"></div>
<div class="progress-bar progress-started" style="width: 0.0%"></div>
</div>
</td>
</tr>
</tbody>
</table>
<div>
<div>
<form id="form-completedJob-table-bottom-page" method="get" action="/api/v1/namespaces/default/services/http:spark-pi-ui-svc:4040/proxy/jobs/?&amp;completedJob.sort=Job+Id&amp;completedJob.desc=true#completed" class="form-inline float-right justify-content-end" style="margin-bottom: 0px;">
<input type="hidden" name="completedJob.sort" value="Job Id"/><input type="hidden" name="completedJob.desc" value="true"/>
<label>1 Pages. Jump to</label>
<input type="text" name="completedJob.page" id="form-completedJob-table-bottom-page-no" value="1" class="col-1 form-control"/>
<label>. Show </label>
<input type="text" id="form-completedJob-table-bottom-page-size" name="completedJob.pageSize" value="100" class="col-1 form-control"/>
<label>items in a page.</label>
<button type="submit" class="btn btn-spark">Go</button>
</form>
</div>
<div>
<span style="float: left; padding-top: 4px; padding-right: 4px;">Page: </span>
<ul class="pagination">
<li class="page-item disabled"><a href="" class="page-link">1</a></li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

Binary file not shown.