Skip to content
Open
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
194 changes: 194 additions & 0 deletions dashboard/src/api/ibexIdsAPI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import axios from 'axios'
import type { AxiosInstance } from 'axios'

export interface IDSNode {
path: string
name: string
dtype: string
shape: number[]
description?: string
units?: string
parent_path?: string
}

export interface TimeSeriesData {
name: string
unit: string
shape: number[]
downsampled_shape: number[]
ndim: number
path: string
description: string
coordinates: Array<{
name: string
target: string
unit: string
shape: number[]
downsampled_shape: number[]
ndim: number
path: string
description: string
coordinates: any[]
shapes_dimension: boolean
value: number[]
}>
value: number[]
}

export class IBEXIdsAPI {
client: AxiosInstance
private baseURL: string

constructor(baseURL: string = '/api/ibex') {
this.baseURL = baseURL
this.client = axios.create({
baseURL: baseURL,
timeout: 30000,
headers: {
'Content-Type': 'application/json'
}
})
}

async listNodes(baseUri: string): Promise<IDSNode[]> {
try {
console.log('Listing nodes with URI:', baseUri)
const params = new URLSearchParams()
params.append('uri', baseUri)
const response = await this.client.get('/data_entry/list_idses', { params })
console.log('List IDSes response:', response.data)
const nodes: IDSNode[] = []
if (response.data.idses && Array.isArray(response.data.idses)) {
response.data.idses.forEach((ids: any) => {
if (ids.occurrences && Array.isArray(ids.occurrences)) {
ids.occurrences.forEach((occ: any) => {
nodes.push({
path: `${ids.name}:${occ}`,
name: `${ids.name}:${occ}`,
dtype: 'IDS',
shape: [],
description: `${ids.name} (occurrence ${occ})`
})
})
}
})
}
console.log(`Found ${nodes.length} IDS nodes:`, nodes)
return nodes
} catch (error: any) {
console.error('Failed to list nodes:', error)
throw error
}
}

async getPlotData(uri: string, showErrorBars: boolean = false): Promise<TimeSeriesData> {
try {
const params = new URLSearchParams()
params.append('uri', uri)
params.append('show_error_bars', showErrorBars ? 'true' : 'false')
const response = await this.client.get('/data/plot_data', { params })
if (response.data.data) {
return response.data.data as TimeSeriesData
}
return response.data as TimeSeriesData
} catch (error) {
console.error('Failed to get plot data:', error)
throw error
}
}

async getNodeChildren(baseUri: string, idsName: string): Promise<IDSNode[]> {
try {
console.log(`Getting child nodes for ${idsName} in URI:`, baseUri)
const name = idsName.includes(':') ? idsName.split(':')[0] : idsName
const nodeUri = `${baseUri}#${name}`
console.log('Node info URI:', nodeUri)
const params = new URLSearchParams()
params.append('uri', nodeUri)
params.append('show_error_bars', 'true')
const response = await this.client.get('/ids_info/node_info', { params })
console.log('Node info response:', response.data)
const children: IDSNode[] = []
const extractNodes = (data: any, prefix = '', isRoot = true) => {
if (Array.isArray(data)) {
data.forEach((item: any) => {
if (item.name) {
const fullPath = isRoot ? item.name : (prefix ? `${prefix}/${item.name}` : item.name)
children.push({
path: fullPath,
name: item.name,
dtype: item.type || item.dtype || 'unknown',
shape: item.shape || [item.ndim || 0],
description: item.description,
units: item.units
})
}
if (item.children) {
const newPrefix = isRoot ? item.name : (prefix ? `${prefix}/${item.name}` : item.name)
extractNodes(item.children, newPrefix, false)
}
})
} else if (data && typeof data === 'object') {
if (data.name) {
const fullPath = isRoot ? '' : (prefix || data.name)
if (fullPath) {
children.push({
path: fullPath,
name: data.name,
dtype: data.type || data.dtype || 'unknown',
shape: data.shape || [data.ndim || 0],
description: data.description,
units: data.units
})
}
}
if (data.children) {
extractNodes(data.children, '', false)
}
}
}
extractNodes(response.data)
console.log(`Found ${children.length} child nodes:`, children)
return children
} catch (error: any) {
console.error('Failed to get node children:', error)
return []
}
}

async findFields(baseUri: string, idsName: string): Promise<string[]> {
try {
console.log(`Finding fields for ${idsName} in URI:`, baseUri)
const name = idsName.includes(':') ? idsName.split(':')[0] : idsName
const nodeUri = `${baseUri}#${name}`
const params = new URLSearchParams()
params.append('uri', nodeUri)
const response = await this.client.get('/ids_info/node_info', { params })
const fields: string[] = []
const extractFieldNames = (data: any) => {
if (Array.isArray(data)) {
data.forEach((item: any) => {
if (item.name) {
fields.push(item.name)
}
if (item.children) {
extractFieldNames(item.children)
}
})
} else if (data && typeof data === 'object') {
if (data.children) {
extractFieldNames(data.children)
}
}
}
extractFieldNames(response.data)
console.log(`Found ${fields.length} fields:`, fields)
return fields
} catch (error: any) {
console.error('Failed to find fields:', error)
return []
}
}
}

