Skip to content

Commit

Permalink
Fetch config and datasets from backend + run sync (#25)
Browse files Browse the repository at this point in the history
* Fetch config and datasets from backend

* Filter out havested metadata

* Run synchronize and show logs

* Remove changing primevue locale as it does not works

See: primefaces/primevue#6872

* Add route copy_preview

* Add confirmation step

* Add readonly source platform field

* Use dev_config.yaml

* Mock vue-i18n

* Add spinner and disable button

* Rename involvedResources to copyPreview

* Skip failing test which dpends on external data
  • Loading branch information
arnaud-morvan authored Feb 17, 2025
1 parent 7c9bdd4 commit 58fda1d
Show file tree
Hide file tree
Showing 20 changed files with 679 additions and 51 deletions.
81 changes: 81 additions & 0 deletions backend/maelstro/core/clone.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,87 @@ def gn_dst(self) -> GnApi:
def gs_dst(self) -> RestService:
return self.geo_hnd.get_gs_service(self.dst_name, is_source=False)

def copy_preview(
self,
copy_meta: bool,
copy_layers: bool,
copy_styles: bool,
) -> dict[str, list[dict[str, Any]]]:
self.copy_meta = copy_meta
self.copy_layers = copy_layers
self.copy_styles = copy_styles

with get_georchestra_handler() as geo_hnd:
self.geo_hnd = geo_hnd

zipdata = self.gn_src.get_record_zip(self.uuid).read()
self.meta = Meta(zipdata)

preview: dict[str, list[dict[str, Any]]] = {
"metadata": [],
"data": [],
}

src_gn_info = self.geo_hnd.get_service_info(
self.src_name, is_source=True, is_geonetwork=True
)
src_gn_url = src_gn_info["url"]
dst_gn_info = self.geo_hnd.get_service_info(
self.dst_name, is_source=False, is_geonetwork=True
)
dst_gn_url = dst_gn_info["url"]

preview["metadata"].append(
{
"src": src_gn_url,
"dst": dst_gn_url,
"metadata": (
[
{
"title": self.meta.get_title(),
}
]
if self.copy_meta
else []
),
}
)

dst_gs_info = self.geo_hnd.get_service_info(
self.dst_name, is_source=False, is_geonetwork=False
)
dst_gs_url = dst_gs_info["url"]

geoservers = self.meta.get_gs_layers(config.get_gs_sources())
for server_url, layer_names in geoservers.items():
styles: set[str] = set()
for layer_name in layer_names:

gs_src = self.geo_hnd.get_gs_service(server_url, True)
layers = {}
for layer_name in layer_names:
resp = gs_src.rest_client.get(f"/rest/layers/{layer_name}.json")
raise_for_status(resp)
layers[layer_name] = resp.json()

for layer in layers.values():
styles.update(self.get_styles_from_layer(layer).keys())

preview["data"].append(
{
"src": server_url,
"dst": dst_gs_url,
"layers": (
[str(layer_name) for layer_name in layer_names]
if self.copy_layers
else []
),
"styles": list(styles) if self.copy_styles else [],
}
)

return preview

def clone_dataset(
self,
copy_meta: bool,
Expand Down
13 changes: 13 additions & 0 deletions backend/maelstro/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,19 @@ def get_layers(src_name: str, uuid: str) -> list[dict[str, str]]:
return meta.get_ogc_geoserver_layers()


@app.get("/copy_preview")
def get_copy_preview(
src_name: str,
dst_name: str,
metadataUuid: str,
copy_meta: bool = True,
copy_layers: bool = True,
copy_styles: bool = True,
) -> dict[str, Any]:
clone_ds = CloneDataset(src_name, dst_name, metadataUuid)
return clone_ds.copy_preview(copy_meta, copy_layers, copy_styles)


@app.put(
"/copy",
responses={
Expand Down
1 change: 1 addition & 0 deletions backend/tests/test_API.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ def test_search4():
assert response.json()['hits']['total']['value'] == 22


@pytest.mark.skip("Depends on extenal data")
def test_search5():
response = client.post("/search/GeonetworkRennes", json={
"source": [],
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ services:
condition: service_completed_successfully
environment:
MAELSTRO_CONFIG: /app/dev_config.yaml
DEMO_LOGIN: testadmin
DEMO_PASSWORD: testadmin
LOCAL_LOGIN: testadmin
LOCAL_PASSWORD: testadmin
PGHOST: "database"
Expand Down
17 changes: 17 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"vue-router": "^4.5.0"
},
"devDependencies": {
"@pinia/testing": "^0.1.7",
"@tsconfig/node22": "^22.0.0",
"@types/jsdom": "^21.1.7",
"@types/node": "^22.13.1",
Expand Down
49 changes: 49 additions & 0 deletions frontend/src/components/LogsReport.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<script setup lang="ts">
import type { Log } from '@/services/synchronize.service'
defineProps<{ logs: Log[] }>()
const logClass = (status_code: number) => {
if (status_code >= 200 && status_code < 300) {
return 'log-success'
} else {
return 'log-error'
}
}
</script>

<template>
<div class="mb-4 font-semibold">Logs</div>
<div v-for="(log, index) in logs" :key="index">
<div v-if="['POST', 'PUT'].includes(log.method!)" :class="logClass(log.status_code!)">
{{ log.method }} {{ log.url }}
</div>
<div v-if="log.operation" class="log-info">{{ log.operation }}</div>
</div>
</template>

<style scoped>
.log-info {
background-color: #e0f7fa; /* Bleu clair */
border-left: 5px solid #039be5;
padding: 10px;
margin-bottom: 5px;
border-radius: 5px;
}
.log-success {
background-color: #e8f5e9; /* Vert pâle */
border-left: 5px solid #43a047; /* Bordure vert plus foncé */
padding: 10px;
margin-bottom: 5px;
border-radius: 5px;
}
.log-error {
background-color: #ffebee; /* Rouge clair */
border-left: 5px solid #d32f2f;
padding: 10px;
margin-bottom: 5px;
border-radius: 5px;
}
</style>
20 changes: 20 additions & 0 deletions frontend/src/components/__tests__/LogsReport.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import LogsReport from '../LogsReport.vue'

describe('LogsReport', () => {
it('renders properly', () => {
const wrapper = mount(LogsReport, {
props: {
logs: [
{
method: 'PUT',
status_code: 200,
url: 'http://proxy:8080/geoserver/rest/styles/point.sld',
},
],
},
})
expect(wrapper.text()).toContain('http://proxy:8080/geoserver/rest/styles/point.sld')
})
})
13 changes: 12 additions & 1 deletion frontend/src/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,16 @@
"User guide": "Guide d'utilisation",
"Metadata": "Métadonnées",
"Layers": "Couches",
"Styles": "Styles"
"Styles": "Styles",
"Dataset is mandatory": "Le champ métadonnées est obligatoire",
"Destination is mandatory": "Le champ destination est obligatoire",
"Source platform": "Plateforme source",
"Source:": "Source :",
"Destination:": "Destination :",
"Metadata:": "Métadonnées :",
"Layers:": "Couches :",
"Styles:": "Styles :",
"Go back to form": "Retourner au formulaire",
"Confirm": "Confirmer",
"Following data and metadata will be copied:": "Les données et métadonnées suivantes seront copiées :"
}
12 changes: 2 additions & 10 deletions frontend/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Aura from '@primevue/themes/aura'
import { createPinia } from 'pinia'
import PrimeVue, { usePrimeVue, type PrimeVueLocaleOptions } from 'primevue/config'
import { createApp, watch } from 'vue'
import PrimeVue, { type PrimeVueLocaleOptions } from 'primevue/config'
import { createApp } from 'vue'
import App from './App.vue'
import './assets/main.css'
import router from './router'
Expand Down Expand Up @@ -51,12 +51,4 @@ app.use(PrimeVue, {
locale: primeLocales[i18n.global.locale],
})

watch(
() => i18n.global.locale, // Observe la langue actuelle de Vue I18n
(newLocale) => {
const primevue = usePrimeVue()
primevue.config.locale = primeLocales[newLocale] || primeLocales.en
},
)

app.mount('#app')
4 changes: 4 additions & 0 deletions frontend/src/router/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createRouter, createWebHistory } from 'vue-router'
import FormView from '../views/FormView.vue'
import BaseLayout from '@/layouts/BaseLayout.vue'
import { useConfigStore } from '@/stores/config.store'

const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
Expand All @@ -21,6 +22,9 @@ const router = createRouter({
path: 'synchronize',
name: 'synchronize',
component: FormView,
beforeEnter: async () => {
await useConfigStore().fetchConfig()
},
},
],
},
Expand Down
22 changes: 22 additions & 0 deletions frontend/src/services/config.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export type Source = {
name: string
url: string
}

export type Destination = {
name: string
gn_url: string
gs_url: string
}

export const configService = {
async getSources(): Promise<Source[]> {
const response = await fetch('/maelstro-backend/sources')
return await response.json()
},

async getDestinations(): Promise<Destination[]> {
const response = await fetch('/maelstro-backend/destinations')
return await response.json()
},
}
81 changes: 81 additions & 0 deletions frontend/src/services/geonetworkSearch.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
export type SearchResult = {
uuid: string
resourceTitleObject: {
default: string
}
resourceAbstractObject: {
default: string
}
}

interface GnSource {
uuid: string
resourceTitleObject: {
default: string
}
resourceAbstractObject: {
default: string
}
}

interface GnHit {
_source: GnSource
}

export const geonetworkSearchService = {
async search(sourceName: string, query: string): Promise<SearchResult[]> {
// const url =
// 'https://demo.georchestra.org/geonetwork/srv/api/search/records/_search?bucket=bucket'

const url = `/maelstro-backend/search/${sourceName}`

const searchResponse = await fetch(url, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: {
bool: {
must: [
{ terms: { isTemplate: ['n'] } },
{
multi_match: {
query: query,
type: 'bool_prefix',
fields: [
'resourceTitleObject.*',
'resourceAbstractObject.*',
'tag',
'resourceIdentifier',
],
},
},
],
must_not: [
{
terms: {
resourceType: ['service', 'map', 'map/static', 'mapDigital'],
},
},
{ term: { isHarvested: true } },
],
},
},
_source: ['resourceTitleObject', 'resourceAbstractObject', 'uuid'],
from: 0,
size: 20,
}),
})

const hits = (await searchResponse.json()).hits.hits
if (!hits) return []

return hits.map((hit: GnHit) => ({
uuid: hit._source.uuid,
resourceTitleObject: hit._source.resourceTitleObject,
resourceAbstractObject: hit._source.resourceAbstractObject,
}))
},
}
Loading

0 comments on commit 58fda1d

Please sign in to comment.