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