Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion structures-frontend-next/src/pages/ApplicationSettings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,6 @@ const enableMCP = ref(false)
const loading = ref(false)
const activeTab = ref(0)


watch(() => APPLICATION_STATE.currentApplication, (newApp) => {
if (newApp) {
appName.value = newApp.id || ''
Expand Down
127 changes: 65 additions & 62 deletions structures-frontend-next/src/pages/EntityList.vue
Original file line number Diff line number Diff line change
@@ -1,54 +1,3 @@
<template>
<div class="overflow-y-auto">
<Toolbar>
<template #start>
<InputText
v-model="searchText"
placeholder="Search"
@keyup.enter="search"
@focus="($event.target as HTMLInputElement)?.select()"
class="w-md"
/>
<Button icon="pi pi-times" class="ml-2" v-if="searchText" @click="clearSearch" />
</template>
</Toolbar>

<DataTable :value="items" :loading="loading" :paginator="true" :rows="options.rows" :totalRecords="totalItems"
:first="options.first" :lazy="true" :sortField="options.sortField" :sortOrder="options.sortOrder" @page="onPage"
@sort="onSort" :scrollable="true" scrollHeight="flex" :resizableColumns="true" columnResizeMode="expand">
<template v-if="headers.length > 0">
<Column v-for="header in headers" :key="header.field" :field="header.field" :header="header.header"
:sortable="header.sortable" :style="{ width: header.width + 'px' }"
:class="[header.isCollapsable ? '!whitespace-normal' : '']">
<template #body="slotProps">
<div :class="[
header.isCollapsable
? 'whitespace-normal break-words w-[240px] max-w-[240px] text-sm'
: 'truncate'
]">
<span v-if="typeof slotProps.data[header.field] === 'object'">
{{ JSON.stringify(slotProps.data[header.field]) }}
</span>
<span v-else>
{{ isDateField(header.field)
? formatDate(slotProps.data[header.field])
: slotProps.data[header.field]
}}
</span>
</div>
</template>

</Column>
</template>

<template v-if="items.length === 0">
<div class="p-4 text-center">
<Button label="No Data - Push To Search Again" @click="find" v-if="!loading" />
</div>
</template>
</DataTable>
</div>
</template>

<script lang="ts">
import { Component, Vue, Prop } from 'vue-facing-decorator'
Expand Down Expand Up @@ -86,7 +35,7 @@ export default class EntityList extends Vue {
headers: any[] = []
structureProperties: any = {}
structure!: Structure

entitiesService: IEntitiesService = Structures.getEntitiesService()
structureService: IStructureService = Structures.getStructureService()

Expand All @@ -101,7 +50,7 @@ export default class EntityList extends Vue {
const paramId = this.$route.params.id
const id = this.structureId || (Array.isArray(paramId) ? paramId[0] : paramId)

if (!id) {
if (!id) {
this.displayAlert("Missing structure ID.")
return
}
Expand Down Expand Up @@ -166,7 +115,7 @@ const id = this.structureId || (Array.isArray(paramId) ? paramId[0] : paramId)
this.options.first = 0
this.find()
}

search() {
this.options.first = 0
this.find()
Expand All @@ -183,25 +132,25 @@ const id = this.structureId || (Array.isArray(paramId) ? paramId[0] : paramId)

const page = this.options.first / this.options.rows
const orders: Order[] = []

if (this.options.sortField) {
orders.push(new Order(this.options.sortField, this.options.sortOrder === 1 ? Direction.ASC : Direction.DESC))
}

const pageable = Pageable.create(page, this.options.rows, { orders })
const paramId = this.$route.params.id
const id = this.structureId || (Array.isArray(paramId) ? paramId[0] : paramId)

const id = this.structureId || (Array.isArray(paramId) ? paramId[0] : paramId)
const queryPromise = (this.searchText?.length)
? this.entitiesService.search(id, this.searchText, pageable)
: this.entitiesService.findAll(id, pageable)
? this.entitiesService.search(id, this.searchText, pageable)
: this.entitiesService.findAll(id, pageable)

queryPromise
.then((page: Page<any>) => {
.then((page: Page<any>) => {
this.items = page.content ?? []
this.totalItems = page.totalElements ?? 0
this.loading = false

if (!this.finishedInitialLoad) {
setTimeout(() => { this.finishedInitialLoad = true }, 500)
}
Expand All @@ -217,8 +166,62 @@ const id = this.structureId || (Array.isArray(paramId) ? paramId[0] : paramId)
}
</script>

<style scoped>
<template>
<div class="overflow-y-auto w-full">
<Toolbar class="!w-full">
<template #start>
<InputText
v-model="searchText"
placeholder="Search"
@keyup.enter="search"
@focus="($event.target as HTMLInputElement)?.select()"
class="w-1/2"
/>
<Button icon="pi pi-times" class="ml-2" v-if="searchText" @click="clearSearch" />
</template>
</Toolbar>

<DataTable :value="items" :loading="loading" :paginator="true" :rows="options.rows" :totalRecords="totalItems"
:first="options.first" :lazy="true" :sortField="options.sortField" :sortOrder="options.sortOrder" @page="onPage"
@sort="onSort" :scrollable="true" scrollHeight="flex" :resizableColumns="true" columnResizeMode="expand">
<template v-if="headers.length > 0">
<Column v-for="header in headers" :key="header.field" :field="header.field" :header="header.header"
:sortable="header.sortable" :style="{ width: header.width + 'px' }"
:class="[header.isCollapsable ? '!whitespace-normal' : '']">
<template #body="slotProps">
<div :class="[
header.isCollapsable
? 'whitespace-normal break-words w-[240px] max-w-[240px] text-sm'
: 'truncate'
]">
<span v-if="typeof slotProps.data[header.field] === 'object'">
{{ JSON.stringify(slotProps.data[header.field]) }}
</span>
<span v-else>
{{ isDateField(header.field)
? formatDate(slotProps.data[header.field])
: slotProps.data[header.field]
}}
</span>
</div>
</template>

</Column>
</template>

<template v-if="items.length === 0">
<div class="p-4 text-center">
<Button label="No Data - Push To Search Again" @click="find" v-if="!loading" />
</div>
</template>
</DataTable>
</div>
</template>
<style>
.p-datatable .p-button {
margin-top: 1rem;
}
.p-toolbar-start {
width: 100% !important;
}
</style>
99 changes: 78 additions & 21 deletions structures-frontend-next/src/pages/login/Login.vue
Original file line number Diff line number Diff line change
@@ -1,22 +1,30 @@
<template>
<div class="flex w-full justify-center items-center h-screen max-w-[1440px] mx-auto">
<div class="flex w-full justify-center items-center h-screen mx-auto">
<div v-if="isInitialized && state?.oidcCallbackLoading" class="fixed inset-0 bg-white bg-opacity-90 flex items-center justify-center z-50">
<div class="text-center">
<div class="animate-spin rounded-full h-16 w-16 border-b-2 border-blue-600 mx-auto mb-4"></div>
<h2 class="text-xl font-semibold text-gray-800 mb-2">Validating Login...</h2>
<p class="text-gray-600">Please wait while we complete your authentication.</p>
</div>
</div>

<!-- <div class="hidden md:block w-1/2 h-full bg-[url(@/assets/login-page-image.png)] bg-no-repeat bg-cover bg-bottom-left">
</div> -->
<div class="relative w-1/2 h-full bg-gradient-to-br from-[#0A0A0B] from-0% via-[#0A0A0B] via-70% to-[#293A9E] to-100% hidden md:block">
<img src="@/assets/login-page-symbol-new.svg" class="absolute right-0 bottom-0"/>
<img src="@/assets/login-page-symbol-new.svg" class="absolute right-0 bottom-0 h-screen"/>
<img src="@/assets/login-page-logo-new.svg" class="absolute left-[75px] bottom-[56px] max-w-[300px] h-[63px] w-auto xl:max-w-[300px] xl:h-[63px] lg:max-w-[250px] lg:h-[52px] md:max-w-[200px] md:h-[42px] sm:max-w-[150px] sm:h-[32px]"/>
</div>
<div class="w-1/2 h-full flex flex-col justify-around items-center bg-center bg-cover">
<div class="w-[320px] flex flex-col items-center">
<div class="w-1/2 h-full flex flex-col justify-center items-center bg-center bg-cover relative">
<div v-if="showSuccessMessage" class="w-full h-full flex flex-col justify-center items-center text-center">
<div class="mb-6">
<div class="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
<svg class="w-8 h-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
</div>
<h2 class="text-4xl font-semibold text-surface-900 mb-4">Authentication successful</h2>
<p class="text-surface-900 text-base font-normal">You can close this tab and return to your command line</p>
</div>
</div>

<div v-if="!showSuccessMessage" class="w-[320px] flex flex-col items-center">
<img src="@/assets/login-page-logo.svg" class="w-[218px] h-[45px] mb-[53px]" />

<div v-if="isInitialized && shouldShowLoginForm">
Expand Down Expand Up @@ -181,7 +189,7 @@
<p class="text-gray-600">Initializing...</p>
</div>
</div>
<div class="flex gap-2">
<div class="flex gap-2 absolute bottom-8 left-0 right-0 justify-center">
<a href="#" class="text-[#0568FD] border-b-1">
Terms of use
</a>
Expand Down Expand Up @@ -246,6 +254,10 @@ export default class Login extends Vue {

private _isConfigLoaded: boolean = false;
private _isBasicAuthEnabled: boolean = true;

private showSuccessMessage: boolean = false;
private countdown: number = 2;
private countdownInterval: NodeJS.Timeout | null = null;

get isConfigLoaded() { return this._isConfigLoaded; }
get isBasicAuthEnabled() { return this._isBasicAuthEnabled; }
Expand Down Expand Up @@ -291,7 +303,6 @@ export default class Login extends Vue {
});
} catch (error) {
console.error('Failed to load basic config:', error);
// Keep defaults
this._isBasicAuthEnabled = true;
this._isConfigLoaded = false;
console.log('Using default config:', {
Expand Down Expand Up @@ -364,8 +375,12 @@ export default class Login extends Vue {

await this.userState.handleOidcLogin(user);

const redirectPath = referer || '/applications';
await CONTINUUM_UI.navigate(redirectPath);
if (referer) {
this.$route.query.referer = referer;
}

this.showSuccessMessage = true;
this.startCountdown();
} catch (error: unknown) {
console.error('OIDC callback error:', error);
if (error instanceof Error) {
Expand All @@ -391,16 +406,8 @@ export default class Login extends Vue {
try {
await this.userState.authenticate(this.login, this.password);

if (this.referer) {
await CONTINUUM_UI.navigate(this.referer);
} else {
const redirectPath = this.$route.redirectedFrom?.fullPath;
if (redirectPath && redirectPath !== "/") {
await CONTINUUM_UI.navigate(redirectPath);
} else {
await CONTINUUM_UI.navigate('/applications');
}
}
this.showSuccessMessage = true;
this.startCountdown();
} catch (error: unknown) {
console.error('Authentication error:', error);
if (error instanceof Error) {
Expand Down Expand Up @@ -569,5 +576,55 @@ export default class Login extends Vue {
this.auth.resetToEmail();
}
}

private startCountdown() {
this.countdown = 2;

this.countdownInterval = setInterval(() => {
this.countdown--;

if (this.countdown <= 0) {
this.clearCountdown();
this.redirectAfterSuccess();
}
}, 1000);
}

private clearCountdown() {
if (this.countdownInterval) {
clearInterval(this.countdownInterval);
this.countdownInterval = null;
}
}

private async redirectAfterSuccess() {
try {
const refererFromQuery = this.$route.query.referer as string;
if (refererFromQuery) {
await CONTINUUM_UI.navigate(refererFromQuery);
return;
}

if (this.referer) {
await CONTINUUM_UI.navigate(this.referer);
return;
}

const redirectPath = this.$route.redirectedFrom?.fullPath;
if (redirectPath && redirectPath !== "/") {
await CONTINUUM_UI.navigate(redirectPath);
return;
}

await CONTINUUM_UI.navigate('/applications');
} catch (error) {
console.error('Redirect error:', error);
await CONTINUUM_UI.navigate('/applications');
}
}

beforeUnmount() {
this.clearCountdown();
}
}
</script>