initial
This commit is contained in:
parent
aa7c30950c
commit
72716e1c28
41 changed files with 15461 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
.DS_Store
|
||||||
|
backend/target/*
|
||||||
|
backend/.idea
|
||||||
3
backend/.gitignore
vendored
Normal file
3
backend/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
backend/target
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
9
backend/README.md
Normal file
9
backend/README.md
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
# Start the Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn clean spring-boot:run
|
||||||
|
```
|
||||||
|
|
||||||
|
That's it. The server will be using port 8081 which can be changed in `application.properties`
|
||||||
|
|
||||||
|
Now go to the `../frontend` folder and follow the `README` instructions
|
||||||
41
backend/pom.xml
Normal file
41
backend/pom.xml
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
|
<version>3.5.7</version>
|
||||||
|
<relativePath/> <!-- lookup parent from repository -->
|
||||||
|
</parent>
|
||||||
|
<groupId>com.mycompany.backend</groupId>
|
||||||
|
<artifactId>demo</artifactId>
|
||||||
|
<version>0.0.1-SNAPSHOT</version>
|
||||||
|
<name>demo</name>
|
||||||
|
<description>Demo project for Spring Boot</description>
|
||||||
|
<properties>
|
||||||
|
<java.version>25</java.version>
|
||||||
|
</properties>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
</project>
|
||||||
13
backend/src/main/java/com/mycompany/backend/demo/Main.java
Normal file
13
backend/src/main/java/com/mycompany/backend/demo/Main.java
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
package com.mycompany.backend.demo;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
public class Main {
|
||||||
|
|
||||||
|
static void main(String[] args) {
|
||||||
|
SpringApplication.run(Main.class, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package com.mycompany.backend.demo.api;
|
||||||
|
|
||||||
|
import com.mycompany.backend.demo.data.DataType;
|
||||||
|
import com.mycompany.backend.demo.data.DataTypeConfiguration;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
|
||||||
|
public interface DataService {
|
||||||
|
|
||||||
|
DataTypeConfiguration getConfiguration();
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
DataType getDataType(String id);
|
||||||
|
|
||||||
|
DataTypeConfiguration updateType(DataType newEntity);
|
||||||
|
|
||||||
|
void deleteType(String id);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
package com.mycompany.backend.demo.config;
|
||||||
|
|
||||||
|
import com.mycompany.backend.demo.api.DataService;
|
||||||
|
import com.mycompany.backend.demo.data.DataServiceImpl;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* These beans will be created on boot time and can be later injected in any service/component.
|
||||||
|
* @see org.springframework.context.annotation.Configuration
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class AppConfiguration implements WebMvcConfigurer {
|
||||||
|
private static final Logger LOGGER = Logger.getLogger(AppConfiguration.class.getName());
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public DataService repository() {
|
||||||
|
LOGGER.info("Initializing data repository bean...");
|
||||||
|
return new DataServiceImpl();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addCorsMappings(CorsRegistry registry) {
|
||||||
|
registry.addMapping("/**") // all endpoints
|
||||||
|
.allowedOrigins("*") // no restrictions
|
||||||
|
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
||||||
|
.allowedHeaders("*");
|
||||||
|
|
||||||
|
LOGGER.info("CORS policies configured for the REST API");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
package com.mycompany.backend.demo.data;
|
||||||
|
|
||||||
|
import com.mycompany.backend.demo.api.DataService;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple in-memory database
|
||||||
|
*/
|
||||||
|
public class DataServiceImpl implements DataService {
|
||||||
|
private static final List<DataType> INITIAL_DATA = List.of(
|
||||||
|
new DataType("id1", "Asset", List.of(
|
||||||
|
new DataType.Property("category", "string")
|
||||||
|
)),
|
||||||
|
|
||||||
|
new DataType("id2", "Employee", List.of(
|
||||||
|
new DataType.Property("name", "string"),
|
||||||
|
new DataType.Property("email", "string")
|
||||||
|
))
|
||||||
|
);
|
||||||
|
|
||||||
|
private final Map<String, DataType> dataTypes = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public DataServiceImpl() {
|
||||||
|
// Initialize with initial data (won't be needed with a database layer)
|
||||||
|
for (DataType s : INITIAL_DATA) {
|
||||||
|
dataTypes.put(s.id(), s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DataTypeConfiguration getConfiguration() {
|
||||||
|
var sorted = new ArrayList<>(dataTypes.values());
|
||||||
|
sorted.sort(Comparator.comparing(DataType::name));
|
||||||
|
return new DataTypeConfiguration(sorted);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DataType getDataType(String id) {
|
||||||
|
return dataTypes.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DataTypeConfiguration updateType(DataType newEntity) {
|
||||||
|
dataTypes.put(newEntity.id(), newEntity);
|
||||||
|
// 1. Do other stuff here, e.g. update the related objects.
|
||||||
|
// This part must be performed in a single transaction
|
||||||
|
|
||||||
|
// 2. Return the updated configuration with all the changes included
|
||||||
|
return getConfiguration();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deleteType(String id) {
|
||||||
|
dataTypes.remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
package com.mycompany.backend.demo.data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record DataType(String id, String name, List<Property > properties) {
|
||||||
|
|
||||||
|
record Property(String name, String type) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package com.mycompany.backend.demo.data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A dummy global configuration structure, like customer-related ontologies.
|
||||||
|
* For now, only
|
||||||
|
* @param dataTypes
|
||||||
|
*/
|
||||||
|
public record DataTypeConfiguration(List<DataType> dataTypes) implements Serializable {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
/**
|
||||||
|
* Mocks of the data-layer and service classes.
|
||||||
|
*/
|
||||||
|
package com.mycompany.backend.demo.data;
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
package com.mycompany.backend.demo.rest;
|
||||||
|
|
||||||
|
import com.mycompany.backend.demo.api.DataService;
|
||||||
|
import com.mycompany.backend.demo.data.DataType;
|
||||||
|
import com.mycompany.backend.demo.data.DataTypeConfiguration;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/data")
|
||||||
|
public class StaticRoutes {
|
||||||
|
private static final Logger LOGGER = Logger.getLogger(StaticRoutes.class.getName());
|
||||||
|
|
||||||
|
final DataService dataService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spring Boot will automatically find the {@link DataService} bean as it is
|
||||||
|
* configured in the {@link com.mycompany.backend.demo.config.AppConfiguration}
|
||||||
|
*
|
||||||
|
* @param dataService bean
|
||||||
|
*/
|
||||||
|
public StaticRoutes(DataService dataService) {
|
||||||
|
this.dataService = dataService;
|
||||||
|
LOGGER.info("Basic static routes created.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple "get all" endpoint
|
||||||
|
*/
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity<DataTypeConfiguration> listDataTypes() {
|
||||||
|
LOGGER.info("Returning all data types...");
|
||||||
|
return ResponseEntity.ok(dataService.getConfiguration());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple "get by id" endpoint
|
||||||
|
*/
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ResponseEntity<DataType> getDataType(@PathVariable("id") String id) {
|
||||||
|
LOGGER.info("Returning all data types...");
|
||||||
|
var found = dataService.getDataType(id);
|
||||||
|
if (found == null) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
} else {
|
||||||
|
return ResponseEntity.ok(found);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This endpoint updates a single data structure element and returns a new list
|
||||||
|
* of ontologies (they may be interconnected)
|
||||||
|
*
|
||||||
|
* @param modified the updated object
|
||||||
|
* @return the list
|
||||||
|
*/
|
||||||
|
@PostMapping
|
||||||
|
public ResponseEntity<DataTypeConfiguration> updateConfiguration(@RequestBody DataType modified) {
|
||||||
|
LOGGER.info("Updating data type and related data: " + modified);
|
||||||
|
return ResponseEntity.ok(dataService.updateType(modified));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public ResponseEntity<Void> delete(@PathVariable("id") String id) {
|
||||||
|
LOGGER.info("Deleting data type with id: " + id);
|
||||||
|
dataService.deleteType(id);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
3
backend/src/main/resources/application-dev.properties
Normal file
3
backend/src/main/resources/application-dev.properties
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# This "dev" profile will override the default application.properties
|
||||||
|
# See the Spring Boot docs on how to start different profiles in IDE on via command line
|
||||||
|
logging.level.root = trace
|
||||||
3
backend/src/main/resources/application.properties
Normal file
3
backend/src/main/resources/application.properties
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
spring.application.name=DEMO APP
|
||||||
|
server.port=8081
|
||||||
|
server.servlet.context-path=/v1
|
||||||
17
backend/src/main/resources/static/index.html
Normal file
17
backend/src/main/resources/static/index.html
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Hi There!</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<h3>Hi There!</h3>
|
||||||
|
<p>
|
||||||
|
In addition to the REST endpoints, Spring Boot can also serve static resources.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
For REST-only production deployments, serving static pages is usually disabled.
|
||||||
|
</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package com.mycompany.backend.demo;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
class MainTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void contextLoads() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
24
frontend/.gitignore
vendored
Normal file
24
frontend/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Nuxt dev/build outputs
|
||||||
|
.output
|
||||||
|
.data
|
||||||
|
.nuxt
|
||||||
|
.nitro
|
||||||
|
.cache
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Node dependencies
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.DS_Store
|
||||||
|
.fleet
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Local env files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
14
frontend/.vscode/settings.json
vendored
Normal file
14
frontend/.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"github.copilot.nextEditSuggestions.enabled": false,
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"files.autoSave": "onFocusChange",
|
||||||
|
"chat.agent.enabled": false,
|
||||||
|
|
||||||
|
"files.associations": {
|
||||||
|
"main.css": "tailwindcss"
|
||||||
|
},
|
||||||
|
|
||||||
|
"editor.accessibilitySupport": "off"
|
||||||
|
}
|
||||||
44
frontend/README.md
Normal file
44
frontend/README.md
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
# Nuxt Minimal Starter
|
||||||
|
|
||||||
|
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Make sure to install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Spring Boot
|
||||||
|
|
||||||
|
Make sure the backend is running:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ../backend
|
||||||
|
mvn clean spring-boot:run
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Server
|
||||||
|
|
||||||
|
Start the development server on `http://localhost:3000`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Production
|
||||||
|
|
||||||
|
Build the application for production. We don't use the server-side rendering, so we don't do the `build` command
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run generate
|
||||||
|
```
|
||||||
|
|
||||||
|
Locally preview production build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
|
||||||
81
frontend/app/API.ts
Normal file
81
frontend/app/API.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
import type { DataType, DataTypeConfiguration } from "./types";
|
||||||
|
|
||||||
|
enum APIMethod {
|
||||||
|
GET = "GET",
|
||||||
|
POST = "POST",
|
||||||
|
PUT = "PUT",
|
||||||
|
DELETE = "DELETE",
|
||||||
|
}
|
||||||
|
|
||||||
|
export class API {
|
||||||
|
private readonly apiBaseUrl: string;
|
||||||
|
|
||||||
|
constructor(apiBaseUrl: string) {
|
||||||
|
this.apiBaseUrl = apiBaseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
readConfiguration(): Promise<DataTypeConfiguration> {
|
||||||
|
return this.call(APIMethod.GET, "/data");
|
||||||
|
}
|
||||||
|
|
||||||
|
readDataType(id: string): Promise<DataType> {
|
||||||
|
return this.call(APIMethod.GET, "/data/" + id);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteDataType(type: DataType): Promise<void> {
|
||||||
|
return this.call(APIMethod.DELETE, "/data/" + type.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
saveDataType(type: DataType): Promise<DataTypeConfiguration> {
|
||||||
|
return this.call(APIMethod.POST, "/data", type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Helper methods
|
||||||
|
*/
|
||||||
|
private fullUrl(url: string): string {
|
||||||
|
return this.apiBaseUrl + url;
|
||||||
|
}
|
||||||
|
|
||||||
|
private requestOptions<A>(method: APIMethod, obj?: A): RequestInit {
|
||||||
|
const headers: HeadersInit = new Headers();
|
||||||
|
const options: RequestInit = {
|
||||||
|
method: method,
|
||||||
|
headers: headers,
|
||||||
|
};
|
||||||
|
if (obj !== undefined) {
|
||||||
|
const objType = typeof obj;
|
||||||
|
if (objType === "object") {
|
||||||
|
options.body = JSON.stringify(obj);
|
||||||
|
headers.set("Content-Type", "application/json; charset=UTF-8");
|
||||||
|
} else {
|
||||||
|
throw new Error("Unsupported type: " + objType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async call<T, A>(
|
||||||
|
method: APIMethod,
|
||||||
|
url: string,
|
||||||
|
obj?: A
|
||||||
|
): Promise<T> {
|
||||||
|
const options = this.requestOptions(method, obj);
|
||||||
|
const response = await fetch(this.fullUrl(url), options);
|
||||||
|
const status = response.status;
|
||||||
|
const ok = status >= 200 && status < 400;
|
||||||
|
if (ok) {
|
||||||
|
return await response.json();
|
||||||
|
} else {
|
||||||
|
const errorText = await response.text();
|
||||||
|
return Promise.reject(errorText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static instance(): API {
|
||||||
|
const runtimeConfig = useRuntimeConfig();
|
||||||
|
// See the `nuxt.config.ts` file
|
||||||
|
const apiBaseUrl = runtimeConfig.public.backend.baseUrl;
|
||||||
|
return new API(apiBaseUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
15
frontend/app/app.vue
Normal file
15
frontend/app/app.vue
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<template>
|
||||||
|
<UApp>
|
||||||
|
<NuxtRouteAnnouncer />
|
||||||
|
<!-- The 'main' template from the app/layout folder -->
|
||||||
|
<NuxtLayout name="main">
|
||||||
|
<NuxtPage />
|
||||||
|
</NuxtLayout>
|
||||||
|
</UApp>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
// This is the 'main' script.
|
||||||
|
// Nothing is happening here, see the 'main' template for the initialization logic
|
||||||
|
|
||||||
|
console.log("app initialized...");
|
||||||
|
</script>
|
||||||
7
frontend/app/assets/css/main.css
Normal file
7
frontend/app/assets/css/main.css
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
@import "tailwindcss";
|
||||||
|
@import "@nuxt/ui";
|
||||||
|
@plugin "@tailwindcss/typography"; /* Basic topography */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--ui-container: 72rem; /* make the main container smaller */
|
||||||
|
}
|
||||||
48
frontend/app/components/ConfirmationModal.vue
Normal file
48
frontend/app/components/ConfirmationModal.vue
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
<template>
|
||||||
|
<u-modal
|
||||||
|
title="Confirmation"
|
||||||
|
description="Confirmation required for the selected action"
|
||||||
|
:dismissible="false"
|
||||||
|
>
|
||||||
|
<template #body>
|
||||||
|
<p class="my-3">
|
||||||
|
Type <strong>{{ message }}</strong> to confirm.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<u-field-group class="w-full">
|
||||||
|
<u-input
|
||||||
|
v-model="inputMessage"
|
||||||
|
name="user-input"
|
||||||
|
autocomplete="false"
|
||||||
|
type="text"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
<u-button color="error" :disabled="!matched" @click="onSubmit">
|
||||||
|
Submit
|
||||||
|
</u-button>
|
||||||
|
</u-field-group>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</u-modal>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { UModal, UInput, UButton } from "#components";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{ close: [boolean] }>();
|
||||||
|
|
||||||
|
const inputMessage = ref("");
|
||||||
|
|
||||||
|
const matched = computed(() => {
|
||||||
|
return inputMessage.value === props.message;
|
||||||
|
});
|
||||||
|
|
||||||
|
function onSubmit() {
|
||||||
|
emit("close", true);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
96
frontend/app/components/DataTypeEditor.vue
Normal file
96
frontend/app/components/DataTypeEditor.vue
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
<template>
|
||||||
|
<div class="grid grid-cols-2 gap-5">
|
||||||
|
<u-card variant="soft">
|
||||||
|
<u-form-field label="Type Name">
|
||||||
|
<u-input v-model="type.name" class="w-full" />
|
||||||
|
</u-form-field>
|
||||||
|
<table class="w-full">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="w-0">#</th>
|
||||||
|
<th>Property Name</th>
|
||||||
|
<th>Property Type</th>
|
||||||
|
<th> </th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(property, idx) in type.properties" :key="idx">
|
||||||
|
<th class="align-top w-0 pt-4">{{ idx + 1 }}</th>
|
||||||
|
<td>
|
||||||
|
<u-input v-model="property.name" class="w-full" />
|
||||||
|
</td>
|
||||||
|
<td class="align-middle">
|
||||||
|
<u-radio-group
|
||||||
|
v-model="property.type"
|
||||||
|
:items="typeItems"
|
||||||
|
orientation="vertical"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td class="align-middle">
|
||||||
|
<u-button
|
||||||
|
icon="i-bi-trash"
|
||||||
|
size="xs"
|
||||||
|
color="error"
|
||||||
|
@click="deleteProperty(idx)"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<u-button
|
||||||
|
icon="i-bi-plus"
|
||||||
|
size="sm"
|
||||||
|
color="neutral"
|
||||||
|
variant="outline"
|
||||||
|
@click="addProperty"
|
||||||
|
>
|
||||||
|
New Property
|
||||||
|
</u-button>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<p class="text-xs muted">
|
||||||
|
Add and delete type parameters, change their types, the dynamic form
|
||||||
|
on the right will be automatically updated.
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
</u-card>
|
||||||
|
<div v-if="allParametersAreValid">
|
||||||
|
<h4 style="margin-top: 0">Sample Form</h4>
|
||||||
|
<simple-form :type="type" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { DataType, UiPropertyType } from "~/types";
|
||||||
|
|
||||||
|
// Here we define our type as a model, meaning that any changes to this object will be published back to the parent view
|
||||||
|
const type = defineModel<DataType>({ required: true });
|
||||||
|
|
||||||
|
const typeItems = ref<UiPropertyType[]>(["number", "string", "text"]);
|
||||||
|
|
||||||
|
// Form won't be shown if there are empty parameters
|
||||||
|
const allParametersAreValid = computed(() => {
|
||||||
|
const typeProps = type.value.properties;
|
||||||
|
if (typeProps.length == 0) {
|
||||||
|
return false; // No properties at all
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const p of typeProps) {
|
||||||
|
if (p.name.length === 0) {
|
||||||
|
return false; // Missing a name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
function deleteProperty(index: number) {
|
||||||
|
const copy = type.value.properties;
|
||||||
|
copy.splice(index, 1);
|
||||||
|
type.value.properties = copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addProperty() {
|
||||||
|
type.value.properties.push({ name: "", type: "string" });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
42
frontend/app/components/NavBar.vue
Normal file
42
frontend/app/components/NavBar.vue
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
<template>
|
||||||
|
<UNavigationMenu :items="items" class="w-full justify-start bg-neutral-50">
|
||||||
|
<template #list-trailing>
|
||||||
|
<div class="flex flex-1 justify-end mx-2">
|
||||||
|
<UBadge
|
||||||
|
icon="i-mdi-clock-outline"
|
||||||
|
size="md"
|
||||||
|
color="secondary"
|
||||||
|
variant="solid"
|
||||||
|
class="font-mono"
|
||||||
|
>
|
||||||
|
{{ currentTime }}
|
||||||
|
</UBadge>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UNavigationMenu>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { NavigationMenuItem } from "@nuxt/ui";
|
||||||
|
import { appState } from "#imports";
|
||||||
|
|
||||||
|
const state = appState();
|
||||||
|
|
||||||
|
const currentTime = computed(() => {
|
||||||
|
return state.value.time.toLocaleTimeString();
|
||||||
|
});
|
||||||
|
|
||||||
|
const items = ref<NavigationMenuItem[]>([
|
||||||
|
{
|
||||||
|
icon: "i-mdi-home",
|
||||||
|
to: "/",
|
||||||
|
active: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Data Structures",
|
||||||
|
icon: "i-material-symbols-account-tree-outline-rounded",
|
||||||
|
to: "/data",
|
||||||
|
active: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
</script>
|
||||||
41
frontend/app/components/SimpleBreadcrumb.vue
Normal file
41
frontend/app/components/SimpleBreadcrumb.vue
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
<template>
|
||||||
|
<u-breadcrumb :items="items" class="my-nav mb-3" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { BreadcrumbItem } from "@nuxt/ui";
|
||||||
|
import { UBreadcrumb } from "#components";
|
||||||
|
import type { NavRoute } from "~/ui";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
previous: NavRoute[];
|
||||||
|
last: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = computed(() => {
|
||||||
|
const arr: BreadcrumbItem[] = [];
|
||||||
|
for (const h of props.previous) {
|
||||||
|
arr.push({
|
||||||
|
label: h.title,
|
||||||
|
to: h.path,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
arr.push({
|
||||||
|
label: props.last,
|
||||||
|
disabled: true,
|
||||||
|
class: "text-muted",
|
||||||
|
});
|
||||||
|
return arr;
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Override default high margins for <ol> lists */
|
||||||
|
.my-nav ol {
|
||||||
|
margin-top: 0 !important;
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
padding-left: 0 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
66
frontend/app/components/SimpleForm.vue
Normal file
66
frontend/app/components/SimpleForm.vue
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="p-2 bg-neutral-50 rounded-lg shadow-neutral-300 shadow-sm">
|
||||||
|
<table class="table">
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(prop, idx) in type.properties" :key="idx">
|
||||||
|
<td class="font-semibold w-0 text-nowrap px-2">
|
||||||
|
{{ prop.name }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<u-input
|
||||||
|
v-if="prop.type === 'string'"
|
||||||
|
v-model="formData[prop.name]"
|
||||||
|
type="text"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
<u-textarea
|
||||||
|
v-if="prop.type === 'text'"
|
||||||
|
v-model="formData[prop.name]"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
<u-input
|
||||||
|
v-if="prop.type === 'number'"
|
||||||
|
v-model="formData[prop.name]"
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="mx-0">Form Data</h4>
|
||||||
|
<pre class="mx-0 my-0">{{ formData }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { DataType } from "~/types";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
type: DataType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const formData = ref<Record<string, string>>({});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
props,
|
||||||
|
() => {
|
||||||
|
// Property value changed, resetting the form data
|
||||||
|
formData.value = {};
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Override default high margins for <ol> lists */
|
||||||
|
.table {
|
||||||
|
margin-top: 0 !important;
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
10
frontend/app/composables/application.ts
Normal file
10
frontend/app/composables/application.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { AppState } from "~/ui";
|
||||||
|
|
||||||
|
// Important: this exposes a global (per web app) state of type `AppState`.
|
||||||
|
// As with any other `Ref` variable, when the state changes, dependent views are automatically updated.
|
||||||
|
export const appState = (): Ref<AppState> => {
|
||||||
|
return useState(
|
||||||
|
"some-unique-state-key-1",
|
||||||
|
() => new AppState(new Date()) // Initial value
|
||||||
|
);
|
||||||
|
};
|
||||||
29
frontend/app/layouts/main.vue
Normal file
29
frontend/app/layouts/main.vue
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- This is our custom navbar component. It has been automatically imported from the 'components' folder -->
|
||||||
|
<NavBar />
|
||||||
|
<UContainer>
|
||||||
|
<!-- the 'prose' class will apply basic typography for the whole app -->
|
||||||
|
<div class="w-full prose max-w-none py-5">
|
||||||
|
<!-- This is where all the pages will be rendered -->
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</UContainer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { appState } from "#imports";
|
||||||
|
|
||||||
|
const state = appState();
|
||||||
|
|
||||||
|
// A demo
|
||||||
|
|
||||||
|
// This hook will be called when the layout has beed initialized
|
||||||
|
onMounted(async () => {
|
||||||
|
setInterval(() => {
|
||||||
|
state.value.time = new Date();
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("app initialized...");
|
||||||
|
</script>
|
||||||
84
frontend/app/pages/data/[id].vue
Normal file
84
frontend/app/pages/data/[id].vue
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="loadedType">
|
||||||
|
<!-- Show our basic breadcrumb view -->
|
||||||
|
<simple-breadcrumb
|
||||||
|
:last="loadedType.name"
|
||||||
|
:previous="[{ path: '/data', title: 'Data Structures' }]"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Show our editor -->
|
||||||
|
<data-type-editor v-model="loadedType" />
|
||||||
|
|
||||||
|
<u-button
|
||||||
|
color="primary"
|
||||||
|
size="xl"
|
||||||
|
class="my-3"
|
||||||
|
@click="onSave(loadedType)"
|
||||||
|
>
|
||||||
|
<span class="px-3">Save</span>
|
||||||
|
</u-button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="px-3 py-2 border-l-4 border-l-red-700 bg-red-100 mt-5 prose-sm"
|
||||||
|
>
|
||||||
|
<h2 style="margin: 0" class="text-red-700">Important</h2>
|
||||||
|
<ol class="list-decimal">
|
||||||
|
<li>
|
||||||
|
All dynamic changes are performed on a
|
||||||
|
<strong>local copy</strong> of the data that we received from the
|
||||||
|
backend during the page load event.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
If a view requires additional data to be rendered, that data should
|
||||||
|
also be loaded, or a more complex data structure must be sent by the
|
||||||
|
backend.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Additional data can also be loaded dynamically in response to
|
||||||
|
various UI events.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>But!</strong> The save operation is performed atomically in
|
||||||
|
a single <strong>ACID</strong> transaction on the server side. This
|
||||||
|
is the only way we can avoid partial saves or discrepancies in
|
||||||
|
business-critical data. See the architecture proposal for more
|
||||||
|
details.
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else>Loading ....</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { API } from "~/API";
|
||||||
|
import type { DataType } from "~/types";
|
||||||
|
|
||||||
|
// Current page's route (path)
|
||||||
|
const route = useRoute();
|
||||||
|
const api = API.instance();
|
||||||
|
|
||||||
|
// The loading state, initially undefined
|
||||||
|
const loadedType = ref<DataType | undefined>(undefined);
|
||||||
|
|
||||||
|
function onSave(type: DataType) {
|
||||||
|
api.saveDataType(type).then(() => {
|
||||||
|
// Get the fresh copy of the data
|
||||||
|
loadTypeFromUrlParameter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadTypeFromUrlParameter();
|
||||||
|
});
|
||||||
|
|
||||||
|
function loadTypeFromUrlParameter() {
|
||||||
|
// Load the type by the page's [id] parameter
|
||||||
|
const dataId = route.params.id as string;
|
||||||
|
api.readDataType(dataId).then((response) => {
|
||||||
|
// Update the loaded type var
|
||||||
|
loadedType.value = response;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
139
frontend/app/pages/data/index.vue
Normal file
139
frontend/app/pages/data/index.vue
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h1>Data Structures</h1>
|
||||||
|
<table class="w-full lg:w-1/2">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="w-0">ID</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th class="w-0"> </th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="type in types.dataTypes" :key="type.id">
|
||||||
|
<td class="w-0 text-nowrap">
|
||||||
|
{{ type.id }}
|
||||||
|
</td>
|
||||||
|
<td class="w-full">
|
||||||
|
<router-link :to="'/data/' + type.id">{{ type.name }}</router-link>
|
||||||
|
</td>
|
||||||
|
<td class="w-0">
|
||||||
|
<u-button
|
||||||
|
icon="i-bi-trash"
|
||||||
|
size="xs"
|
||||||
|
color="error"
|
||||||
|
@click="deleteType(type)"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<u-button color="primary" @click="showNewTypeModal">Create New</u-button>
|
||||||
|
|
||||||
|
<u-modal
|
||||||
|
v-model:open="newTypeDialogShow"
|
||||||
|
:dismissible="false"
|
||||||
|
title="New Type"
|
||||||
|
description="Create and save a new type"
|
||||||
|
>
|
||||||
|
<template #body>
|
||||||
|
<div class="grid grid-cols-1 gap-3">
|
||||||
|
<u-form-field label="ID">
|
||||||
|
<u-input v-model="newType.id" />
|
||||||
|
</u-form-field>
|
||||||
|
<u-form-field label="Name">
|
||||||
|
<u-input v-model="newType.name" />
|
||||||
|
</u-form-field>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<u-button
|
||||||
|
:disabled="newType.id.length === 0 || newType.name.length === 0"
|
||||||
|
@click="saveNewType"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</u-button>
|
||||||
|
</template>
|
||||||
|
</u-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { API } from "~/API";
|
||||||
|
import { DataType, DataTypeConfiguration } from "~/types";
|
||||||
|
import { confirmationDialog } from "~/ui";
|
||||||
|
|
||||||
|
const types = ref<DataTypeConfiguration>(new DataTypeConfiguration([]));
|
||||||
|
|
||||||
|
// Var that holds our new type instance
|
||||||
|
const newType = ref<DataType>(DataType.empty());
|
||||||
|
const newTypeDialogShow = ref(false);
|
||||||
|
|
||||||
|
const api = API.instance();
|
||||||
|
|
||||||
|
// INIT !! This hook will be called when the layout has beed initialized
|
||||||
|
onMounted(readAllTypes);
|
||||||
|
|
||||||
|
function showNewTypeModal() {
|
||||||
|
newType.value = DataType.empty();
|
||||||
|
newTypeDialogShow.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteType(type: DataType) {
|
||||||
|
confirmationDialog().show("delete", () => {
|
||||||
|
// This will be executed on confirmation
|
||||||
|
|
||||||
|
api.deleteDataType(type).finally(() => {
|
||||||
|
// The delete call doesn't return anything, updating the list...
|
||||||
|
readAllTypes();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveNewType() {
|
||||||
|
api
|
||||||
|
.saveDataType(newType.value)
|
||||||
|
.then((configuration) => {
|
||||||
|
// Update the types list
|
||||||
|
types.value = configuration;
|
||||||
|
toaster.add({
|
||||||
|
title: "Success !!!",
|
||||||
|
color: "success",
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
// Just log the error
|
||||||
|
console.error("API response error", e);
|
||||||
|
toaster.add({
|
||||||
|
title: "Backend Error",
|
||||||
|
color: "error",
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
// Reset the new type states
|
||||||
|
newType.value = DataType.empty();
|
||||||
|
newTypeDialogShow.value = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const toaster = useToast();
|
||||||
|
|
||||||
|
function readAllTypes() {
|
||||||
|
// Call the backend to initialize the available data types
|
||||||
|
api
|
||||||
|
.readConfiguration()
|
||||||
|
.then((configuration) => {
|
||||||
|
// Save the configuration in the global state var
|
||||||
|
// (see the description in the state's declaration (app/composables/application.ts)
|
||||||
|
types.value = configuration;
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
// Log & show the error
|
||||||
|
console.error("API response error", e);
|
||||||
|
toaster.add({
|
||||||
|
title: "Backend Error",
|
||||||
|
color: "error",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
52
frontend/app/pages/index.vue
Normal file
52
frontend/app/pages/index.vue
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
<template>
|
||||||
|
<div class="my-3">
|
||||||
|
<h1>Welcome!</h1>
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
|
<!-- left -->
|
||||||
|
<div class="col-span-2">
|
||||||
|
<p>
|
||||||
|
This is a sample <strong>Nuxt</strong> web application that uses
|
||||||
|
<strong>Spring Boot</strong> as a RESTful backend. Launch the backend
|
||||||
|
from your IDE or use the following commands:
|
||||||
|
</p>
|
||||||
|
<pre>
|
||||||
|
cd backend
|
||||||
|
mvn clean spring-boot:run
|
||||||
|
</pre
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
I've left comments in the most important parts of the code. For more
|
||||||
|
details, use the links section.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<!-- right -->
|
||||||
|
<div class="bg-neutral-100 py-2 px-4 rounded-lg">
|
||||||
|
<h3 style="margin-top: 0 !important">Links</h3>
|
||||||
|
<ol>
|
||||||
|
<li>
|
||||||
|
<a href="https://nuxt.com" target="_blank">Nuxt</a> - the core
|
||||||
|
framework based on Vue.js.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="https://ui.nuxt.com" target="_blank">Nuxt UI</a> - utility
|
||||||
|
components and styling.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="https://spring.io/projects/spring-boot" target="_blank">
|
||||||
|
Spring Boot
|
||||||
|
</a>
|
||||||
|
- the most popular Java framework.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="https://github.com/nvm-sh/nvm" target="_blank">
|
||||||
|
Node Version Manager
|
||||||
|
</a>
|
||||||
|
- an absolute must for any npm project.
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts"></script>
|
||||||
27
frontend/app/types.ts
Normal file
27
frontend/app/types.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
// TypeScript variants of the Java classes.
|
||||||
|
// Typically, standalone DTOs (Data Transfer Objects) are used to communicate with the backend,
|
||||||
|
// but we'll keep it simple here.
|
||||||
|
|
||||||
|
export type UiPropertyType = "number" | "string" | "text";
|
||||||
|
|
||||||
|
export class TypeProperty {
|
||||||
|
constructor(public name: string, public type: UiPropertyType) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DataType {
|
||||||
|
constructor(
|
||||||
|
public id: string,
|
||||||
|
public name: string,
|
||||||
|
public properties: TypeProperty[]
|
||||||
|
) {}
|
||||||
|
|
||||||
|
static empty(): DataType {
|
||||||
|
return new DataType("", "", []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DataTypeConfiguration {
|
||||||
|
constructor(public dataTypes: DataType[]) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Runnable = () => void;
|
||||||
30
frontend/app/ui.ts
Normal file
30
frontend/app/ui.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import ConfirmationModal from "./components/ConfirmationModal.vue";
|
||||||
|
import type { Runnable } from "./types";
|
||||||
|
|
||||||
|
// A sample global state of the web app
|
||||||
|
export class AppState {
|
||||||
|
constructor(public time: Date) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NavRoute {
|
||||||
|
title: string;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfirmationDialog {
|
||||||
|
show(message: string, onSuccess: Runnable): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const confirmationDialog: () => ConfirmationDialog = () => {
|
||||||
|
const overlay = useOverlay();
|
||||||
|
const modal = overlay.create(ConfirmationModal);
|
||||||
|
return {
|
||||||
|
async show(message: string, onSuccess: Runnable) {
|
||||||
|
const instance = modal.open({ message: message });
|
||||||
|
const result = await instance.result;
|
||||||
|
if (result) {
|
||||||
|
onSuccess();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
6
frontend/eslint.config.mjs
Normal file
6
frontend/eslint.config.mjs
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
// @ts-check
|
||||||
|
import withNuxt from './.nuxt/eslint.config.mjs'
|
||||||
|
|
||||||
|
export default withNuxt(
|
||||||
|
// Your custom configs here
|
||||||
|
)
|
||||||
27
frontend/nuxt.config.ts
Normal file
27
frontend/nuxt.config.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
compatibilityDate: "2025-07-15",
|
||||||
|
devtools: { enabled: false },
|
||||||
|
modules: ["@nuxt/eslint", "@nuxt/ui", "@tailwindcss/typography"],
|
||||||
|
|
||||||
|
ssr: false,
|
||||||
|
pages: true,
|
||||||
|
|
||||||
|
css: ["~/assets/css/main.css"],
|
||||||
|
|
||||||
|
ui: {
|
||||||
|
colorMode: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
devServer: {
|
||||||
|
port: 3000,
|
||||||
|
},
|
||||||
|
|
||||||
|
runtimeConfig: {
|
||||||
|
public: {
|
||||||
|
backend: {
|
||||||
|
baseUrl: "http://localhost:8081/v1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
14222
frontend/package-lock.json
generated
Normal file
14222
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
22
frontend/package.json
Normal file
22
frontend/package.json
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"name": "demo",
|
||||||
|
"type": "module",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "nuxt build",
|
||||||
|
"dev": "nuxt dev",
|
||||||
|
"generate": "nuxt generate",
|
||||||
|
"preview": "nuxt preview",
|
||||||
|
"postinstall": "nuxt prepare"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@nuxt/eslint": "^1.10.0",
|
||||||
|
"@nuxt/ui": "^4.2.0",
|
||||||
|
"eslint": "^9.39.1",
|
||||||
|
"nuxt": "^4.2.1",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"vue": "^3.5.24",
|
||||||
|
"vue-router": "^4.6.3",
|
||||||
|
"@tailwindcss/typography": "0.5.19"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
frontend/public/favicon.ico
Normal file
BIN
frontend/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
2
frontend/public/robots.txt
Normal file
2
frontend/public/robots.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
User-Agent: *
|
||||||
|
Disallow:
|
||||||
18
frontend/tsconfig.json
Normal file
18
frontend/tsconfig.json
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
// https://nuxt.com/docs/guide/concepts/typescript
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./.nuxt/tsconfig.app.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./.nuxt/tsconfig.server.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./.nuxt/tsconfig.shared.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./.nuxt/tsconfig.node.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue