Skip to content
2 changes: 1 addition & 1 deletion dashboard/pkg/epinio/components/dashboard/Cards.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const props = defineProps<{
link: object,
linkText: string,
description: string,
slotTitle: string | null,
slotTitle: string | null,
}>()

const setLoading = computed(() => {
Expand Down
253 changes: 253 additions & 0 deletions dashboard/pkg/epinio/list/namespaces.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
<script setup lang="ts">
import { mapGetters, mapState, useStore } from 'vuex';
import { Location, useRouter, useRoute } from 'vue-router';
import { ref, onMounted, computed, watch, nextTick, useAttrs } from 'vue';

import { EPINIO_TYPES } from '../types';
import { Card } from '@components/Card';
import Banner from '@components/Banner/Banner.vue';
import { _CREATE } from '@shell/config/query-params';
import AsyncButton from '@shell/components/AsyncButton';
import ResourceTable from '@shell/components/ResourceTable';
import Masthead from '@shell/components/ResourceList/Masthead';
import { epinioExceptionToErrorsArray } from '../utils/errors';
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
import { validateKubernetesName } from '@shell/utils/validators/kubernetes-name';

const props = defineProps<{
schema: object,
rows: array,
}>();

const attrs = useAttrs();
const store = useStore();
const router = useRouter();
const route = useRoute();
const t = store.getters['i18n/t'];

const errors = ref<array>([]);
const namespaceName = ref('namespaceName');
const showCreateModal = ref<boolean>(false);
const creatingNamespace = ref<boolean>(false);
const touched = ref<boolean>(false);

const mode: string = _CREATE;
const submitted: boolean = false;
const validFields: array = { name: false };
const resource: string = EPINIO_TYPES.NAMESPACE;
const value = ref<array>({ meta: { name: '' } });

const showPromptRemove = computed(() => {
return store.state['action-menu'].showPromptRemove
});

const validationPassed = computed(() => {
// Add here fields that need validation
if (!creatingNamespace.value) {
errors.value = [];
errors.value = getNamespaceErrors(value.value.meta.name);
}

return errors.value?.length === 0;
});

onMounted(() => {
// Opens the create namespace modal if the query is passed as query param
if (route.query.mode === 'openModal') {
openCreateModal();
}
});

watch(
() => showPromptRemove,
(newState, oldState) => {
if (oldState === true && newState === false) {
// Refetch apps when namespace is deleted
store.dispatch('findAll', { type: 'applications', opt: { force: true } });
}
}
);

// Watch for changes in value.meta.name, not needed as there are no rules currently
watch(
() => value.value.meta.name,
() => {
creatingNamespace.value = false;
validateNamespace();
}
);

async function openCreateModal() {
showCreateModal.value = true;
// Focus on the name input field... after it's been displayed
nextTick(() => namespaceName.value.focus());
// Create a skeleton namespace
value.value = await store.dispatch(
`epinio/create`,
{ type: EPINIO_TYPES.NAMESPACE },
);
}

function closeCreateModal() {
showCreateModal.value = false;
errors.value = [];
touched.value = false;
}

async function onSubmit(buttonCb) {
creatingNamespace.value = true;
try {
await value.value.create();
closeCreateModal();
buttonCb(true);
touched.value = false;
} catch (e) {
errors.value = [];
errors.value = epinioExceptionToErrorsArray(e).map(JSON.stringify);
buttonCb(false);
}
}

function validateNamespace(name) {
if (!name?.length && !touched.value) {
touched.value = true;
}

errors.value = getNamespaceErrors(name);
}

function getNamespaceErrors(name) {
const kubernetesErrors = validateKubernetesName(
name || '',
t('epinio.namespace.name'),
store.getters,
undefined,
[],
);

if (kubernetesErrors.length) {
return [kubernetesErrors.join(', ')];
}

const validateName = name.match(/[a-z0-9]([-a-z0-9]*[a-z0-9])?/);

if (
!validateName ||
validateName[0] !== name &&
!errors.value.includes(t('epinio.namespace.validations.name'))
) {
return [t('epinio.namespace.validations.name')];
}

return [];
}
</script>

<template>
<div>
<Masthead
:schema="schema"
:resource="resource"
>
<template v-slot:createButton>
<button
class="btn role-primary"
@click="openCreateModal"
>
{{ t('generic.create') }}
</button>
</template>
</Masthead>
<ResourceTable
v-bind="attrs"
:rows="rows"
:groupable="false"
:schema="schema"
key-field="_key"
:useQueryParamsForSimpleFiltering="true"
/>
<div
v-if="showCreateModal"
class="modal"
>
<Card
class="modal-content"
:show-actions="true"
>
<template #title>
<h4
v-clean-html="t('epinio.namespace.create')"
/>
</template>
<template class="model-body" #body>
<LabeledInput
ref="namespaceName"
v-model:value="value.meta.name"
:label="t('epinio.namespace.name')"
:required="true"
/>
<Banner
v-if="touched"
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template>
<template class="model-actions" #actions>
<button
class="btn role-secondary mr-10"
@click="closeCreateModal"
>
{{ t('generic.cancel') }}
</button>
<AsyncButton
:disabled="!validationPassed"
:mode="mode"
@click="onSubmit"
/>
</template>
</Card>
</div>
</div>
</template>

<style lang='scss' scoped>
.modal {
position: fixed; /* Stay in place */
z-index: 50; /* Sit on top */
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgb(0,0,0);
background-color: rgba(0,0,0,0.4);
border-radius: var(--border-radius);

.banner {
margin-bottom: 0;
}
}

.modal-content {
background-color: var(--default);
margin: 15% auto;
padding: 20px;
border: 1px solid #888;
width: 50%;
max-width: 500px;

.model-body {
min-height: 116px;
}

.model-actions {
justify-content: flex-end;
display: flex;
flex: 1;
}

}
</style>

47 changes: 47 additions & 0 deletions dashboard/pkg/epinio/pages/auth/verify.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { mapGetters, mapState, useStore } from 'vuex';
import { Location, useRouter, useRoute } from 'vue-router';
import epinioAuth from '../../utils/auth';
import Banner from '@components/Banner/Banner.vue';
import { dashboardUrl } from '../../utils/embedded-helpers';

const store = useStore();
const router = useRouter();
const route = useRoute();

interface Data {
error: string,
}

const error = ref<string>('');

onMounted(async () => {
const { error: routeError, error_description: errorDescription } = route.query;

error.value = errorDescription || routeError;
console.log(document.referrer);
if (error.value) {
console.error('Dex indicates failure', error); // eslint-disable-line no-console
} else {
await epinioAuth.dexRedirect(route, {
dexUrl: document.referrer,
dashboardUrl: dashboardUrl()
});
}
});
</script>

<template>
{{error}}
<main class="main-layout">
<Banner
v-if="error"
color="error"
:label="error"
/>
<h1 class="text-center mt-50">
Logging In&hellip;
</h1>
</main>
</template>
8 changes: 8 additions & 0 deletions dashboard/pkg/epinio/pages/c/_cluster/_resource/_id.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<script setup lang="ts">
import ResourceDetail from '@shell/components/ResourceDetail/index.vue';
</script>

<template>
<ResourceDetail />
</template>

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<script lang="ts">
import ResourceDetail from '@shell/components/ResourceDetail/index.vue';
</script>

<template>
<ResourceDetail />
</template>

8 changes: 8 additions & 0 deletions dashboard/pkg/epinio/pages/c/_cluster/_resource/create.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<script lang="ts">
import ResourceDetail from '@shell/components/ResourceDetail/index.vue';
</script>

<template>
<ResourceDetail />
</template>

8 changes: 8 additions & 0 deletions dashboard/pkg/epinio/pages/c/_cluster/_resource/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<script setup lang="ts">
import ResourceList from '@shell/components/ResourceList/index.vue';
</script>

<template>
<ResourceList />
</template>

11 changes: 8 additions & 3 deletions dashboard/pkg/epinio/routing/epinio-routing.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
// Don't forget to create a VueJS page called index.vue in the /pages folder!!!
import ListEpinio from "../pages/index.vue";
import Dashboard from "../pages/c/_cluster/dashboard.vue";
import AuthVerify from "../pages/auth/verify.vue";
import ListApp from "../pages/c/_cluster/applications/index.vue";
import CreateApp from "../pages/c/_cluster/applications/createapp/index.vue";
import AboutEpinio from "../pages/c/_cluster/about.vue";
// import { BLANK_CLUSTER } from '../config/epinio';
import { EPINIO_PRODUCT_NAME } from '../types';
import ListEpinioResource from '../pages/c/_cluster/_resource/index.vue';


// // import ViewResource from '@shell/pages/c/_cluster/_product/_resource/_id.vue';
Expand Down Expand Up @@ -38,7 +40,7 @@ const routes = [
{
name: `${EPINIO_PRODUCT_NAME}-auth-verify`,
path: `/:product/auth/verify`,
component: ListEpinio, //AuthVerify,
component: AuthVerify,
meta,
},
{
Expand All @@ -65,8 +67,11 @@ const routes = [
{
name: `${EPINIO_PRODUCT_NAME}-c-cluster-resource`,
path: `/:product/c/:cluster/:resource`,
component: ListEpinio, //ListEpinioResource,
meta,
component: ListEpinioResource,
meta: {
product: EPINIO_PRODUCT_NAME,
pkg: EPINIO_PRODUCT_NAME,
},
},
{
name: `${EPINIO_PRODUCT_NAME}-c-cluster-resource-create`,
Expand Down
Loading
Loading