Volumes Management UI (kubeflow/kubeflow#5684)
* vwa(back): first commit of volumes web app backend
Signed-off-by: Kimonas Sotirchos <kimwnasptd@arrikto.com>
* vwa(front): init code for volumes web app frontend
Signed-off-by: Kimonas Sotirchos <kimwnasptd@arrikto.com>
* vwa: {Make,Docker}files
Signed-off-by: Kimonas Sotirchos <kimwnasptd@arrikto.com>
* vwa: gitignore
Signed-off-by: Kimonas Sotirchos <kimwnasptd@arrikto.com>
* vwa: README
Signed-off-by: Kimonas Sotirchos <kimwnasptd@arrikto.com>
* review: Rename Jupyter to Volumes in README
Signed-off-by: Kimonas Sotirchos <kimwnasptd@arrikto.com>
* review: Rephrase comment in default flavor
Signed-off-by: Kimonas Sotirchos <kimwnasptd@arrikto.com>
* review: Remove snapshot reference from default flavor
Signed-off-by: Kimonas Sotirchos <kimwnasptd@arrikto.com>
This commit is contained in:
parent
a1e52c2b9e
commit
a63eff43ed
|
|
@ -0,0 +1,3 @@
|
|||
**/__pycache__/
|
||||
**/static/*
|
||||
py/
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
# --- Build the backend kubeflow-wheel ---
|
||||
FROM python:3.8 AS backend-kubeflow-wheel
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
COPY ./common/backend/ .
|
||||
RUN python3 setup.py bdist_wheel
|
||||
|
||||
# --- Build the frontend kubeflow library ---
|
||||
FROM node:12 as frontend-kubeflow-lib
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
COPY ./common/frontend/kubeflow-common-lib/package*.json ./
|
||||
RUN npm install
|
||||
|
||||
COPY ./common/frontend/kubeflow-common-lib/ .
|
||||
RUN npm run build
|
||||
|
||||
# --- Build the frontend ---
|
||||
FROM node:12-buster-slim as frontend
|
||||
|
||||
WORKDIR /src
|
||||
COPY ./volumes/frontend/package*.json ./
|
||||
RUN npm install
|
||||
COPY --from=frontend-kubeflow-lib /src/dist/kubeflow/ ./node_modules/kubeflow/
|
||||
|
||||
COPY ./volumes/frontend/ .
|
||||
|
||||
RUN npm run build -- --output-path=./dist/default --configuration=production
|
||||
RUN npm run build -- --output-path=./dist/rok --configuration=rok-prod
|
||||
|
||||
# Web App
|
||||
FROM python:3.7-slim-buster
|
||||
|
||||
WORKDIR /package
|
||||
COPY --from=backend-kubeflow-wheel /src .
|
||||
RUN pip3 install .
|
||||
|
||||
WORKDIR /src
|
||||
COPY ./volumes/backend/requirements.txt .
|
||||
RUN pip3 install -r requirements.txt
|
||||
|
||||
COPY ./volumes/backend/apps/ ./apps
|
||||
COPY ./volumes/backend/entrypoint.py .
|
||||
|
||||
COPY --from=frontend /src/dist/default/ /src/apps/default/static/
|
||||
COPY --from=frontend /src/dist/rok/ /src/apps/rok/static/
|
||||
|
||||
ENTRYPOINT ["/bin/bash","-c","gunicorn -w 3 --bind 0.0.0.0:5000 --access-logfile - entrypoint:app"]
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
# See: https://github.com/moby/moby/issues/12886#issuecomment-480575928
|
||||
# Ignore everything and node_modules
|
||||
**
|
||||
#
|
||||
# Don't ignore the necessary files for the app
|
||||
!common/
|
||||
!volumes/
|
||||
|
||||
# Ignore node_modules and pycache inside accepted dirs
|
||||
**/node_modules/*
|
||||
**/__pycache__/*
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
IMG ?= gcr.io/arrikto-playground/kubeflow/volumes-web-app
|
||||
TAG ?= $(shell git describe --tags)
|
||||
DOCKERFILE=volumes/Dockerfile
|
||||
|
||||
docker-build:
|
||||
cp Dockerfile.dockerignore ../.dockerignore
|
||||
-cd ../ && docker build -t ${IMG}:${TAG} -f ${DOCKERFILE} .
|
||||
rm ../.dockerignore
|
||||
|
||||
|
||||
docker-push:
|
||||
docker push $(IMG):$(TAG)
|
||||
|
||||
image: docker-build docker-push
|
||||
@echo "Updated image ${IMG}"
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
# Volumes web app
|
||||
|
||||
This web app is responsible for allowing the user to manipulate PVCs in their Kubeflow cluster. To achieve this it provides a user friendly way to handle the lifecycle of PVC objects.
|
||||
|
||||
## Development
|
||||
|
||||
Requirements:
|
||||
* node 12.0.0
|
||||
* python 3.7
|
||||
|
||||
### Frontend
|
||||
|
||||
```bash
|
||||
# build the common library
|
||||
cd components/crud-web-apps/common/frontend/kubeflow-common-lib
|
||||
npm i
|
||||
npm run build
|
||||
cd dist/kubeflow
|
||||
npm link
|
||||
|
||||
# build the app frontend
|
||||
cd ../../../volumes/frontend
|
||||
npm i
|
||||
npm link kubeflow
|
||||
npm run build:watch
|
||||
```
|
||||
|
||||
### Backend
|
||||
```bash
|
||||
# create a virtual env and install deps
|
||||
# https://packaging.python.org/guides/installing-using-pip-and-virtual-environments/
|
||||
cd component/crud-web-apps/volumes/backend
|
||||
python3.7 -m pip install --user virtualenv
|
||||
python3.7 -m venv web-apps-dev
|
||||
source web-apps-dev/bin/activate
|
||||
|
||||
# install the deps on the activated virtual env
|
||||
make -C backend install-deps
|
||||
|
||||
# run the backend
|
||||
make -C backend run-dev
|
||||
```
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
SHELL=bash
|
||||
|
||||
install-deps:
|
||||
pushd ../../../py && \
|
||||
pip install -e . && \
|
||||
popd
|
||||
pip install -r requirements.txt
|
||||
|
||||
run:
|
||||
APP_PREFIX=/volumes \
|
||||
gunicorn -w 3 --bind 0.0.0.0:5000 --access-logfile - entrypoint:app
|
||||
|
||||
run-rok:
|
||||
UI_FLAVOR=rok \
|
||||
APP_PREFIX=/volumes \
|
||||
gunicorn -w 3 --bind 0.0.0.0:5000 --access-logfile - entrypoint:app
|
||||
|
||||
run-dev:
|
||||
UI_FLAVOR=default \
|
||||
BACKEND_MODE=dev \
|
||||
APP_PREFIX=/ \
|
||||
gunicorn -w 3 --bind 0.0.0.0:5000 --access-logfile - entrypoint:app
|
||||
|
||||
run-dev-rok:
|
||||
UI_FLAVOR=rok \
|
||||
BACKEND_MODE=dev \
|
||||
APP_PREFIX=/ \
|
||||
gunicorn -w 3 --bind 0.0.0.0:5000 --access-logfile - entrypoint:app
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import kubeflow.kubeflow.crud_backend as base
|
||||
from kubeflow.kubeflow.crud_backend import config, logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_app(name=__name__, static_folder="static",
|
||||
cfg: config.Config = None):
|
||||
cfg = config.Config() if cfg is None else cfg
|
||||
|
||||
app = base.create_app(name, static_folder, cfg)
|
||||
|
||||
return app
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
from kubernetes import client
|
||||
|
||||
|
||||
def handle_storage_class(vol):
|
||||
"""
|
||||
vol: dict (send from the frontend)
|
||||
|
||||
If the fronend sent the special values `{none}` or `{empty}` then the
|
||||
backend will need to set the corresponding storage_class value that the
|
||||
python client expects.
|
||||
"""
|
||||
if "class" not in vol:
|
||||
return None
|
||||
if vol["class"] == "{none}":
|
||||
return ""
|
||||
if vol["class"] == "{empty}":
|
||||
return None
|
||||
else:
|
||||
return vol["class"]
|
||||
|
||||
|
||||
def pvc_from_dict(body, namespace):
|
||||
"""
|
||||
body: json object (frontend json data)
|
||||
|
||||
Convert the PVC json object that is sent from the backend to a python
|
||||
client PVC instance.
|
||||
"""
|
||||
return client.V1PersistentVolumeClaim(
|
||||
metadata=client.V1ObjectMeta(name=body["name"], namespace=namespace),
|
||||
spec=client.V1PersistentVolumeClaimSpec(
|
||||
access_modes=[body["mode"]],
|
||||
storage_class_name=handle_storage_class(body),
|
||||
resources=client.V1ResourceRequirements(
|
||||
requests={"storage": body["size"]}
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
from kubeflow.kubeflow.crud_backend import api, status
|
||||
|
||||
|
||||
def pvc_status(pvc):
|
||||
"""
|
||||
Set the status of the pvc
|
||||
"""
|
||||
if pvc.metadata.deletion_timestamp is not None:
|
||||
return status.create_status(
|
||||
status.STATUS_PHASE.TERMINATING, "Deleting Volume..."
|
||||
)
|
||||
|
||||
if pvc.status.phase == "Bound":
|
||||
return status.create_status(status.STATUS_PHASE.READY, "Bound")
|
||||
|
||||
# The PVC is in Pending state, we check the Events to find out why
|
||||
evs = api.v1_core.list_namespaced_event(
|
||||
namespace=pvc.metadata.namespace,
|
||||
field_selector=api.events_field_selector(
|
||||
"PersistentVolumeClaim", pvc.metadata.name
|
||||
),
|
||||
).items
|
||||
|
||||
# If there are no events, then the PVC was just created
|
||||
if len(evs) == 0:
|
||||
return status.create_status(
|
||||
status.STATUS_PHASE.WAITING, "Provisioning Volume..."
|
||||
)
|
||||
|
||||
msg = f"Pending: {evs[0].message}"
|
||||
state = evs[0].reason
|
||||
if evs[0].reason == "WaitForFirstConsumer":
|
||||
phase = status.STATUS_PHASE.UNAVAILABLE
|
||||
msg = (
|
||||
"Pending: This volume will be bound when its first consumer"
|
||||
+ " is created. E.g., when you first browse its contents, or"
|
||||
+ " attach it to a notebook server"
|
||||
)
|
||||
elif evs[0].reason == "Provisioning":
|
||||
phase = status.STATUS_PHASE.WAITING
|
||||
elif evs[0].reason == "FailedBinding":
|
||||
phase = status.STATUS_PHASE.WARNING
|
||||
elif evs[0].type == "Warning":
|
||||
phase = status.STATUS_PHASE.WARNING
|
||||
elif evs[0].type == "Normal":
|
||||
phase = status.STATUS_PHASE.READY
|
||||
|
||||
return status.create_status(phase, msg, state)
|
||||
|
||||
|
||||
def viewer_status(viewer):
|
||||
"""
|
||||
Return a string representing the status of that viewer. If a deletion
|
||||
timestamp is set we want to return a `Terminating` state.
|
||||
"""
|
||||
try:
|
||||
ready = viewer["status"]["ready"]
|
||||
except KeyError:
|
||||
return status.STATUS_PHASE.UNINITIALIZED
|
||||
|
||||
if "deletionTimestamp" in viewer["metadata"]:
|
||||
return status.STATUS_PHASE.TERMINATING
|
||||
|
||||
if not ready:
|
||||
return status.STATUS_PHASE.WAITING
|
||||
|
||||
return status.STATUS_PHASE.READY
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
from kubeflow.kubeflow.crud_backend import api, helpers
|
||||
|
||||
from . import status
|
||||
|
||||
|
||||
def parse_pvc(pvc):
|
||||
"""
|
||||
pvc: client.V1PersistentVolumeClaim
|
||||
|
||||
Process the PVC and format it as the UI expects it.
|
||||
"""
|
||||
try:
|
||||
capacity = pvc.status.capacity["storage"]
|
||||
except Exception:
|
||||
capacity = pvc.spec.resources.requests["storage"]
|
||||
|
||||
parsed_pvc = {
|
||||
"name": pvc.metadata.name,
|
||||
"namespace": pvc.metadata.namespace,
|
||||
"status": status.pvc_status(pvc),
|
||||
"age": {
|
||||
"uptime": helpers.get_uptime(pvc.metadata.creation_timestamp),
|
||||
"timestamp": pvc.metadata.creation_timestamp.strftime(
|
||||
"%d/%m/%Y, %H:%M:%S"
|
||||
),
|
||||
},
|
||||
"capacity": capacity,
|
||||
"modes": pvc.spec.access_modes,
|
||||
"class": pvc.spec.storage_class_name,
|
||||
}
|
||||
|
||||
return parsed_pvc
|
||||
|
||||
|
||||
def get_pods_using_pvc(pvc, namespace):
|
||||
"""
|
||||
Return a list of Pods that are using the given PVC
|
||||
"""
|
||||
pods = api.list_pods(namespace)
|
||||
mounted_pods = []
|
||||
|
||||
for pod in pods.items:
|
||||
pvcs = get_pod_pvcs(pod)
|
||||
if pvc in pvcs:
|
||||
mounted_pods.append(pod)
|
||||
|
||||
return mounted_pods
|
||||
|
||||
|
||||
def get_pod_pvcs(pod):
|
||||
"""
|
||||
Return a list of PVC name that the given Pod
|
||||
is using. If it doesn't use any, then an empty list will
|
||||
be returned.
|
||||
"""
|
||||
pvcs = []
|
||||
if not pod.spec.volumes:
|
||||
return []
|
||||
|
||||
vols = pod.spec.volumes
|
||||
for vol in vols:
|
||||
# Check if the volume is a pvc
|
||||
if not vol.persistent_volume_claim:
|
||||
continue
|
||||
|
||||
pvcs.append(vol.persistent_volume_claim.claim_name)
|
||||
|
||||
return pvcs
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import os
|
||||
|
||||
from kubeflow.kubeflow.crud_backend import config, logging
|
||||
|
||||
from ..common import create_app as create_default_app
|
||||
from .routes import bp as routes_bp
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_app(name=__name__, cfg: config.Config = None):
|
||||
cfg = config.Config() if cfg is None else cfg
|
||||
|
||||
# Properly set the static serving directory
|
||||
static_dir = os.path.join(
|
||||
os.path.abspath(os.path.dirname(__file__)), "static"
|
||||
)
|
||||
|
||||
app = create_default_app(name, static_dir, cfg)
|
||||
|
||||
log.info("Setting STATIC_DIR to: " + static_dir)
|
||||
app.config["STATIC_DIR"] = static_dir
|
||||
|
||||
# Register the app's blueprints
|
||||
app.register_blueprint(routes_bp)
|
||||
|
||||
return app
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
from flask import Blueprint
|
||||
|
||||
bp = Blueprint("default_routes", __name__)
|
||||
|
||||
from . import delete, get, post # noqa: F401, E402
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
from werkzeug import exceptions
|
||||
|
||||
from kubeflow.kubeflow.crud_backend import api, logging
|
||||
|
||||
from ...common import utils as common_utils
|
||||
from . import bp
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@bp.route("/api/namespaces/<namespace>/pvcs/<pvc>", methods=["DELETE"])
|
||||
def delete_pvc(pvc, namespace):
|
||||
"""
|
||||
Delete a PVC only if it is not used from any Pod
|
||||
"""
|
||||
pods = common_utils.get_pods_using_pvc(pvc, namespace)
|
||||
if pods:
|
||||
pod_names = [p.metadata.name for p in pods]
|
||||
raise exceptions.Conflict("Cannot delete PVC '%s' because it is being"
|
||||
" used by pods: %s" % (pvc, pod_names))
|
||||
|
||||
log.info("Deleting PVC %s/%s...", namespace, pvc)
|
||||
api.delete_pvc(pvc, namespace)
|
||||
log.info("Successfully deleted PVC %s/%s", namespace, pvc)
|
||||
|
||||
return api.success_response("message",
|
||||
"PVC %s successfully deleted." % pvc)
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
from kubeflow.kubeflow.crud_backend import api, logging
|
||||
|
||||
from ...common import utils
|
||||
from . import bp
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@bp.route("/api/namespaces/<namespace>/pvcs")
|
||||
def get_pvcs(namespace):
|
||||
# Return the list of PVCs
|
||||
pvcs = api.list_pvcs(namespace)
|
||||
content = [utils.parse_pvc(pvc) for pvc in pvcs.items]
|
||||
|
||||
return api.success_response("pvcs", content)
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
from flask import request
|
||||
|
||||
from kubeflow.kubeflow.crud_backend import api, decorators, logging
|
||||
|
||||
from ...common import form
|
||||
from . import bp
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@bp.route("/api/namespaces/<namespace>/pvcs", methods=["POST"])
|
||||
@decorators.request_is_json_type
|
||||
@decorators.required_body_params("name", "mode", "class", "size", "type")
|
||||
def post_pvc(namespace):
|
||||
body = request.get_json()
|
||||
log.info("Received body: %s", body)
|
||||
|
||||
pvc = form.pvc_from_dict(body, namespace)
|
||||
|
||||
log.info("Creating PVC '%s'...", pvc)
|
||||
api.create_pvc(pvc, namespace)
|
||||
log.info("Successfully created PVC %s/%s", namespace, pvc.metadata.name)
|
||||
|
||||
return api.success_response("message", "PVC created successfully.")
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import os
|
||||
|
||||
from kubeflow.kubeflow.crud_backend import config, logging, rok
|
||||
|
||||
from ..common import create_app as create_default_app
|
||||
from .routes import bp as routes_bp
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_app(name=__name__, cfg: config.Config = None):
|
||||
cfg = config.Config() if cfg is None else cfg
|
||||
|
||||
# Properly set the static serving directory
|
||||
static_dir = os.path.join(
|
||||
os.path.abspath(os.path.dirname(__file__)), "static"
|
||||
)
|
||||
|
||||
app = create_default_app(name, static_dir, cfg)
|
||||
|
||||
log.info("Setting STATIC_DIR to: " + static_dir)
|
||||
app.config["STATIC_DIR"] = static_dir
|
||||
|
||||
# Register the app's blueprints
|
||||
app.register_blueprint(rok.bp)
|
||||
app.register_blueprint(routes_bp)
|
||||
|
||||
return app
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
apiVersion: kubeflow.org/v1alpha1
|
||||
kind: PVCViewer
|
||||
metadata:
|
||||
name: {name}
|
||||
namespace: {namespace}
|
||||
spec:
|
||||
pvc: {name}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
from flask import Blueprint
|
||||
|
||||
bp = Blueprint("rok_routes", __name__)
|
||||
|
||||
from . import get, post, delete # noqa: F401, E402
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
from werkzeug import exceptions
|
||||
|
||||
from kubeflow.kubeflow.crud_backend import api, logging
|
||||
|
||||
from ...common import utils as common_utils
|
||||
from .. import utils as rok_utils
|
||||
from . import bp
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@bp.route("/api/namespaces/<namespace>/pvcs/<pvc>", methods=["DELETE"])
|
||||
def delete_pvc(pvc, namespace):
|
||||
"""
|
||||
Delete a PVC, even if it is only mounted on PVCViewer Pods.
|
||||
Get list of PVCViewers that use the requested PVC. If no other Pods
|
||||
are using that PVC then delete the Viewer Pods as well as the PVC.
|
||||
"""
|
||||
pods = common_utils.get_pods_using_pvc(pvc, namespace)
|
||||
non_viewer_pods = [p for p in pods if not rok_utils.is_viewer_pod(p)]
|
||||
if non_viewer_pods:
|
||||
pod_names = [p.metadata.name for p in non_viewer_pods]
|
||||
raise exceptions.Conflict("Cannot delete PVC '%s' because it is being"
|
||||
" used by pods: %s" % (pvc, pod_names))
|
||||
|
||||
log.info("Deleting PVC %s/%s...", namespace, pvc)
|
||||
api.delete_pvc(pvc, namespace)
|
||||
log.info("Successfully deleted PVC %s/%s", namespace, pvc)
|
||||
|
||||
return api.success_response("message",
|
||||
"PVC %s successfully deleted." % pvc)
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
from kubeflow.kubeflow.crud_backend import api, logging
|
||||
|
||||
from ...common import status
|
||||
from .. import utils
|
||||
from . import bp
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@bp.route("/api/namespaces/<namespace>/pvcs")
|
||||
def get_pvcs(namespace):
|
||||
# Get the active viewers for each pvc as a dictionary
|
||||
# with Key:PVC name and Value:Status of Viewer
|
||||
viewers_lst = api.list_custom_rsrc(*utils.PVCVIEWER, namespace)
|
||||
|
||||
viewers = {}
|
||||
for v in viewers_lst["items"]:
|
||||
pvc_name = v["spec"]["pvc"]
|
||||
viewers[pvc_name] = status.viewer_status(v)
|
||||
|
||||
# Return the list of PVCs and the corresponding Viewer's state
|
||||
pvcs = api.list_pvcs(namespace)
|
||||
content = [utils.parse_pvc(pvc, viewers) for pvc in pvcs.items]
|
||||
|
||||
return api.success_response("pvcs", content)
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
from flask import request
|
||||
|
||||
from kubeflow.kubeflow.crud_backend import api, decorators, logging
|
||||
|
||||
from ...common import form
|
||||
from .. import utils as rok_utils
|
||||
from . import bp
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@bp.route("/api/namespaces/<namespace>/viewers", methods=["POST"])
|
||||
@decorators.request_is_json_type
|
||||
@decorators.required_body_params("name")
|
||||
def post_viewer(namespace):
|
||||
body = request.get_json()
|
||||
log.info("Received body: %s", body)
|
||||
|
||||
name = body["name"]
|
||||
viewer = rok_utils.load_pvcviewer_yaml_template(name=name,
|
||||
namespace=namespace)
|
||||
|
||||
log.info("Creating PVCViewer '%s'...", viewer)
|
||||
api.create_custom_rsrc(*rok_utils.PVCVIEWER, viewer, namespace)
|
||||
log.info("Successfully created PVCViewer %s/%s", namespace, name)
|
||||
|
||||
return api.success_response("message", "PVCViewer created successfully.")
|
||||
|
||||
|
||||
@bp.route("/api/namespaces/<namespace>/pvcs", methods=["POST"])
|
||||
@decorators.request_is_json_type
|
||||
@decorators.required_body_params("name", "mode", "class", "size", "type")
|
||||
def post_pvc(namespace):
|
||||
body = request.get_json()
|
||||
log.info("Received body: ", body)
|
||||
|
||||
pvc = form.pvc_from_dict(body, namespace)
|
||||
rok_utils.add_pvc_rok_annotations(pvc, body)
|
||||
|
||||
log.info("Creating PVC '%s'...", pvc)
|
||||
api.create_pvc(pvc, namespace)
|
||||
log.info("Successfully created PVC %s/%s", namespace, pvc.metadata.name)
|
||||
|
||||
return api.success_response("message", "PVC created successfully.")
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
import os
|
||||
|
||||
from kubeflow.kubeflow.crud_backend import helpers, status
|
||||
|
||||
from ..common import utils as common_utils
|
||||
|
||||
KIND = "PVCViewer"
|
||||
GROUP = "kubeflow.org"
|
||||
VERSION = "v1alpha1"
|
||||
PLURAL = "pvcviewers"
|
||||
PVCVIEWER = [GROUP, VERSION, PLURAL]
|
||||
|
||||
PVCVIEWER_YAML = os.path.join(
|
||||
os.path.abspath(os.path.dirname(__file__)), "pvcviewer.yaml"
|
||||
)
|
||||
|
||||
|
||||
def load_pvcviewer_yaml_template(**kwargs):
|
||||
"""
|
||||
kwargs: the parameters to be replaced in the yaml
|
||||
|
||||
Reads the yaml for the web app's custom resource, replaces the variables
|
||||
and returns it as a python dict.
|
||||
"""
|
||||
return helpers.load_param_yaml(PVCVIEWER_YAML, **kwargs)
|
||||
|
||||
|
||||
def add_pvc_rok_annotations(pvc, body):
|
||||
"""Set the necessary Rok annotations"""
|
||||
annotations = pvc.metadata.annotations or {}
|
||||
|
||||
if body["type"] == "rok_snapshot" and "snapshot" in body:
|
||||
annotations["rok/origin"] = body["snapshot"]
|
||||
|
||||
labels = pvc.metadata.labels or {}
|
||||
labels["component"] = "singleuser-storage"
|
||||
|
||||
pvc.metadata.annotations = annotations
|
||||
pvc.metadata.labels = labels
|
||||
|
||||
|
||||
def parse_pvc(pvc, viewers):
|
||||
"""
|
||||
pvc: client.V1PersistentVolumeClaim
|
||||
viewers: dict(Key:PVC Name, Value: Viewer's Status)
|
||||
|
||||
Process the PVC and format it as the UI expects it. If a Viewer is active
|
||||
for that PVC, then include this information
|
||||
"""
|
||||
parsed_pvc = common_utils.parse_pvc(pvc)
|
||||
parsed_pvc["viewer"] = viewers.get(pvc.metadata.name,
|
||||
status.STATUS_PHASE.UNINITIALIZED)
|
||||
|
||||
return parsed_pvc
|
||||
|
||||
|
||||
def get_viewer_owning_pod(pod):
|
||||
"""
|
||||
Return a list of PVCViewer names that own the Pod
|
||||
"""
|
||||
owner_refs = pod.metadata.owner_references
|
||||
for owner_ref in owner_refs:
|
||||
if owner_ref.kind == KIND:
|
||||
return owner_ref.name
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def is_viewer_pod(pod):
|
||||
"""
|
||||
Checks if the given Pod belongs to a PVCViewer
|
||||
"""
|
||||
return get_viewer_owning_pod(pod) is not None
|
||||
|
||||
|
||||
def get_viewers_owning_pods(pods):
|
||||
"""
|
||||
Return the name of PVCViewers that own a subset of the given Pods
|
||||
"""
|
||||
viewers = []
|
||||
for pod in pods:
|
||||
if not is_viewer_pod(pod):
|
||||
continue
|
||||
|
||||
viewers.append(get_viewer_owning_pod(pod))
|
||||
|
||||
return viewers
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import os
|
||||
import sys
|
||||
|
||||
from apps import default, rok
|
||||
from kubeflow.kubeflow.crud_backend import config, logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_config(mode):
|
||||
"""Return a config based on the selected mode."""
|
||||
config_classes = {
|
||||
config.BackendMode.DEVELOPMENT.value: config.DevConfig,
|
||||
config.BackendMode.DEVELOPMENT_FULL.value: config.DevConfig,
|
||||
config.BackendMode.PRODUCTION.value: config.ProdConfig,
|
||||
config.BackendMode.PRODUCTION_FULL.value: config.ProdConfig,
|
||||
}
|
||||
cfg_class = config_classes.get(mode)
|
||||
if not cfg_class:
|
||||
raise RuntimeError("Backend mode '%s' is not implemented. Choose one"
|
||||
" of %s" % (mode, list(config_classes.keys())))
|
||||
return cfg_class()
|
||||
|
||||
|
||||
APP_NAME = os.environ.get("APP_NAME", "Volumes Web App")
|
||||
BACKEND_MODE = os.environ.get("BACKEND_MODE",
|
||||
config.BackendMode.PRODUCTION.value)
|
||||
UI_FLAVOR = os.environ.get("UI_FLAVOR", "default")
|
||||
PREFIX = os.environ.get("APP_PREFIX", "/")
|
||||
|
||||
cfg = get_config(BACKEND_MODE)
|
||||
cfg.PREFIX = PREFIX
|
||||
|
||||
# Load the app based on UI_FLAVOR env var
|
||||
if UI_FLAVOR == "default":
|
||||
app = default.create_app(APP_NAME, cfg)
|
||||
elif UI_FLAVOR == "rok":
|
||||
app = rok.create_app(APP_NAME, cfg)
|
||||
else:
|
||||
log.error("No UI flavor for '%s'", UI_FLAVOR)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
||||
|
|
@ -0,0 +1 @@
|
|||
gunicorn
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
# Editor configuration, see https://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# compiled output
|
||||
/dist
|
||||
/tmp
|
||||
/out-tsc
|
||||
# Only exists if Bazel was run
|
||||
/bazel-out
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# profiling files
|
||||
chrome-profiler-events*.json
|
||||
speed-measure-plugin*.json
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.history/*
|
||||
|
||||
# misc
|
||||
/.sass-cache
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
testem.log
|
||||
/typings
|
||||
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Rok files
|
||||
**/browse-in-rok-gray.svg
|
||||
**/browse-in-rok-grey.svg
|
||||
**/browse-in-rok-blue.svg
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"printWidth": 80,
|
||||
"useTabs": false,
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "avoid",
|
||||
"proseWrap": "preserve"
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
# Frontend
|
||||
|
||||
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 8.3.20.
|
||||
|
||||
## Development server
|
||||
|
||||
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
|
||||
|
||||
## Code scaffolding
|
||||
|
||||
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
|
||||
|
||||
## Build
|
||||
|
||||
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
|
||||
|
||||
## Running end-to-end tests
|
||||
|
||||
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
|
||||
|
||||
## Further help
|
||||
|
||||
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"frontend": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"style": "scss"
|
||||
}
|
||||
},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"preserveSymlinks": true,
|
||||
"outputPath": "dist/frontend",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"aot": false,
|
||||
"assets": ["src/favicon.ico", "src/assets"],
|
||||
"styles": ["src/styles.scss"],
|
||||
"scripts": [],
|
||||
"crossOrigin": "use-credentials"
|
||||
},
|
||||
"configurations": {
|
||||
"rok": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.rok.ts"
|
||||
}
|
||||
]
|
||||
},
|
||||
"rok-prod": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.rok.prod.ts"
|
||||
}
|
||||
],
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"extractCss": true,
|
||||
"namedChunks": false,
|
||||
"aot": true,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true,
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "2mb",
|
||||
"maximumError": "5mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "6kb",
|
||||
"maximumError": "10kb"
|
||||
}
|
||||
]
|
||||
},
|
||||
"production": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}
|
||||
],
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"extractCss": true,
|
||||
"namedChunks": false,
|
||||
"aot": true,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true,
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "2mb",
|
||||
"maximumError": "5mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "6kb",
|
||||
"maximumError": "10kb"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"options": {
|
||||
"browserTarget": "frontend:build"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "frontend:build:production"
|
||||
},
|
||||
"rok": {
|
||||
"browserTarget": "frontend:build:rok"
|
||||
}
|
||||
}
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "frontend:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"main": "src/test.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"karmaConfig": "karma.conf.js",
|
||||
"assets": ["src/favicon.ico", "src/assets"],
|
||||
"styles": ["src/styles.scss"],
|
||||
"scripts": []
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": [
|
||||
"tsconfig.app.json",
|
||||
"tsconfig.spec.json",
|
||||
"e2e/tsconfig.json"
|
||||
],
|
||||
"exclude": ["**/node_modules/**"]
|
||||
}
|
||||
},
|
||||
"e2e": {
|
||||
"builder": "@angular-devkit/build-angular:protractor",
|
||||
"options": {
|
||||
"protractorConfig": "e2e/protractor.conf.js",
|
||||
"devServerTarget": "frontend:serve"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"devServerTarget": "frontend:serve:production"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultProject": "frontend"
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
|
||||
# For additional information regarding the format and rule options, please see:
|
||||
# https://github.com/browserslist/browserslist#queries
|
||||
|
||||
# You can see what browsers were selected by your queries by running:
|
||||
# npx browserslist
|
||||
|
||||
> 0.5%
|
||||
last 2 versions
|
||||
Firefox ESR
|
||||
not dead
|
||||
not IE 9-11 # For IE 9-11 support, remove 'not'.
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
// @ts-check
|
||||
// Protractor configuration file, see link for more information
|
||||
// https://github.com/angular/protractor/blob/master/lib/config.ts
|
||||
|
||||
const { SpecReporter } = require('jasmine-spec-reporter');
|
||||
|
||||
/**
|
||||
* @type { import("protractor").Config }
|
||||
*/
|
||||
exports.config = {
|
||||
allScriptsTimeout: 11000,
|
||||
specs: [
|
||||
'./src/**/*.e2e-spec.ts'
|
||||
],
|
||||
capabilities: {
|
||||
browserName: 'chrome'
|
||||
},
|
||||
directConnect: true,
|
||||
baseUrl: 'http://localhost:4200/',
|
||||
framework: 'jasmine',
|
||||
jasmineNodeOpts: {
|
||||
showColors: true,
|
||||
defaultTimeoutInterval: 30000,
|
||||
print: function() {}
|
||||
},
|
||||
onPrepare() {
|
||||
require('ts-node').register({
|
||||
project: require('path').join(__dirname, './tsconfig.json')
|
||||
});
|
||||
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { AppPage } from './app.po';
|
||||
import { browser, logging } from 'protractor';
|
||||
|
||||
describe('workspace-project App', () => {
|
||||
let page: AppPage;
|
||||
|
||||
beforeEach(() => {
|
||||
page = new AppPage();
|
||||
});
|
||||
|
||||
it('should display welcome message', () => {
|
||||
page.navigateTo();
|
||||
expect(page.getTitleText()).toEqual('frontend app is running!');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Assert that there are no errors emitted from the browser
|
||||
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
|
||||
expect(logs).not.toContain(jasmine.objectContaining({
|
||||
level: logging.Level.SEVERE,
|
||||
} as logging.Entry));
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { browser, by, element } from 'protractor';
|
||||
|
||||
export class AppPage {
|
||||
navigateTo() {
|
||||
return browser.get(browser.baseUrl) as Promise<any>;
|
||||
}
|
||||
|
||||
getTitleText() {
|
||||
return element(by.css('app-root .content span')).getText() as Promise<string>;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/e2e",
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"types": [
|
||||
"jasmine",
|
||||
"jasminewd2",
|
||||
"node"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
// Karma configuration file, see link for more information
|
||||
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
basePath: '',
|
||||
frameworks: ['jasmine', '@angular-devkit/build-angular'],
|
||||
plugins: [
|
||||
require('karma-jasmine'),
|
||||
require('karma-chrome-launcher'),
|
||||
require('karma-jasmine-html-reporter'),
|
||||
require('karma-coverage-istanbul-reporter'),
|
||||
require('@angular-devkit/build-angular/plugins/karma')
|
||||
],
|
||||
client: {
|
||||
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
||||
},
|
||||
coverageIstanbulReporter: {
|
||||
dir: require('path').join(__dirname, './coverage/frontend'),
|
||||
reports: ['html', 'lcovonly', 'text-summary'],
|
||||
fixWebpackSourcePaths: true
|
||||
},
|
||||
reporters: ['progress', 'kjhtml'],
|
||||
port: 9876,
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: true,
|
||||
browsers: ['Chrome'],
|
||||
singleRun: false,
|
||||
restartOnFileChange: true
|
||||
});
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,63 @@
|
|||
{
|
||||
"name": "frontend",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"build": "npm run copyLibAssets && ng build --prod --base-href /volumes/ --deploy-url static/",
|
||||
"build:watch": "npm run copyLibAssets && ng build --watch --deploy-url static/ --outputPath ../backend/apps/default/static/ --outputHashing all",
|
||||
"build:watch:rok": "npm run copyLibAssets && ng build --watch --deploy-url static/ --configuration=rok --outputPath ../backend/apps/rok/static/ --outputHashing all",
|
||||
"copyLibAssets": "cp ./node_modules/kubeflow/assets/* ./src/assets/",
|
||||
"test": "ng test",
|
||||
"lint": "ng lint",
|
||||
"e2e": "ng e2e"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "~8.2.14",
|
||||
"@angular/cdk": "^8.2.3",
|
||||
"@angular/common": "~8.2.14",
|
||||
"@angular/compiler": "~8.2.14",
|
||||
"@angular/cdk-experimental": "^8.2.3",
|
||||
"@angular/core": "~8.2.14",
|
||||
"@angular/forms": "~8.2.14",
|
||||
"@angular/material": "^8.2.3",
|
||||
"@angular/platform-browser": "~8.2.14",
|
||||
"@angular/platform-browser-dynamic": "~8.2.14",
|
||||
"@angular/router": "~8.2.14",
|
||||
"@fortawesome/angular-fontawesome": "^0.5.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.26",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.12.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.12.0",
|
||||
"date-fns": "^1.29.0",
|
||||
"lodash-es": "^4.17.11",
|
||||
"hammerjs": "^2.0.8",
|
||||
"lodash": "^4.17.15",
|
||||
"material-icons": "^0.3.1",
|
||||
"raw-loader": "^4.0.0",
|
||||
"rxjs": "~6.4.0",
|
||||
"tslib": "^1.10.0",
|
||||
"zone.js": "~0.9.1",
|
||||
"@kubernetes/client-node": "^0.12.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "~0.803.20",
|
||||
"@angular/cli": "~8.3.20",
|
||||
"@angular/compiler-cli": "~8.2.14",
|
||||
"@angular/language-service": "~8.2.14",
|
||||
"@types/node": "~8.9.4",
|
||||
"@types/jasmine": "~3.3.8",
|
||||
"@types/jasminewd2": "~2.0.3",
|
||||
"codelyzer": "^5.0.0",
|
||||
"jasmine-core": "~3.4.0",
|
||||
"jasmine-spec-reporter": "~4.2.1",
|
||||
"karma": "~4.1.0",
|
||||
"karma-chrome-launcher": "~2.2.0",
|
||||
"karma-coverage-istanbul-reporter": "~2.0.1",
|
||||
"karma-jasmine": "~2.0.1",
|
||||
"karma-jasmine-html-reporter": "^1.4.0",
|
||||
"protractor": "~5.4.0",
|
||||
"ts-node": "~7.0.0",
|
||||
"tslint": "~5.15.0",
|
||||
"typescript": "~3.5.3"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
import { IndexComponent } from './pages/index/index.component';
|
||||
|
||||
const routes: Routes = [{ path: '', component: IndexComponent }];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class AppRoutingModule {}
|
||||
|
|
@ -0,0 +1 @@
|
|||
<router-outlet></router-outlet>
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import { TestBed, async } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [RouterTestingModule],
|
||||
declarations: [AppComponent],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
it('should create the app', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.debugElement.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
it(`should have as title 'frontend'`, () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.debugElement.componentInstance;
|
||||
expect(app.title).toEqual('frontend');
|
||||
});
|
||||
|
||||
it('should render title', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
fixture.detectChanges();
|
||||
const compiled = fixture.debugElement.nativeElement;
|
||||
expect(compiled.querySelector('.content span').textContent).toContain(
|
||||
'frontend app is running!',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss'],
|
||||
})
|
||||
export class AppComponent {}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { ErrorStateMatcher } from '@angular/material/core';
|
||||
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
import {
|
||||
ResourceTableModule,
|
||||
NamespaceSelectModule,
|
||||
ConfirmDialogModule,
|
||||
FormModule,
|
||||
ImmediateErrorStateMatcher,
|
||||
KubeflowModule,
|
||||
} from 'kubeflow';
|
||||
|
||||
import { IndexComponent } from './pages/index/index.component';
|
||||
import { FormDefaultComponent } from './pages/form/form-default/form-default.component';
|
||||
import { FormRokComponent } from './pages/form/form-rok/form-rok.component';
|
||||
import { IndexDefaultComponent } from './pages/index/index-default/index-default.component';
|
||||
import { IndexRokComponent } from './pages/index/index-rok/index-rok.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
IndexComponent,
|
||||
FormDefaultComponent,
|
||||
FormRokComponent,
|
||||
IndexDefaultComponent,
|
||||
IndexRokComponent,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
CommonModule,
|
||||
AppRoutingModule,
|
||||
ResourceTableModule,
|
||||
NamespaceSelectModule,
|
||||
ConfirmDialogModule,
|
||||
FormModule,
|
||||
KubeflowModule,
|
||||
],
|
||||
providers: [
|
||||
{ provide: ErrorStateMatcher, useClass: ImmediateErrorStateMatcher },
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
entryComponents: [FormDefaultComponent, FormRokComponent],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
<div class="form--container" mat-dialog-content>
|
||||
<form novalidate (ngSubmit)="onSubmit()" [formGroup]="formCtrl">
|
||||
<lib-form-section
|
||||
title="New Volume"
|
||||
text="Create a new empty Volume."
|
||||
icon="fa:fas:hdd"
|
||||
>
|
||||
</lib-form-section>
|
||||
|
||||
<!--Name / Namespace-->
|
||||
<lib-form-name-namespace-inputs
|
||||
[nameControl]="formCtrl.get('name')"
|
||||
[namespaceControl]="formCtrl.get('namespace')"
|
||||
resourceName="Volume"
|
||||
[existingNames]="pvcNames"
|
||||
>
|
||||
</lib-form-name-namespace-inputs>
|
||||
|
||||
<!--Snapshot Chooser-->
|
||||
<mat-form-field
|
||||
*ngIf="formCtrl.get('type').value === 'snapshot'"
|
||||
appearance="outline"
|
||||
class="wide"
|
||||
>
|
||||
<mat-label>Snapshot</mat-label>
|
||||
<mat-select formControlName="snapshot"> </mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<!--Size-->
|
||||
<lib-positive-number-input
|
||||
[sizeControl]="formCtrl.get('size')"
|
||||
min="1"
|
||||
step="1"
|
||||
label="Volume size in Gi"
|
||||
></lib-positive-number-input>
|
||||
|
||||
<!--Storage Class-->
|
||||
<mat-form-field appearance="outline" class="wide">
|
||||
<mat-label>Storage Class</mat-label>
|
||||
<mat-select formControlName="class">
|
||||
<mat-option value="{none}">None</mat-option>
|
||||
<mat-option *ngFor="let sc of storageClasses" [value]="sc">
|
||||
{{ sc }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<!--Access Mode-->
|
||||
<mat-form-field appearance="outline" class="wide">
|
||||
<mat-label>Access Mode</mat-label>
|
||||
<mat-select formControlName="mode">
|
||||
<mat-option value="ReadWriteOnce">ReadWriteOnce</mat-option>
|
||||
<mat-option value="ReadOnlyMany">ReadOnlyMany</mat-option>
|
||||
<mat-option value="ReadWriteMany">ReadWriteMany</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
class="form--button-margin"
|
||||
type="submit"
|
||||
[disabled]="!formCtrl.valid || blockSubmit"
|
||||
>
|
||||
CREATE
|
||||
</button>
|
||||
|
||||
<button mat-raised-button type="button" (click)="onCancel()">
|
||||
CANCEL
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { FormDefaultComponent } from './form-default.component';
|
||||
|
||||
describe('FormDefaultComponent', () => {
|
||||
let component: FormDefaultComponent;
|
||||
let fixture: ComponentFixture<FormDefaultComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [FormDefaultComponent],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(FormDefaultComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import {
|
||||
FormBuilder,
|
||||
FormGroup,
|
||||
Validators,
|
||||
FormControl,
|
||||
ValidatorFn,
|
||||
} from '@angular/forms';
|
||||
import { Subscription } from 'rxjs';
|
||||
import {
|
||||
NamespaceService,
|
||||
getExistingNameValidator,
|
||||
dns1035Validator,
|
||||
getNameError,
|
||||
DIALOG_RESP,
|
||||
} from 'kubeflow';
|
||||
import { VWABackendService } from 'src/app/services/backend.service';
|
||||
import { PVCPostObject } from 'src/app/types';
|
||||
import { MatDialogRef } from '@angular/material';
|
||||
|
||||
@Component({
|
||||
selector: 'app-form-default',
|
||||
templateUrl: './form-default.component.html',
|
||||
styleUrls: ['./form-default.component.scss'],
|
||||
})
|
||||
export class FormDefaultComponent implements OnInit, OnDestroy {
|
||||
public TYPE_ROK_SNAPSHOT = 'rok_snapshot';
|
||||
public TYPE_EMPTY = 'empty';
|
||||
|
||||
public subs = new Subscription();
|
||||
public formCtrl: FormGroup;
|
||||
public blockSubmit = false;
|
||||
|
||||
public currNamespace = '';
|
||||
public pvcNames = new Set<string>();
|
||||
public storageClasses: string[] = [];
|
||||
public defaultStorageClass: string;
|
||||
|
||||
constructor(
|
||||
public ns: NamespaceService,
|
||||
public fb: FormBuilder,
|
||||
public backend: VWABackendService,
|
||||
public dialog: MatDialogRef<FormDefaultComponent>,
|
||||
) {
|
||||
this.formCtrl = this.fb.group({
|
||||
type: ['empty', [Validators.required]],
|
||||
name: ['', [Validators.required]],
|
||||
namespace: ['', [Validators.required]],
|
||||
size: [10, []],
|
||||
class: ['$empty', [Validators.required]],
|
||||
mode: ['ReadWriteOnce', [Validators.required]],
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.formCtrl.controls.namespace.disable();
|
||||
|
||||
this.backend.getStorageClasses().subscribe(storageClasses => {
|
||||
this.storageClasses = storageClasses;
|
||||
|
||||
// Once we have the list of storage classes, get the
|
||||
// default one from the backend and make it the preselected
|
||||
this.backend.getDefaultStorageClass().subscribe(defaultClass => {
|
||||
this.defaultStorageClass = defaultClass;
|
||||
this.formCtrl.controls.class.setValue(defaultClass);
|
||||
});
|
||||
});
|
||||
|
||||
this.subs.add(
|
||||
this.ns.getSelectedNamespace().subscribe(ns => {
|
||||
this.currNamespace = ns;
|
||||
this.formCtrl.controls.namespace.setValue(ns);
|
||||
|
||||
this.backend.getPVCs(ns).subscribe(pvcs => {
|
||||
this.pvcNames.clear();
|
||||
pvcs.forEach(pvc => this.pvcNames.add(pvc.name));
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.subs.unsubscribe();
|
||||
}
|
||||
|
||||
public onSubmit() {
|
||||
const pvc: PVCPostObject = JSON.parse(JSON.stringify(this.formCtrl.value));
|
||||
pvc.size = pvc.size + 'Gi';
|
||||
this.blockSubmit = true;
|
||||
|
||||
this.backend.createPVC(this.currNamespace, pvc).subscribe(
|
||||
result => {
|
||||
this.dialog.close(DIALOG_RESP.ACCEPT);
|
||||
},
|
||||
error => {
|
||||
this.blockSubmit = false;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public onCancel() {
|
||||
this.dialog.close(DIALOG_RESP.CANCEL);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
<div class="form--container" mat-dialog-content>
|
||||
<form novalidate (ngSubmit)="onSubmit()" [formGroup]="formCtrl">
|
||||
<lib-form-section
|
||||
title="New Volume"
|
||||
text="Create a new empty Volume or clone an existing snapshot."
|
||||
icon="fa:fas:hdd"
|
||||
>
|
||||
</lib-form-section>
|
||||
|
||||
<!--Volume Type-->
|
||||
<mat-form-field appearance="outline" class="wide">
|
||||
<mat-label>Volume Type</mat-label>
|
||||
<mat-select formControlName="type">
|
||||
<mat-option [value]="TYPE_EMPTY">Empty Volume</mat-option>
|
||||
<mat-option [value]="TYPE_ROK_SNAPSHOT">
|
||||
Clone an existing Rok snapshot
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<!--Name / Namespace-->
|
||||
<lib-form-name-namespace-inputs
|
||||
[nameControl]="formCtrl.get('name')"
|
||||
[namespaceControl]="formCtrl.get('namespace')"
|
||||
resourceName="Volume"
|
||||
[existingNames]="pvcNames"
|
||||
>
|
||||
</lib-form-name-namespace-inputs>
|
||||
|
||||
<!--Snapshot Chooser-->
|
||||
<lib-rok-url-input
|
||||
*ngIf="formCtrl.get('type').value === TYPE_ROK_SNAPSHOT"
|
||||
[control]="formCtrl.controls.snapshot"
|
||||
[mode]="'file'"
|
||||
(urlEntered)="rokUrlChanged($event)"
|
||||
></lib-rok-url-input>
|
||||
|
||||
<!--Size-->
|
||||
<lib-positive-number-input
|
||||
[sizeControl]="formCtrl.get('size')"
|
||||
min="1"
|
||||
step="1"
|
||||
label="Volume size in Gi"
|
||||
></lib-positive-number-input>
|
||||
|
||||
<!--Storage Class-->
|
||||
<mat-form-field appearance="outline" class="wide">
|
||||
<mat-label>Storage Class</mat-label>
|
||||
<mat-select formControlName="class">
|
||||
<mat-option
|
||||
value="{none}"
|
||||
[disabled]="classIsDisabled('{none}')"
|
||||
[matTooltip]="classTooltip('{none}')"
|
||||
>
|
||||
None</mat-option
|
||||
>
|
||||
<mat-option
|
||||
*ngFor="let sc of storageClasses"
|
||||
[value]="sc"
|
||||
[disabled]="classIsDisabled(sc)"
|
||||
[matTooltip]="classTooltip(sc)"
|
||||
>
|
||||
{{ sc }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="formCtrl.controls.class.hasError('notRokClass')">
|
||||
Must use a Rok provided Storage Class when cloning a snapshot from Rok
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<!--Access Mode-->
|
||||
<mat-form-field appearance="outline" class="wide">
|
||||
<mat-label>Access Mode</mat-label>
|
||||
<mat-select formControlName="mode">
|
||||
<mat-option value="ReadWriteOnce">ReadWriteOnce</mat-option>
|
||||
<mat-option value="ReadOnlyMany">ReadOnlyMany</mat-option>
|
||||
<mat-option value="ReadWriteMany">ReadWriteMany</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
class="form--button-margin"
|
||||
type="submit"
|
||||
[disabled]="!formCtrl.valid || blockSubmit"
|
||||
>
|
||||
CREATE
|
||||
</button>
|
||||
|
||||
<button mat-raised-button type="button" (click)="onCancel()">
|
||||
CANCEL
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { FormRokComponent } from './form-rok.component';
|
||||
|
||||
describe('FormRokComponent', () => {
|
||||
let component: FormRokComponent;
|
||||
let fixture: ComponentFixture<FormRokComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [FormRokComponent],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(FormRokComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { FormBuilder, Validators, FormControl } from '@angular/forms';
|
||||
import {
|
||||
NamespaceService,
|
||||
RokService,
|
||||
DIALOG_RESP,
|
||||
SnackBarService,
|
||||
SnackType,
|
||||
rokUrlValidator,
|
||||
updateNonDirtyControl,
|
||||
} from 'kubeflow';
|
||||
|
||||
import { VWABackendService } from 'src/app/services/backend.service';
|
||||
import { PVCPostObject } from 'src/app/types';
|
||||
import { MatDialogRef } from '@angular/material';
|
||||
import { FormDefaultComponent } from '../form-default/form-default.component';
|
||||
import { environment } from '@app/environment';
|
||||
import { rokStorageClassValidator } from './utils';
|
||||
import { concatMap } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'app-form-rok',
|
||||
templateUrl: './form-rok.component.html',
|
||||
styleUrls: [
|
||||
'../form-default/form-default.component.scss',
|
||||
'./form-rok.component.scss',
|
||||
],
|
||||
})
|
||||
// TODO: Use an abstract class to eliminate common code
|
||||
export class FormRokComponent extends FormDefaultComponent
|
||||
implements OnInit, OnDestroy {
|
||||
public env = environment;
|
||||
|
||||
private rokManagedStorageClasses: string[] = [];
|
||||
|
||||
constructor(
|
||||
public ns: NamespaceService,
|
||||
public fb: FormBuilder,
|
||||
public backend: VWABackendService,
|
||||
public dialog: MatDialogRef<FormDefaultComponent>,
|
||||
public snack: SnackBarService,
|
||||
public rok: RokService,
|
||||
) {
|
||||
super(ns, fb, backend, dialog);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
super.ngOnInit();
|
||||
|
||||
this.formCtrl.get('size').setValue(5);
|
||||
|
||||
this.subs.add(
|
||||
this.formCtrl.get('type').valueChanges.subscribe(volType => {
|
||||
if (volType === this.TYPE_EMPTY) {
|
||||
this.formCtrl.removeControl('snapshot');
|
||||
this.formCtrl.get('class').setValidators([Validators.required]);
|
||||
updateNonDirtyControl(this.formCtrl.get('size'), 5);
|
||||
updateNonDirtyControl(
|
||||
this.formCtrl.get('class'),
|
||||
this.defaultStorageClass,
|
||||
);
|
||||
} else {
|
||||
// Add a Rok URL control
|
||||
this.formCtrl.addControl(
|
||||
'snapshot',
|
||||
new FormControl(
|
||||
'',
|
||||
[Validators.required],
|
||||
[rokUrlValidator(this.rok)],
|
||||
),
|
||||
);
|
||||
|
||||
// Set the size value to none to force the user to handle this field
|
||||
updateNonDirtyControl(this.formCtrl.get('size'), null);
|
||||
|
||||
// Ensure that the StorageClass used is provisioned from Rok
|
||||
this.formCtrl
|
||||
.get('class')
|
||||
.setValidators([
|
||||
Validators.required,
|
||||
rokStorageClassValidator(this.rokManagedStorageClasses),
|
||||
]);
|
||||
}
|
||||
|
||||
this.formCtrl.get('class').updateValueAndValidity({ onlySelf: true });
|
||||
}),
|
||||
);
|
||||
|
||||
// Get the list of Rok managed storage classes
|
||||
this.rok.getRokManagedStorageClasses().subscribe(classes => {
|
||||
this.rokManagedStorageClasses = classes;
|
||||
|
||||
// update the validators if type is Rok Snapshot
|
||||
if (this.formCtrl.get('type').value === this.TYPE_ROK_SNAPSHOT) {
|
||||
this.formCtrl
|
||||
.get('class')
|
||||
.setValidators([
|
||||
Validators.required,
|
||||
rokStorageClassValidator(this.rokManagedStorageClasses),
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
this.rok.initCSRF();
|
||||
}
|
||||
|
||||
public rokUrlChanged(url: string) {
|
||||
this.rok.getObjectMetadata(url).subscribe({
|
||||
next: headers => {
|
||||
// Autofill the name
|
||||
let size = parseInt(headers.get('content-length'), 10);
|
||||
size = size / Math.pow(1024, 3);
|
||||
this.formCtrl.get('size').setValue(size);
|
||||
this.snack.open(
|
||||
'Successfully retrieved snapshot information.',
|
||||
SnackType.Success,
|
||||
3000,
|
||||
);
|
||||
},
|
||||
error: err => {
|
||||
this.snack.open(`'${url}' is not a valid Rok URL`, SnackType.Error, -1);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Handle the Storage Classes Select state
|
||||
*/
|
||||
public classIsDisabled(name: string) {
|
||||
const volType = this.formCtrl.controls.type.value;
|
||||
if (volType !== this.TYPE_ROK_SNAPSHOT) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.rokManagedStorageClasses.includes(name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public classTooltip(name: string) {
|
||||
if (!this.classIsDisabled(name)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return 'This Storage Class is not backed by Rok';
|
||||
}
|
||||
|
||||
public onSubmit() {
|
||||
// TODO: Could use a lodash helper instead
|
||||
const pvc: PVCPostObject = JSON.parse(JSON.stringify(this.formCtrl.value));
|
||||
pvc.size = pvc.size + 'Gi';
|
||||
this.blockSubmit = true;
|
||||
|
||||
// Check if the Rok URL is valid
|
||||
if (pvc.type === this.TYPE_ROK_SNAPSHOT) {
|
||||
this.rok.getObjectMetadata(pvc.snapshot, false).subscribe(headers => {
|
||||
this.backend.createPVC(this.currNamespace, pvc).subscribe(result => {
|
||||
this.dialog.close(DIALOG_RESP.ACCEPT);
|
||||
});
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.backend.createPVC(this.currNamespace, pvc).subscribe(result => {
|
||||
this.dialog.close(DIALOG_RESP.ACCEPT);
|
||||
});
|
||||
this.blockSubmit = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import { ValidatorFn, FormControl } from '@angular/forms';
|
||||
|
||||
export function rokStorageClassValidator(
|
||||
rokManagedClasses: string[],
|
||||
): ValidatorFn {
|
||||
return (control: FormControl): { [key: string]: any } => {
|
||||
const currentClass = control.value;
|
||||
if (!rokManagedClasses.includes(currentClass)) {
|
||||
return { notRokClass: true };
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
import {
|
||||
PropertyValue,
|
||||
StatusValue,
|
||||
ActionListValue,
|
||||
ActionIconValue,
|
||||
TRUNCATE_TEXT_SIZE,
|
||||
TableConfig,
|
||||
} from 'kubeflow';
|
||||
|
||||
export const tableConfig: TableConfig = {
|
||||
title: 'Volumes',
|
||||
newButtonText: 'NEW VOLUME',
|
||||
columns: [
|
||||
{
|
||||
matHeaderCellDef: 'Status',
|
||||
matColumnDef: 'status',
|
||||
value: new StatusValue(),
|
||||
},
|
||||
{
|
||||
matHeaderCellDef: 'Name',
|
||||
matColumnDef: 'name',
|
||||
value: new PropertyValue({
|
||||
field: 'name',
|
||||
tooltipField: 'name',
|
||||
truncate: TRUNCATE_TEXT_SIZE.SMALL,
|
||||
}),
|
||||
},
|
||||
{
|
||||
matHeaderCellDef: 'Age',
|
||||
matColumnDef: 'age',
|
||||
value: new PropertyValue({
|
||||
field: 'age.uptime',
|
||||
tooltipField: 'age.timestamp',
|
||||
}),
|
||||
},
|
||||
{
|
||||
matHeaderCellDef: 'Size',
|
||||
matColumnDef: 'size',
|
||||
value: new PropertyValue({ field: 'capacity' }),
|
||||
},
|
||||
{
|
||||
matHeaderCellDef: 'Access Mode',
|
||||
matColumnDef: 'modes',
|
||||
value: new PropertyValue({ field: 'modes' }),
|
||||
},
|
||||
{
|
||||
matHeaderCellDef: 'Storage Class',
|
||||
matColumnDef: 'class',
|
||||
value: new PropertyValue({ field: 'class' }),
|
||||
},
|
||||
|
||||
// the apps should import the actions they want
|
||||
],
|
||||
};
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import { ActionListValue, ActionIconValue, TableColumn, TableConfig } from 'kubeflow';
|
||||
import { tableConfig } from '../config';
|
||||
|
||||
const actionsCol: TableColumn = {
|
||||
matHeaderCellDef: '',
|
||||
matColumnDef: 'actions',
|
||||
value: new ActionListValue([
|
||||
new ActionIconValue({
|
||||
name: 'delete',
|
||||
tooltip: 'Delete Volume',
|
||||
color: 'warn',
|
||||
field: 'deleteAction',
|
||||
iconReady: 'material:delete',
|
||||
}),
|
||||
]),
|
||||
};
|
||||
|
||||
export const defaultConfig: TableConfig = {
|
||||
title: tableConfig.title,
|
||||
newButtonText: tableConfig.newButtonText,
|
||||
columns: tableConfig.columns.concat(actionsCol),
|
||||
};
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<lib-namespace-select *ngIf="!env.production"></lib-namespace-select>
|
||||
|
||||
<lib-resource-table
|
||||
[config]="config"
|
||||
[data]="processedData"
|
||||
[trackByFn]="pvcTrackByFn"
|
||||
(actionsEmitter)="reactToAction($event)"
|
||||
></lib-resource-table>
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { IndexDefaultComponent } from './index-default.component';
|
||||
|
||||
describe('IndexDefaultComponent', () => {
|
||||
let component: IndexDefaultComponent;
|
||||
let fixture: ComponentFixture<IndexDefaultComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [IndexDefaultComponent],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(IndexDefaultComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,181 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material';
|
||||
import {
|
||||
NamespaceService,
|
||||
ActionEvent,
|
||||
ConfirmDialogService,
|
||||
ExponentialBackoff,
|
||||
STATUS_TYPE,
|
||||
DIALOG_RESP,
|
||||
DialogConfig,
|
||||
SnackBarService,
|
||||
SnackType,
|
||||
} from 'kubeflow';
|
||||
import { defaultConfig } from './config';
|
||||
import { environment } from '@app/environment';
|
||||
import { VWABackendService } from 'src/app/services/backend.service';
|
||||
import { PVCResponseObject, PVCProcessedObject } from 'src/app/types';
|
||||
import { Subscription, Observable, Subject } from 'rxjs';
|
||||
import { isEqual } from 'lodash';
|
||||
import { FormDefaultComponent } from '../../form/form-default/form-default.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-index-default',
|
||||
templateUrl: './index-default.component.html',
|
||||
styleUrls: ['./index-default.component.scss'],
|
||||
})
|
||||
export class IndexDefaultComponent implements OnInit {
|
||||
public env = environment;
|
||||
public poller: ExponentialBackoff;
|
||||
|
||||
public currNamespace = '';
|
||||
public subs = new Subscription();
|
||||
|
||||
public config = defaultConfig;
|
||||
public rawData: PVCResponseObject[] = [];
|
||||
public processedData: PVCProcessedObject[] = [];
|
||||
public pvcsWaitingViewer = new Set<string>();
|
||||
|
||||
constructor(
|
||||
public ns: NamespaceService,
|
||||
public confirmDialog: ConfirmDialogService,
|
||||
public backend: VWABackendService,
|
||||
public dialog: MatDialog,
|
||||
public snackBar: SnackBarService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.poller = new ExponentialBackoff({ interval: 1000, retries: 3 });
|
||||
|
||||
// Poll for new data and reset the poller if different data is found
|
||||
this.subs.add(
|
||||
this.poller.start().subscribe(() => {
|
||||
if (!this.currNamespace) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.backend.getPVCs(this.currNamespace).subscribe(pvcs => {
|
||||
if (!isEqual(this.rawData, pvcs)) {
|
||||
this.rawData = pvcs;
|
||||
|
||||
// Update the frontend's state
|
||||
this.processedData = this.parseIncomingData(pvcs);
|
||||
this.poller.reset();
|
||||
}
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
// Reset the poller whenever the selected namespace changes
|
||||
this.subs.add(
|
||||
this.ns.getSelectedNamespace().subscribe(ns => {
|
||||
this.currNamespace = ns;
|
||||
this.pvcsWaitingViewer = new Set<string>();
|
||||
this.poller.reset();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public reactToAction(a: ActionEvent) {
|
||||
switch (a.action) {
|
||||
case 'newResourceButton': // TODO: could also use enums here
|
||||
this.newResourceClicked();
|
||||
break;
|
||||
case 'delete':
|
||||
this.deleteVolumeClicked(a.data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Functions for handling the action events
|
||||
public newResourceClicked() {
|
||||
const ref = this.dialog.open(FormDefaultComponent, {
|
||||
width: '600px',
|
||||
panelClass: 'form--dialog-padding',
|
||||
});
|
||||
|
||||
ref.afterClosed().subscribe(res => {
|
||||
if (res === DIALOG_RESP.ACCEPT) {
|
||||
this.snackBar.open(
|
||||
'Volume was submitted successfully.',
|
||||
SnackType.Success,
|
||||
2000,
|
||||
);
|
||||
this.poller.reset();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public deleteVolumeClicked(pvc: PVCProcessedObject) {
|
||||
const deleteDialogConfig: DialogConfig = {
|
||||
title: `Are you sure you want to delete this volume? ${pvc.name}`,
|
||||
message: 'Warning: All data in this volume will be lost.',
|
||||
accept: 'DELETE',
|
||||
confirmColor: 'warn',
|
||||
cancel: 'CANCEL',
|
||||
error: '',
|
||||
applying: 'DELETING',
|
||||
width: '600px',
|
||||
};
|
||||
|
||||
const ref = this.confirmDialog.open(pvc.name, deleteDialogConfig);
|
||||
const delSub = ref.componentInstance.applying$.subscribe(applying => {
|
||||
if (!applying) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Close the open dialog only if the DELETE request succeeded
|
||||
this.backend.deletePVC(this.currNamespace, pvc.name).subscribe({
|
||||
next: _ => {
|
||||
this.poller.reset();
|
||||
ref.close(DIALOG_RESP.ACCEPT);
|
||||
},
|
||||
error: err => {
|
||||
// Simplify the error message
|
||||
const errorMsg = err;
|
||||
console.log(err);
|
||||
deleteDialogConfig.error = errorMsg;
|
||||
ref.componentInstance.applying$.next(false);
|
||||
},
|
||||
});
|
||||
|
||||
// DELETE request has succeeded
|
||||
ref.afterClosed().subscribe(res => {
|
||||
delSub.unsubscribe();
|
||||
if (res !== DIALOG_RESP.ACCEPT) {
|
||||
return;
|
||||
}
|
||||
|
||||
pvc.status.phase = STATUS_TYPE.TERMINATING;
|
||||
pvc.status.message = 'Preparing to delete the Volume...';
|
||||
pvc.deleteAction = STATUS_TYPE.UNAVAILABLE;
|
||||
this.pvcsWaitingViewer.delete(pvc.name);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Utility funcs
|
||||
public parseIncomingData(pvcs: PVCResponseObject[]): PVCProcessedObject[] {
|
||||
const pvcsCopy = JSON.parse(JSON.stringify(pvcs)) as PVCProcessedObject[];
|
||||
|
||||
for (const pvc of pvcsCopy) {
|
||||
pvc.deleteAction = this.parseDeletionActionStatus(pvc);
|
||||
pvc.ageValue = pvc.age.uptime;
|
||||
pvc.ageTooltip = pvc.age.timestamp;
|
||||
}
|
||||
|
||||
return pvcsCopy;
|
||||
}
|
||||
|
||||
public parseDeletionActionStatus(pvc: PVCProcessedObject) {
|
||||
if (pvc.status.phase !== STATUS_TYPE.TERMINATING) {
|
||||
return STATUS_TYPE.READY;
|
||||
}
|
||||
|
||||
return STATUS_TYPE.TERMINATING;
|
||||
}
|
||||
|
||||
public pvcTrackByFn(index: number, pvc: PVCProcessedObject) {
|
||||
return `${pvc.name}/${pvc.namespace}/${pvc.capacity}`;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import { ActionListValue, ActionIconValue, TableColumn, TableConfig } from 'kubeflow';
|
||||
import { tableConfig } from '../config';
|
||||
|
||||
const actionsCol: TableColumn = {
|
||||
matHeaderCellDef: '',
|
||||
matColumnDef: 'actions',
|
||||
value: new ActionListValue([
|
||||
new ActionIconValue({
|
||||
name: 'edit',
|
||||
tooltip: 'Browse',
|
||||
color: 'primary',
|
||||
field: 'editAction',
|
||||
iconInit: 'material:folder',
|
||||
iconReady: 'custom:folderSearch',
|
||||
}),
|
||||
new ActionIconValue({
|
||||
name: 'delete',
|
||||
tooltip: 'Delete Volume',
|
||||
color: 'warn',
|
||||
field: 'deleteAction',
|
||||
iconReady: 'material:delete',
|
||||
}),
|
||||
]),
|
||||
};
|
||||
|
||||
export const rokConfig: TableConfig = {
|
||||
title: tableConfig.title,
|
||||
newButtonText: tableConfig.newButtonText,
|
||||
columns: tableConfig.columns.concat([actionsCol]),
|
||||
};
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { IndexRokComponent } from './index-rok.component';
|
||||
|
||||
describe('IndexRokComponent', () => {
|
||||
let component: IndexRokComponent;
|
||||
let fixture: ComponentFixture<IndexRokComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [IndexRokComponent],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(IndexRokComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material';
|
||||
import {
|
||||
NamespaceService,
|
||||
ConfirmDialogService,
|
||||
DIALOG_RESP,
|
||||
SnackBarService,
|
||||
SnackType,
|
||||
RokService,
|
||||
ActionEvent,
|
||||
STATUS_TYPE,
|
||||
} from 'kubeflow';
|
||||
import { environment } from '@app/environment';
|
||||
import { VWABackendService } from 'src/app/services/backend.service';
|
||||
import { IndexDefaultComponent } from '../index-default/index-default.component';
|
||||
import { FormRokComponent } from '../../form/form-rok/form-rok.component';
|
||||
import { rokConfig } from './config';
|
||||
import { PVCProcessedObjectRok, PVCResponseObjectRok } from 'src/app/types';
|
||||
|
||||
@Component({
|
||||
selector: 'app-index-rok',
|
||||
templateUrl: '../index-default/index-default.component.html',
|
||||
styleUrls: ['../index-default/index-default.component.scss'],
|
||||
})
|
||||
export class IndexRokComponent extends IndexDefaultComponent implements OnInit {
|
||||
config = rokConfig;
|
||||
|
||||
constructor(
|
||||
public ns: NamespaceService,
|
||||
public confirmDialog: ConfirmDialogService,
|
||||
public backend: VWABackendService,
|
||||
public dialog: MatDialog,
|
||||
public snackBar: SnackBarService,
|
||||
public rok: RokService,
|
||||
) {
|
||||
super(ns, confirmDialog, backend, dialog, snackBar);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
super.ngOnInit();
|
||||
|
||||
this.rok.initCSRF();
|
||||
}
|
||||
|
||||
// Functions for handling the action events
|
||||
public newResourceClicked() {
|
||||
const ref = this.dialog.open(FormRokComponent, {
|
||||
width: '600px',
|
||||
panelClass: 'form--dialog-padding',
|
||||
});
|
||||
|
||||
ref.afterClosed().subscribe(res => {
|
||||
if (res === DIALOG_RESP.ACCEPT) {
|
||||
this.snackBar.open(
|
||||
'Volume was submitted successfully.',
|
||||
SnackType.Success,
|
||||
2000,
|
||||
);
|
||||
this.poller.reset();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public reactToAction(a: ActionEvent) {
|
||||
super.reactToAction(a);
|
||||
|
||||
switch (a.action) {
|
||||
case 'edit':
|
||||
this.editClicked(a.data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public editClicked(pvc: PVCProcessedObjectRok) {
|
||||
if (pvc.viewer === STATUS_TYPE.READY) {
|
||||
this.openEditWindow(pvc);
|
||||
return;
|
||||
}
|
||||
|
||||
this.pvcsWaitingViewer.add(pvc.name);
|
||||
pvc.editAction = this.parseViewerActionStatus(pvc);
|
||||
|
||||
this.backend.createViewer(this.currNamespace, pvc.name).subscribe({
|
||||
next: res => {
|
||||
this.poller.reset();
|
||||
},
|
||||
error: err => {
|
||||
this.pvcsWaitingViewer.delete(pvc.name);
|
||||
pvc.editAction = this.parseViewerActionStatus(pvc);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Utility funcs
|
||||
public parseIncomingData(
|
||||
pvcs: PVCResponseObjectRok[],
|
||||
): PVCProcessedObjectRok[] {
|
||||
const parsedPVCs = [];
|
||||
for (const pvc of super.parseIncomingData(pvcs)) {
|
||||
const parsedPVC = pvc as PVCProcessedObjectRok;
|
||||
|
||||
parsedPVC.editAction = this.parseViewerActionStatus(parsedPVC);
|
||||
parsedPVCs.push(parsedPVC);
|
||||
}
|
||||
|
||||
return parsedPVCs;
|
||||
}
|
||||
|
||||
public parseViewerActionStatus(pvc: PVCProcessedObjectRok): STATUS_TYPE {
|
||||
// If the PVC is being created or there was an error, then
|
||||
// don't allow the user to edit it
|
||||
if (
|
||||
pvc.status.phase === STATUS_TYPE.UNINITIALIZED ||
|
||||
pvc.status.phase === STATUS_TYPE.WAITING ||
|
||||
pvc.status.phase === STATUS_TYPE.WARNING ||
|
||||
pvc.status.phase === STATUS_TYPE.TERMINATING ||
|
||||
pvc.status.phase === STATUS_TYPE.ERROR
|
||||
) {
|
||||
return STATUS_TYPE.UNAVAILABLE;
|
||||
}
|
||||
|
||||
// The PVC is either READY or UNAVAILABLE(WaitForFirstConsumer)
|
||||
|
||||
// If the user had clicked to view the files and the viewer just
|
||||
// became ready, then open the edit window
|
||||
if (
|
||||
this.pvcsWaitingViewer.has(pvc.name) &&
|
||||
pvc.viewer === STATUS_TYPE.READY
|
||||
) {
|
||||
this.pvcsWaitingViewer.delete(pvc.name);
|
||||
this.openEditWindow(pvc);
|
||||
return STATUS_TYPE.READY;
|
||||
}
|
||||
|
||||
// If the user clicked to view the files and the viewer
|
||||
// is stil uninitialized or unavailable, then show a spinner
|
||||
if (
|
||||
this.pvcsWaitingViewer.has(pvc.name) &&
|
||||
(pvc.viewer === STATUS_TYPE.UNINITIALIZED ||
|
||||
pvc.viewer === STATUS_TYPE.WAITING)
|
||||
) {
|
||||
return STATUS_TYPE.WAITING;
|
||||
}
|
||||
|
||||
// If the user hasn't yet clicked to edit the pvc, then the viewer
|
||||
// button should be enabled
|
||||
if (
|
||||
!this.pvcsWaitingViewer.has(pvc.name) &&
|
||||
pvc.status.state === 'WaitForFirstConsumer'
|
||||
) {
|
||||
return STATUS_TYPE.UNINITIALIZED;
|
||||
}
|
||||
|
||||
return pvc.viewer;
|
||||
}
|
||||
|
||||
public openEditWindow(pvc: PVCProcessedObjectRok) {
|
||||
const url =
|
||||
this.env.viewerUrl + `/volume/browser/${pvc.namespace}/${pvc.name}/`;
|
||||
|
||||
window.open(url, `${pvc.name}: Edit file contents`, 'height=600,width=800');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
<app-index-default *ngIf="env.ui === 'default'"></app-index-default>
|
||||
<app-index-rok *ngIf="env.ui === 'rok'"></app-index-rok>
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { IndexComponent } from './index.component';
|
||||
|
||||
describe('IndexComponent', () => {
|
||||
let component: IndexComponent;
|
||||
let fixture: ComponentFixture<IndexComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [IndexComponent],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(IndexComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { environment } from '@app/environment';
|
||||
|
||||
@Component({
|
||||
selector: 'app-index',
|
||||
templateUrl: './index.component.html',
|
||||
styleUrls: ['./index.component.scss'],
|
||||
})
|
||||
export class IndexComponent {
|
||||
env = environment;
|
||||
|
||||
constructor() {}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { VWABackendService } from './backend.service';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
|
||||
describe('VWABackendService', () => {
|
||||
beforeEach(() => TestBed.configureTestingModule({
|
||||
imports: [HttpClientModule]
|
||||
}));
|
||||
|
||||
it('should be created', () => {
|
||||
const service: VWABackendService = TestBed.get(VWABackendService);
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { BackendService, SnackBarService } from 'kubeflow';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { catchError, map } from 'rxjs/operators';
|
||||
import { PVCResponseObject, VWABackendResponse, PVCPostObject } from '../types';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class VWABackendService extends BackendService {
|
||||
constructor(public http: HttpClient, public snackBar: SnackBarService) {
|
||||
super(http, snackBar);
|
||||
}
|
||||
|
||||
public getPVCs(namespace: string): Observable<PVCResponseObject[]> {
|
||||
const url = `api/namespaces/${namespace}/pvcs`;
|
||||
|
||||
return this.http.get<VWABackendResponse>(url).pipe(
|
||||
catchError(error => this.handleError(error)),
|
||||
map((resp: VWABackendResponse) => {
|
||||
return resp.pvcs;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// POST
|
||||
public createViewer(namespace: string, viewer: string) {
|
||||
const url = `api/namespaces/${namespace}/viewers`;
|
||||
|
||||
return this.http
|
||||
.post<VWABackendResponse>(url, { name: viewer })
|
||||
.pipe(catchError(error => this.handleError(error)));
|
||||
}
|
||||
|
||||
public createPVC(namespace: string, pvc: PVCPostObject) {
|
||||
const url = `api/namespaces/${namespace}/pvcs`;
|
||||
|
||||
return this.http
|
||||
.post<VWABackendResponse>(url, pvc)
|
||||
.pipe(catchError(error => this.handleError(error)));
|
||||
}
|
||||
|
||||
// DELETE
|
||||
public deletePVC(namespace: string, pvc: string) {
|
||||
const url = `api/namespaces/${namespace}/pvcs/${pvc}`;
|
||||
|
||||
return this.http
|
||||
.delete<VWABackendResponse>(url)
|
||||
.pipe(catchError(error => this.handleError(error, false)));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import { Status, BackendResponse } from 'kubeflow';
|
||||
|
||||
export interface VWABackendResponse extends BackendResponse {
|
||||
pvcs?: PVCResponseObject[];
|
||||
}
|
||||
|
||||
export interface PVCResponseObject {
|
||||
age: {
|
||||
uptime: string;
|
||||
timestamp: string;
|
||||
};
|
||||
capacity: string;
|
||||
class: string;
|
||||
modes: string[];
|
||||
name: string;
|
||||
namespace: string;
|
||||
status: Status;
|
||||
}
|
||||
|
||||
export interface PVCProcessedObject extends PVCResponseObject {
|
||||
deleteAction?: string;
|
||||
editAction?: string;
|
||||
ageValue?: string;
|
||||
ageTooltip?: string;
|
||||
}
|
||||
|
||||
export interface PVCPostObject {
|
||||
name: string;
|
||||
type: string;
|
||||
size: string | number;
|
||||
class: string;
|
||||
mode: string;
|
||||
snapshot: string;
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from './backend';
|
||||
export * from './rok';
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { PVCResponseObject, PVCProcessedObject } from './backend';
|
||||
import { STATUS_TYPE } from 'kubeflow';
|
||||
|
||||
export interface PVCResponseObjectRok extends PVCResponseObject {
|
||||
viewer: STATUS_TYPE;
|
||||
}
|
||||
|
||||
export interface PVCProcessedObjectRok
|
||||
extends PVCResponseObjectRok,
|
||||
PVCProcessedObject {}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export const environment = {
|
||||
production: true,
|
||||
viewerUrl: '',
|
||||
resource: 'pvcviewers',
|
||||
ui: 'default',
|
||||
};
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export const environment = {
|
||||
production: true,
|
||||
viewerUrl: '',
|
||||
resource: 'pvcviewers',
|
||||
ui: 'rok',
|
||||
};
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
// This file can be replaced during build by using the `fileReplacements` array.
|
||||
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
|
||||
// The list of file replacements can be found in `angular.json`.
|
||||
|
||||
export const environment = {
|
||||
production: false,
|
||||
viewerUrl: '//localhost:8081',
|
||||
resource: 'pvcviewers',
|
||||
ui: 'rok',
|
||||
};
|
||||
|
||||
/*
|
||||
* For easier debugging in development mode, you can import the following file
|
||||
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
|
||||
*
|
||||
* This import should be commented out in production mode because it will have a negative impact
|
||||
* on performance if an error is thrown.
|
||||
*/
|
||||
// import 'zone.js/dist/zone-error'; // Included with Angular CLI.
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
// This file can be replaced during build by using the `fileReplacements` array.
|
||||
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
|
||||
// The list of file replacements can be found in `angular.json`.
|
||||
|
||||
export const environment = {
|
||||
production: false,
|
||||
viewerUrl: '//localhost:8081',
|
||||
resource: 'pvcviewers',
|
||||
ui: 'default',
|
||||
};
|
||||
|
||||
/*
|
||||
* For easier debugging in development mode, you can import the following file
|
||||
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
|
||||
*
|
||||
* This import should be commented out in production mode because it will have a negative impact
|
||||
* on performance if an error is thrown.
|
||||
*/
|
||||
// import 'zone.js/dist/zone-error'; // Included with Angular CLI.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 948 B |
|
|
@ -0,0 +1,18 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Frontend</title>
|
||||
<base href="/" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script defer src="/dashboard_lib.bundle.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
body {
|
||||
--accent-color: #007dfc;
|
||||
--accent-color-light: #69abff;
|
||||
--accent-color-dark: #0052c8;
|
||||
--kubeflow-color: #003c75;
|
||||
--primary-background-color: #2196f3;
|
||||
--sidebar-color: #f8fafb;
|
||||
--border-color: #f4f4f6;
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import { enableProdMode } from '@angular/core';
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
|
||||
import { AppModule } from './app/app.module';
|
||||
import { environment } from './environments/environment';
|
||||
|
||||
if (environment.production) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||
.catch(err => console.error(err));
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
/**
|
||||
* This file includes polyfills needed by Angular and is loaded before the app.
|
||||
* You can add your own extra polyfills to this file.
|
||||
*
|
||||
* This file is divided into 2 sections:
|
||||
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
|
||||
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
|
||||
* file.
|
||||
*
|
||||
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
|
||||
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
|
||||
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
|
||||
*
|
||||
* Learn more in https://angular.io/guide/browser-support
|
||||
*/
|
||||
|
||||
/***************************************************************************************************
|
||||
* BROWSER POLYFILLS
|
||||
*/
|
||||
|
||||
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
|
||||
// import 'classlist.js'; // Run `npm install --save classlist.js`.
|
||||
|
||||
/**
|
||||
* Web Animations `@angular/platform-browser/animations`
|
||||
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
|
||||
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
|
||||
*/
|
||||
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
|
||||
|
||||
/**
|
||||
* By default, zone.js will patch all possible macroTask and DomEvents
|
||||
* user can disable parts of macroTask/DomEvents patch by setting following flags
|
||||
* because those flags need to be set before `zone.js` being loaded, and webpack
|
||||
* will put import in the top of bundle, so user need to create a separate file
|
||||
* in this directory (for example: zone-flags.ts), and put the following flags
|
||||
* into that file, and then add the following code before importing zone.js.
|
||||
* import './zone-flags.ts';
|
||||
*
|
||||
* The flags allowed in zone-flags.ts are listed here.
|
||||
*
|
||||
* The following flags will work for all browsers.
|
||||
*
|
||||
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
|
||||
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
|
||||
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
|
||||
*
|
||||
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
|
||||
* with the following flag, it will bypass `zone.js` patch for IE/Edge
|
||||
*
|
||||
* (window as any).__Zone_enable_cross_context_check = true;
|
||||
*
|
||||
*/
|
||||
|
||||
/***************************************************************************************************
|
||||
* Zone JS is required by default for Angular itself.
|
||||
*/
|
||||
import 'zone.js/dist/zone'; // Included with Angular CLI.
|
||||
|
||||
|
||||
/***************************************************************************************************
|
||||
* APPLICATION IMPORTS
|
||||
*/
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
/* You can add global styles to this file, and also import other style files */
|
||||
@import '~kubeflow/styles.scss';
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
|
||||
|
||||
import 'zone.js/dist/zone-testing';
|
||||
import { getTestBed } from '@angular/core/testing';
|
||||
import {
|
||||
BrowserDynamicTestingModule,
|
||||
platformBrowserDynamicTesting
|
||||
} from '@angular/platform-browser-dynamic/testing';
|
||||
|
||||
declare const require: any;
|
||||
|
||||
// First, initialize the Angular testing environment.
|
||||
getTestBed().initTestEnvironment(
|
||||
BrowserDynamicTestingModule,
|
||||
platformBrowserDynamicTesting()
|
||||
);
|
||||
// Then we find all the tests.
|
||||
const context = require.context('./', true, /\.spec\.ts$/);
|
||||
// And load the modules.
|
||||
context.keys().map(context);
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/app",
|
||||
"types": []
|
||||
},
|
||||
"files": [
|
||||
"src/main.ts",
|
||||
"src/polyfills.ts"
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"src/test.ts",
|
||||
"src/**/*.spec.ts"
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"outDir": "./dist/out-tsc",
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"downlevelIteration": true,
|
||||
"experimentalDecorators": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"target": "es2015",
|
||||
"paths": {
|
||||
"@shared/*": ["../../angular-frontend-shared/*"],
|
||||
"@app/environment": ["src/environments/environment"]
|
||||
},
|
||||
"typeRoots": ["node_modules/@types"],
|
||||
"lib": ["es2018", "dom"]
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"fullTemplateTypeCheck": true,
|
||||
"strictInjectionParameters": true
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/spec",
|
||||
"types": [
|
||||
"jasmine",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"files": [
|
||||
"src/test.ts",
|
||||
"src/polyfills.ts"
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
{
|
||||
"extends": "tslint:recommended",
|
||||
"rules": {
|
||||
"array-type": false,
|
||||
"arrow-parens": false,
|
||||
"deprecation": {
|
||||
"severity": "warning"
|
||||
},
|
||||
"component-class-suffix": true,
|
||||
"contextual-lifecycle": true,
|
||||
"directive-class-suffix": true,
|
||||
"directive-selector": [true, "attribute", "app", "camelCase"],
|
||||
"component-selector": [true, "element", "app", "kebab-case"],
|
||||
"import-blacklist": [true, "rxjs/Rx"],
|
||||
"interface-name": false,
|
||||
"max-classes-per-file": false,
|
||||
"max-line-length": [true, 140],
|
||||
"member-access": false,
|
||||
"member-ordering": [
|
||||
true,
|
||||
{
|
||||
"order": [
|
||||
"static-field",
|
||||
"instance-field",
|
||||
"static-method",
|
||||
"instance-method"
|
||||
]
|
||||
}
|
||||
],
|
||||
"no-consecutive-blank-lines": false,
|
||||
"no-console": [true, "debug", "info", "time", "timeEnd", "trace"],
|
||||
"no-empty": false,
|
||||
"no-inferrable-types": [true, "ignore-params"],
|
||||
"no-non-null-assertion": true,
|
||||
"no-redundant-jsdoc": true,
|
||||
"no-switch-case-fall-through": true,
|
||||
"no-var-requires": false,
|
||||
"object-literal-key-quotes": [true, "as-needed"],
|
||||
"object-literal-sort-keys": false,
|
||||
"ordered-imports": false,
|
||||
"quotemark": [true, "single"],
|
||||
"trailing-comma": false,
|
||||
"no-conflicting-lifecycle": true,
|
||||
"no-host-metadata-property": true,
|
||||
"no-input-rename": true,
|
||||
"no-inputs-metadata-property": true,
|
||||
"no-output-native": true,
|
||||
"no-output-on-prefix": true,
|
||||
"no-output-rename": true,
|
||||
"no-outputs-metadata-property": true,
|
||||
"template-banana-in-box": true,
|
||||
"template-no-negated-async": true,
|
||||
"use-lifecycle-interface": true,
|
||||
"use-pipe-transform-interface": true
|
||||
},
|
||||
"rulesDirectory": ["codelyzer"]
|
||||
}
|
||||
Loading…
Reference in New Issue