Merge branch 'dev'

cleanup_stuff
Jon Eugster 1 year ago
commit 8c84d3fae7

Binary file not shown.

Before

Width:  |  Height:  |  Size: 398 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 308 KiB

@ -86,7 +86,7 @@ function InventoryList({items, docType, openDoc, defaultTab=null, level=undefine
{[...modifiedItems].sort(
// For lemas, sort entries `available > disabled > locked`
// otherwise alphabetically
(x, y) => +(docType == "Lemma") * (+x.locked - +y.locked || +x.disabled - +y.disabled)
(x, y) => +(docType == "Lemma") * (+x.locked - +y.locked || +x.disabled - +y.disabled) || x.displayName.localeCompare(y.displayName)
).filter(item => !item.hidden && ((tab ?? categories[0]) == item.category)).map((item, i) => {
return <InventoryItem key={`${item.category}-${item.name}`}
showDoc={() => {openDoc({name: item.name, type: docType})}}

@ -7,12 +7,12 @@ import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';
import '../css/landing_page.css'
import coverRobo from '../assets/covers/formaloversum.png'
import coverNNG from '../assets/covers/nng.png'
import bgImage from '../assets/bg.jpg'
import Markdown from './markdown';
import {PrivacyPolicyPopup} from './popup/privacy_policy'
import { GameTile, useGetGameInfoQuery } from '../state/api'
import path from 'path';
const flag = {
'Dutch': '🇳🇱',
@ -33,47 +33,42 @@ function GithubIcon({url='https://github.com'}) {
</div>
}
function GameTile({
title,
gameId,
intro, // Catchy intro phrase.
image=null,
worlds='?',
levels='?',
prereq='&ndash;', // Optional list of games that this game builds on. Use markdown.
description, // Longer description. Supports Markdown.
language}) {
function Tile({gameId, data}: {gameId: string, data: GameTile|undefined}) {
let navigate = useNavigate();
const routeChange = () =>{
navigate(gameId);
}
if (typeof data === 'undefined') {
return <></>
}
return <div className="game" onClick={routeChange}>
<div className="wrapper">
<div className="title">{title}</div>
<div className="short-description">{intro}
<div className="title">{data.title}</div>
<div className="short-description">{data.short}
</div>
{ image ? <img className="image" src={image} alt="" /> : <div className="image"/> }
<div className="long description"><Markdown>{description}</Markdown></div>
{ data.image ? <img className="image" src={path.join("data", gameId, data.image)} alt="" /> : <div className="image"/> }
<div className="long description"><Markdown>{data.long}</Markdown></div>
</div>
<table className="info">
<tbody>
<tr>
<td title="consider playing these games first.">Prerequisites</td>
<td><Markdown>{prereq}</Markdown></td>
<td><Markdown>{data.prerequisites.join(', ')}</Markdown></td>
</tr>
<tr>
<td>Worlds</td>
<td>{worlds}</td>
<td>{data.worlds}</td>
</tr>
<tr>
<td>Levels</td>
<td>{levels}</td>
<td>{data.levels}</td>
</tr>
<tr>
<td>Language</td>
<td title={`in ${language}`}>{flag[language]}</td>
<td title={`in ${data.languages.join(', ')}`}>{data.languages.map((lan) => flag[lan]).join(', ')}</td>
</tr>
</tbody>
</table>
@ -89,6 +84,56 @@ function LandingPage() {
const openImpressum = () => setImpressum(true);
const closeImpressum = () => setImpressum(false);
// const [allGames, setAllGames] = React.useState([])
// const [allTiles, setAllTiles] = React.useState([])
// const getTiles=()=>{
// fetch('featured_games.json', {
// headers : {
// 'Content-Type': 'application/json',
// 'Accept': 'application/json'
// }
// }
// ).then(function(response){
// return response.json()
// }).then(function(data) {
// setAllGames(data.featured_games)
// })
// }
// React.useEffect(()=>{
// getTiles()
// },[])
// React.useEffect(()=>{
// Promise.allSettled(
// allGames.map((gameId) => (
// fetch(`data/g/${gameId}/game.json`).catch(err => {return undefined})))
// ).then(responses =>
// responses.forEach((result) => console.log(result)))
// // Promise.all(responses.map(res => {
// // if (res.status == "fulfilled") {
// // console.log(res.value.json())
// // return res.value.json()
// // } else {
// // return undefined
// // }
// // }))
// // ).then(allData => {
// // setAllTiles(allData.map(data => data?.tile))
// // })
// },[allGames])
// TODO: I would like to read the supported games list form a JSON,
// Then load all these games in
//
let allGames = [
"leanprover-community/nng4",
"djvelleman/stg4",
"hhu-adam/robo"]
let allTiles = allGames.map((gameId) => (useGetGameInfoQuery({game: `g/${gameId}`}).data?.tile))
return <div className="landing-page">
<header style={{backgroundImage: `url(${bgImage})`}}>
@ -105,8 +150,19 @@ function LandingPage() {
</div>
</header>
<div className="game-list">
<GameTile
{allTiles.length == 0 ?
<p>No Games loaded. Use <a>http://localhost:3000/#/g/local/FOLDER</a> to open a
game directly from a local folder.
</p>
: allGames.map((id, i) => (
<Tile
key={id}
gameId={`g/${id}`}
data={allTiles[i]}
/>
))
}
{/* <GameTile
title="Natural Number Game"
gameId="g/hhu-adam/NNG4"
intro="The classical introduction game for Lean."
@ -129,18 +185,7 @@ This is a good first introduction to Lean!"
levels="30"
language="English"
/>
<GameTile
title="Formaloversum"
gameId="g/hhu-adam/Robo"
intro="Erkunde das Leansche Universum mit deinem Robo, welcher dir bei der Verständigung mit den Formalosophen zur Seite steht."
description="
Dieses Spiel führt die Grundlagen zur Beweisführung in Lean ein und schneidet danach verschiedene Bereiche des Bachelorstudiums an.
(Das Spiel befindet sich noch in der Entstehungsphase.)"
image={coverRobo}
language="German"
/>
*/}
</div>
<section>
<div className="wrapper">

@ -34,6 +34,7 @@ import { DualEditor } from './infoview/main'
import { GameHint } from './infoview/rpc_api'
import { DeletedHints, Hint, Hints } from './hints'
import { PrivacyPolicyPopup } from './popup/privacy_policy'
import path from 'path';
import '@fontsource/roboto/300.css'
import '@fontsource/roboto/400.css'
@ -467,6 +468,11 @@ function Introduction({impressum, setImpressum}) {
const gameInfo = useGetGameInfoQuery({game: gameId})
const {worldId} = useContext(WorldLevelIdContext)
let image: string = gameInfo.data?.worlds.nodes[worldId].image
const toggleImpressum = () => {
setImpressum(!impressum)
}
@ -480,7 +486,12 @@ function Introduction({impressum, setImpressum}) {
:
<Split minSize={0} snapOffset={200} sizes={[25, 50, 25]} className={`app-content level`}>
<IntroductionPanel gameInfo={gameInfo} />
<div className="world-image-container empty"></div>
<div className="world-image-container empty">
{image &&
<img src={path.join("data", gameId, image)} alt="" />
}
</div>
<InventoryPanel levelInfo={inventory?.data} />
</Split>
}

@ -336,6 +336,16 @@ td code {
background-color: #eee;
}
.world-image-container {
display: flex;
flex-direction: column;
justify-content: center;
}
.world-image-container img {
object-fit: contain;
}
.typewriter-interface .proof {
background-color: #fff;
}

@ -3,14 +3,28 @@
*/
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export interface GameTile {
title: string
short: string
long: string
languages: Array<string>
prerequisites: Array<string>
worlds: number
levels: number
image: string
}
export interface GameInfo {
title: null|string,
introduction: null|string,
info: null|string,
worlds: null|{nodes: {[id:string]: {id: string, title: string, introduction: string}}, edges: string[][]},
worlds: null|{nodes: {[id:string]: {id: string, title: string, introduction: string, image: string}}, edges: string[][]},
worldSize: null|{[key: string]: number},
authors: null|string[],
conclusion: null|string,
tile: null|GameTile,
image: null|string
}
export interface InventoryTile {
@ -36,7 +50,8 @@ export interface LevelInfo {
lemmaTab: null|string,
statementName: null|string,
displayName: null|string,
template: null|string
template: null|string,
image: null|string
}
/** Used to display the inventory on the welcome page */
@ -59,19 +74,19 @@ interface Doc {
// Define a service using a base URL and expected endpoints
export const apiSlice = createApi({
reducerPath: 'gameApi',
baseQuery: fetchBaseQuery({ baseUrl: window.location.origin + "/api" }),
baseQuery: fetchBaseQuery({ baseUrl: window.location.origin + "/data" }),
endpoints: (builder) => ({
getGameInfo: builder.query<GameInfo, {game: string}>({
query: ({game}) => `${game}/game`,
query: ({game}) => `${game}/game.json`,
}),
loadLevel: builder.query<LevelInfo, {game: string, world: string, level: number}>({
query: ({game, world, level}) => `${game}/level/${world}/${level}`,
query: ({game, world, level}) => `${game}/level__${world}__${level}.json`,
}),
loadInventoryOverview: builder.query<InventoryOverview, {game: string}>({
query: ({game}) => `${game}/inventory`,
query: ({game}) => `${game}/inventory.json`,
}),
loadDoc: builder.query<Doc, {game: string, name: string, type: "lemma"|"tactic"}>({
query: ({game, type, name}) => `${game}/doc/${type}/${name}`,
query: ({game, type, name}) => `${game}/doc__${type}__${name}.json`,
}),
}),
})

@ -46,7 +46,9 @@ elab "Title" t:str : command => do
match ← getCurLayer with
| .Level => modifyCurLevel fun level => pure {level with title := t.getString}
| .World => modifyCurWorld fun world => pure {world with title := t.getString}
| .Game => modifyCurGame fun game => pure {game with title := t.getString}
| .Game => modifyCurGame fun game => pure {game with
title := t.getString
tile := {game.tile with title := t.getString}}
/-- Define the introduction of the current game/world/level. -/
elab "Introduction" t:str : command => do
@ -58,10 +60,32 @@ elab "Introduction" t:str : command => do
/-- Define the info of the current game. Used for e.g. credits -/
elab "Info" t:str : command => do
match ← getCurLayer with
| .Level => pure ()
| .World => pure ()
| .Level =>
logError "Can't use `Info` in a level!"
pure ()
| .World =>
logError "Can't use `Info` in a world"
pure ()
| .Game => modifyCurGame fun game => pure {game with info := t.getString}
/-- Provide the location of the image for the current game/world/level.
Paths are relative to the lean project's root. -/
elab "Image" t:str : command => do
let file := t.getString
if not <| ← System.FilePath.pathExists file then
logWarningAt t s!"Make sure the cover image '{file}' exists."
if not <| file.startsWith "images/" then
logWarningAt t s!"The file name should start with `images/`. Make sure all images are in that folder."
match ← getCurLayer with
| .Level =>
logWarning "Level-images not implemented yet" -- TODO
modifyCurLevel fun level => pure {level with image := file}
| .World =>
modifyCurWorld fun world => pure {world with image := file}
| .Game =>
logWarning "Main image of the game not implemented yet" -- TODO
modifyCurGame fun game => pure {game with image := file}
/-- Define the conclusion of the current game or current level if some
building a level. -/
@ -71,6 +95,38 @@ elab "Conclusion" t:str : command => do
| .World => modifyCurWorld fun world => pure {world with conclusion := t.getString}
| .Game => modifyCurGame fun game => pure {game with conclusion := t.getString}
/-- A list of games that should be played before this one. Example `Prerequisites "NNG" "STG"`. -/
elab "Prerequisites" t:str* : command => do
modifyCurGame fun game => pure {game with
tile := {game.tile with prerequisites := t.map (·.getString) |>.toList}}
/-- Short caption for the game (1 sentence) -/
elab "CaptionShort" t:str : command => do
modifyCurGame fun game => pure {game with
tile := {game.tile with short := t.getString}}
/-- More detailed description what the game is about (2-4 sentences). -/
elab "CaptionLong" t:str : command => do
modifyCurGame fun game => pure {game with
tile := {game.tile with long := t.getString}}
/-- A list of Languages the game is translated to. For example `Languages "German" "English"`.
NOTE: For the time being, only a single language is supported.
-/
elab "Languages" t:str* : command => do
modifyCurGame fun game => pure {game with
tile := {game.tile with languages := t.map (·.getString) |>.toList}}
/-- The Image of the game (optional). TODO: Not impementeds -/
elab "CoverImage" t:str : command => do
let file := t.getString
if not <| ← System.FilePath.pathExists file then
logWarningAt t s!"Make sure the cover image '{file}' exists."
if not <| file.startsWith "images/" then
logWarningAt t s!"The file name should start with `images/`. Make sure all images are in that folder."
modifyCurGame fun game => pure {game with
tile := {game.tile with image := file}}
/-! # Inventory
@ -626,6 +682,27 @@ elab "Template" tacs:tacticSeq : tactic => do
modifyLevel (←getCurLevelId) fun level => do
return {level with template := s!"{template}"}
open IO.FS System FilePath in
/-- Copies the folder `images/` to `.lake/gamedata/images/` -/
def copyImages : IO Unit := do
let target : FilePath := ".lake" / "gamedata"
if ← FilePath.pathExists "images" then
for file in ← walkDir "images" do
let outFile := target.join file
-- create the directories
if ← file.isDir then
createDirAll outFile
else
if let some parent := outFile.parent then
createDirAll parent
-- copy file
let content ← readBinFile file
writeBinFile outFile content
-- TODO: Notes for testing if a declaration has the simp attribute
-- -- Test: From zulip
@ -647,7 +724,7 @@ elab "Template" tacs:tacticSeq : tactic => do
#eval IO.FS.createDirAll ".lake/gamedata/"
-- TODO: register all of this as ToJson instance?
def saveGameData (allItemsByType : HashMap InventoryType (HashSet Name)) : CommandElabM Unit:= do
def saveGameData (allItemsByType : HashMap InventoryType (HashSet Name)) : CommandElabM Unit := do
let game ← getCurGame
let env ← getEnv
let path : System.FilePath := s!"{← IO.currentDir}" / ".lake" / "gamedata"
@ -656,6 +733,9 @@ def saveGameData (allItemsByType : HashMap InventoryType (HashSet Name)) : Comma
IO.FS.removeDirAll path
IO.FS.createDirAll path
-- copy the images folder
copyImages
for (worldId, world) in game.worlds.nodes.toArray do
for (levelId, level) in world.levels.toArray do
IO.FS.writeFile (path / s!"level__{worldId}__{levelId}.json") (toString (toJson (level.toInfo env)))
@ -848,8 +928,12 @@ elab "MakeGame" : command => do
-- Items that should not be displayed in inventory
let mut hiddenItems : HashSet Name := {}
let allWorlds := game.worlds.nodes.toArray
let nrWorlds := allWorlds.size
let mut nrLevels := 0
-- Calculate which "items" are used/new in which world
for (worldId, world) in game.worlds.nodes.toArray do
for (worldId, world) in allWorlds do
let mut usedItems : HashSet Name := {}
let mut newItems : HashSet Name := {}
for inventoryType in #[.Tactic, .Definition, .Lemma] do
@ -888,9 +972,12 @@ elab "MakeGame" : command => do
-- logInfo m!"{worldId} uses: {usedItems.toList}"
-- logInfo m!"{worldId} introduces: {newItems.toList}"
-- Moreover, count the number of levels in the game
nrLevels := nrLevels + world.levels.toArray.size
/- for each "item" this is a HashSet of `worldId`s that introduce this item -/
let mut worldsWithNewItem : HashMap Name (HashSet Name) := {}
for (worldId, _world) in game.worlds.nodes.toArray do
for (worldId, _world) in allWorlds do
for newItem in newItemsInWorld.findD worldId {} do
worldsWithNewItem := worldsWithNewItem.insert newItem $
(worldsWithNewItem.findD newItem {}).insert worldId
@ -902,7 +989,7 @@ elab "MakeGame" : command => do
let mut dependencyReasons : HashMap (Name × Name) (HashSet Name) := {}
-- Calculate world dependency graph `game.worlds`
for (dependentWorldId, _dependentWorld) in game.worlds.nodes.toArray do
for (dependentWorldId, _dependentWorld) in allWorlds do
let mut dependsOnWorlds : HashSet Name := {}
-- Adding manual dependencies that were specified via the `Dependency` command.
for (sourceId, targetId) in game.worlds.edges do
@ -953,11 +1040,21 @@ elab "MakeGame" : command => do
logError m!"{w1} depends on {w2} because of {items.toList}."
else
worldDependsOnWorlds ← removeTransitive worldDependsOnWorlds
-- need to delete all existing edges as they are already present in `worldDependsOnWorlds`.
modifyCurGame fun game =>
pure {game with worlds := {game.worlds with edges := Array.empty}}
for (dependentWorldId, worldIds) in worldDependsOnWorlds.toArray do
modifyCurGame fun game =>
pure {game with worlds := {game.worlds with
edges := game.worlds.edges.append (worldIds.toArray.map fun wid => (wid, dependentWorldId))}}
-- Add the number of levels and worlds to the tile for the landing page
modifyCurGame fun game => pure {game with tile := {game.tile with
levels := nrLevels
worlds := nrWorlds }}
-- Apparently we need to reload `game` to get the changes to `game.worlds` we just made
let game ← getCurGame

@ -265,7 +265,9 @@ structure GameLevel where
lemmas: InventoryInfo := default
/-- A proof template that is printed in an empty editor. -/
template: Option String := none
deriving Inhabited, Repr
/-- The image for this level. -/
image : String := default
deriving Inhabited, Repr
/-- Json-encodable version of `GameLevel`
Fields:
@ -286,6 +288,7 @@ structure LevelInfo where
displayName : Option String
statementName : Option String
template : Option String
image: Option String
deriving ToJson, FromJson
def GameLevel.toInfo (lvl : GameLevel) (env : Environment) : LevelInfo :=
@ -315,6 +318,7 @@ def GameLevel.toInfo (lvl : GameLevel) (env : Environment) : LevelInfo :=
-- Note: we could call `.find!` because we check in `Statement` that the
-- lemma doc must exist.
template := lvl.template
image := lvl.image
}
/-! ## World -/
@ -331,17 +335,46 @@ structure World where
conclusion : String := default
/-- The levels of the world. -/
levels: HashMap Nat GameLevel := default
/-- The introduction image of the world. -/
image: String := default
deriving Inhabited
instance : ToJson World := ⟨
fun world => Json.mkObj [
("name", toJson world.name),
("title", world.title),
("introduction", world.introduction)]
("introduction", world.introduction),
("image", world.image)]
/-! ## Game -/
/-- A tile as they are displayed on the servers landing page. -/
structure GameTile where
/-- The title of the game -/
title: String
/-- One catch phrase about the game -/
short: String := default
/-- One paragraph description what the game is about -/
long: String := default
/-- List of languages the game supports
TODO: What's the expectected format
TODO: Must be a list with a single language currently
-/
languages: List String := default
/-- A list of games which this one builds upon -/
prerequisites: List String := default
/-- Number of worlds in the game -/
worlds: Nat := default
/-- Number of levels in the game -/
levels: Nat := default
/-- A cover image of the game
TODO: What's the format? -/
image: String := default
deriving Inhabited, ToJson
structure Game where
/-- Internal name of the game. -/
name : Name
@ -356,6 +389,10 @@ structure Game where
/-- TODO: currently unused. -/
authors : List String := default
worlds : Graph Name World := default
/-- The tile displayed on the server's landing page. -/
tile : GameTile := default
/-- The path to the background image of the world. -/
image : String := default
deriving Inhabited, ToJson
def getGameJson (game : «Game») : Json := Id.run do

@ -126,7 +126,7 @@ def findHints (goal : MVarId) (doc : FileWorker.EditableDocument) (initParams :
then
let userFVars := hintFVars.map fun v => bij.forward.findD v.fvarId! v.fvarId!
let text := (← evalHintMessage hint.text) (userFVars.map Expr.fvar)
let ctx := {env := ← getEnv, mctx := ← getMCtx, lctx := ← getLCtx, opts := {}}
let ctx := {env := ← getEnv, mctx := ← getMCtx, lctx := lctx, opts := {}}
let text ← (MessageData.withContext ctx text).toString
return some { text := text, hidden := hint.hidden }
else return none

@ -108,12 +108,11 @@ async function doImport (owner, repo, id) {
} catch (e) {
progress[id].output += `Error: ${e.toString()}\n${e.stack}`
} finally {
// if (artifactId) {
// // fs.rmSync(`tmp/artifact_${artifactId}.zip`, {force: true, recursive: true});
// // fs.rmSync(`tmp/artifact_${artifactId}`, {force: true, recursive: true});
// // fs.rmSync(`tmp/artifact_${artifactId}_inner`, {force: true, recursive: true});
// // fs.rmSync(`tmp/archive_${artifactId}.tar`, {force: true, recursive: true});
// }
// clean-up temp. files
if (artifactId) {
fs.rmSync(`${__dirname}/../games/tmp/${owner}_${repo}_${artifactId}.zip`, {force: true, recursive: false});
fs.rmSync(`${__dirname}/../games/tmp/${owner}_${repo}_${artifactId}`, {force: true, recursive: true});
}
progress[id].done = true
}
await new Promise(resolve => setTimeout(resolve, 10000))

@ -34,34 +34,15 @@ var router = express.Router();
router.get('/import/status/:owner/:repo', importStatus)
router.get('/import/trigger/:owner/:repo', importTrigger)
function loadJson(req, filename) {
const owner = req.params.owner;
const repo = req.params.repo
return JSON.parse(fs.readFileSync(path.join(getGameDir(owner,repo),".lake","gamedata",filename)))
}
router.get("/api/g/:owner/:repo/game", (req, res) => {
res.send(loadJson(req, `game.json`));
});
router.get("/api/g/:owner/:repo/inventory", (req, res) => {
res.send(loadJson(req, `inventory.json`));
});
router.get("/api/g/:owner/:repo/level/:world/:level", (req, res) => {
const world = req.params.world;
const level = req.params.level;
res.send(loadJson(req, `level__${world}__${level}.json`));
});
router.get("/api/g/:owner/:repo/doc/:type/:name", (req, res) => {
const type = req.params.type;
const name = req.params.name;
res.send(loadJson(req, `doc__${type}__${name}.json`));
});
const server = app
.use(express.static(path.join(__dirname, '../client/dist/')))
.use(express.static(path.join(__dirname, '../client/dist/'))) // TODO: add a dist folder from inside the game
.use('/data/g/:owner/:repo/*', (req, res, next) => {
const owner = req.params.owner;
const repo = req.params.repo
const filename = req.params[0];
req.url = filename;
express.static(path.join(getGameDir(owner,repo),".lake","gamedata"))(req, res, next);
})
.use('/', router)
.listen(PORT, () => console.log(`Listening on ${PORT}`));
@ -84,13 +65,13 @@ function getGameDir(owner, repo) {
if (owner == 'local') {
if(!isDevelopment) {
console.error(`No local games in production mode.`)
return
return ""
}
} else {
if(!fs.existsSync(path.join(__dirname, '..', 'games'))) {
console.error(`Did not find the following folder: ${path.join(__dirname, '..', 'games')}`)
console.error('Did you already import any games?')
return
return ""
}
}
@ -100,7 +81,7 @@ function getGameDir(owner, repo) {
if(!fs.existsSync(game_dir)) {
console.error(`Game '${game_dir}' does not exist!`)
return
return ""
}
return game_dir;
@ -114,7 +95,7 @@ function startServerProcess(owner, repo) {
let serverProcess
if (isDevelopment) {
let args = ["--server", game_dir]
serverProcess = cp.spawn("./gameserver", args,
serverProcess = cp.spawn("./gameserver", args, // TODO: find gameserver inside the games
{ cwd: path.join(__dirname, "./.lake/build/bin/") })
} else {
serverProcess = cp.spawn("./bubblewrap.sh",

@ -4,10 +4,10 @@
[{"url": "https://github.com/leanprover/std4.git",
"type": "git",
"subDir": null,
"rev": "a652e09bd81bcb43ea132d64ecc16580b0c7fa50",
"rev": "2e4a3586a8f16713f16b2d2b3af3d8e65f3af087",
"name": "std",
"manifestFile": "lake-manifest.json",
"inputRev": "v4.3.0-rc2",
"inputRev": "v4.3.0",
"inherited": false,
"configFile": "lakefile.lean"}],
"name": "GameServer",

@ -1 +1 @@
leanprover/lean4:v4.3.0-rc2
leanprover/lean4:v4.3.0

@ -41,7 +41,7 @@ export default defineConfig({
'/import': {
target: 'http://localhost:8080',
},
'/api': {
'/data': {
target: 'http://localhost:8080',
},
}

Loading…
Cancel
Save