export const ibexIdsAPI = new IBEXIdsAPI('/api/ibex')
99 changes: 97 additions & 2 deletions dashboard/src/components/DetailView.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { config } from '../config'
import DataRow from './DataRow.vue'
import AuthDialog from './AuthDialog.vue'
import RowAdder from './RowAdder.vue'
import { formatUri } from '../utils/uriHelper'

const router = useRouter()

const _showAllFields =
typeof config.displayFields === 'string' && new String(config.displayFields).toLowerCase() === 'all'
Expand Down Expand Up @@ -282,6 +286,43 @@ function sortOutputs(key: string) {
outputSort.value.asc = true
}
}

function navigateToIDS(uri: string) {
// Convert old format: imas://uda.iter.org/uda?path=/path&backend=hdf5
// to new format: imas:backend?path=/path
let normalizedUri = uri

// Check if it's old format with backend parameter
const oldFormatMatch = uri.match(/^imas:\/\/([^/]+)\/([^?]+)\?path=([^&]+)&backend=([^&]+)/)
if (oldFormatMatch) {
const [, , , path, backend] = oldFormatMatch
normalizedUri = `imas:${backend}?path=${path}`
}
// If URI doesn't have backend type and matches new format, default to hdf5
else if (uri.match(/^imas:\w+\?path=/) === null && uri.match(/^imas:\?path=/)) {
// Missing backend, default to hdf5
normalizedUri = uri.replace(/^imas:/, 'imas:hdf5')
}
// If it's already in new format but missing backend, add hdf5
else if (uri.match(/^imas:\?path=/)) {
normalizedUri = uri.replace(/^imas:/, 'imas:hdf5')
}

// Open in new tab with normalized URI
window.open(
router.resolve({
path: '/ids-explorer',
query: { uri: normalizedUri }
}).href,
'_blank'
)
}

function isIMASUri(uri: string): boolean {
// Check if URI starts with IMAS protocol and IBEX is enabled
return uri.startsWith('imas:') && config.ibexEnabled === true
}

</script>

<template>
Expand Down Expand Up @@ -368,7 +409,23 @@ function sortOutputs(key: string) {
</thead>
<tbody>
<tr v-for="input in sortedInputs" :key="input.uuid.hex">
<td>{{ input.uri }}</td>
<td class="pl-1">
<!-- Clickable IMAS URI -->
<div
v-if="isIMASUri(input.uri)"
class="uri-cell"
@click="navigateToIDS(input.uri)"
:title="input.uri"
>
<v-icon size="small" class="mr-2">mdi-link-variant</v-icon>
{{ input.uri }}
<v-icon size="x-small" class="ml-2">mdi-open-in-new</v-icon>
</div>
<!-- Non-clickable other URIs -->
<div v-else class="text-truncate" :title="input.uri">
{{ input.uri }}
</div>
</td>
</tr>
<tr v-if="inputs.length === 0">
<td>No input data</td>
Expand Down Expand Up @@ -407,7 +464,23 @@ function sortOutputs(key: string) {
</thead>
<tbody>
<tr v-for="output in sortedOutputs" :key="output.uuid.hex">
<td>{{ output.uri }}</td>
<td class="pl-1">
<!-- Clickable IMAS URI -->
<div
v-if="isIMASUri(output.uri)"
class="uri-cell"
@click="navigateToIDS(output.uri)"
:title="output.uri"
>
<v-icon size="small" class="mr-2">mdi-link-variant</v-icon>
{{ output.uri }}
<v-icon size="x-small" class="ml-2">mdi-open-in-new</v-icon>
</div>
<!-- Non-clickable other URIs -->
<div v-else class="text-truncate" :title="output.uri">
{{ output.uri }}
</div>
</td>
</tr>
<tr v-if="outputs.length === 0">
<td>No output data</td>
Expand Down Expand Up @@ -511,3 +584,25 @@ function sortOutputs(key: string) {
</v-row>
</v-container>
</template>

<style scoped>
.uri-cell {
cursor: pointer;
color: var(--v-primary-base);
padding: 12px;
display: flex;
align-items: center;
border-radius: 4px;
transition: background-color 0.2s;
}
.uri-cell:hover {
background: rgba(33, 150, 243, 0.1);
text-decoration: underline;
}
.text-truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding: 12px;
}
</style>
Loading
Loading