[Closes #44] Merge branch '44-migrate-frontend' into develop

pull/53/head
Jef Roosens 2021-04-29 11:51:19 +02:00
commit 4b3ae8a9a4
Signed by: Jef Roosens
GPG Key ID: B580B976584B5F30
25 changed files with 541 additions and 14 deletions

View File

@ -1,20 +1,42 @@
*
# Source code
# =====BACKEND=====
## Source code
!src/
# Cargo files
## Cargo files
!Cargo.toml
!Cargo.lock
# Entrypoint for devop container
## Entrypoint for devop container
!docker/entrypoint_dev.sh
!docker/entrypoint.sh
# Config file
## Config file
!Rocket.toml
# Database migrations
## Database migrations
!migrations/
## Crontab for release container
!docker/crontab
# =====FRONTEND=====
## Source code
!web/src/
## Public assets
!web/public/
!web/index.html
## Production env file
!web/.env.production
## Package manager stuff
!web/package.json
!web/yarn.lock
## Project configs
!web/tsconfig.json
!web/vite.config.ts

View File

@ -1,9 +1,18 @@
pipeline:
test:
build-frontend:
image: node:15-alpine3.13
pull: true
commands:
- cd web && yarn install
- cd web && yarn run build
test-backend:
image: chewingbever/fej-builder:latest
# Always update the builder image
pull: true
commands:
- cargo test
# TODO build dev & rel image, deploy these images
branches: [ master, develop ]

View File

@ -4,6 +4,10 @@ version = "1.0.2"
authors = ["Jef Roosens <roosensjef@gmail.com>"]
edition = "2018"
[features]
# Enables hosting of the frontend
frontend = []
[lib]
name = "fej"
path = "src/fej/lib.rs"
@ -41,4 +45,4 @@ diesel_migrations = "1.4.0"
[dependencies.rocket_contrib]
version = "0.4.7"
default-features = false
features = ["json", "diesel_postgres_pool"]
features = ["json", "diesel_postgres_pool", "serve"]

View File

@ -1,5 +1,5 @@
# vim: filetype=dockerfile
FROM chewingbever/fej-builder:latest AS builder
FROM chewingbever/fej-builder:latest AS backend-builder
COPY --chown=builder:builder Cargo.toml Cargo.lock ./
COPY --chown=builder:builder src/ ./src/
@ -19,7 +19,18 @@ COPY --chown=builder:builder migrations/ ./migrations/
RUN cargo install \
--path . \
--root /app/output \
--target x86_64-unknown-linux-musl
--target x86_64-unknown-linux-musl \
--features frontend
FROM node:15-alpine3.13 AS frontend-builder
COPY ./web /app
WORKDIR /app
# Build the frontend
RUN yarn install && \
yarn run build
# Now, we create the actual image
@ -41,8 +52,9 @@ RUN apk update && \
# Switch to non-root user
USER fej:fej
# Copy binary over to final image
COPY --from=builder --chown=fej:fej /app/output/bin /app/bin
# Copy binary & frontend over to final image
COPY --from=backend-builder --chown=fej:fej /app/output/bin /app/bin
COPY --from=frontend-builder --chown=fej:fej /app/dist /app/dist
# Embed config file inside container
# The workdir is changed so that the config file is read properly

3
fejctl
View File

@ -1,6 +1,7 @@
#!/usr/bin/env bash
image='chewingbever/fej'
web_dir='web'
# Small wrapper around the docker-compose command
#
@ -98,10 +99,12 @@ function main() {
# Building
b | build ) dcr build --bin "$bin" && dc -- logs -f app ;;
br | build-release ) dc -br build ;;
bf | build-frontend ) cd "$web_dir" && yarn run build ;;
# Running
r | run ) dcr run --bin "$bin" && dc -- logs -f app ;;
rr | run-release ) dc -br -- up --build --detach && dc -r -- logs -f app ;;
rf | run-frontend ) dcr run --bin server && cd "$web_dir" && yarn run dev ;;
s | stop ) dc down ;;
sr | stop-release ) dc -r stop ;;

View File

@ -20,6 +20,8 @@ use rocket::fairing::{Fairing, Info, Kind};
use rocket::http::Header;
use rocket::{Request, Response, Rocket};
use rocket_contrib::databases::diesel;
#[cfg(feature = "frontend")]
use rocket_contrib::serve::StaticFiles;
pub struct CORS;
@ -60,12 +62,21 @@ fn run_db_migrations(rocket: Rocket) -> Result<Rocket, Rocket> {
}
fn rocket() -> rocket::Rocket {
rocket::ignite()
// This needs to be muted for the frontend feature
let mut rocket = rocket::ignite()
.attach(CORS)
.attach(FejDbConn::fairing())
.attach(AdHoc::on_attach("Database Migrations", run_db_migrations))
.mount("/ivago", routes::ivago())
.register(catchers![catchers::not_found])
.mount("/api/ivago", routes::ivago()) // /api being hardcoded is temporary
.register(catchers![catchers::not_found]);
// TODO make all of this not hard-coded
#[cfg(feature = "frontend")]
{
rocket = rocket.mount("/", StaticFiles::from("/app/dist"));
}
rocket
}
fn main() {

1
web/.env 100644
View File

@ -0,0 +1 @@
VITE_ENDPOINT=http://localhost:8000/api

View File

@ -0,0 +1 @@
VITE_ENDPOINT=https://fej.roosens.me/api

130
web/.gitignore vendored 100644
View File

@ -0,0 +1,130 @@
# Created by https://www.toptal.com/developers/gitignore/api/node
# Edit at https://www.toptal.com/developers/gitignore?templates=node
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
.env*.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Storybook build outputs
.out
.storybook-out
storybook-static
# rollup.js default build output
dist/
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# Temporary folders
tmp/
temp/
# End of https://www.toptal.com/developers/gitignore/api/node

27
web/README.md 100644
View File

@ -0,0 +1,27 @@
# Vue 3 + Typescript + Vite
This template should help get you started developing with Vue 3 and Typescript in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Vetur](https://marketplace.visualstudio.com/items?itemName=octref.vetur). Make sure to enable `vetur.experimental.templateInterpolationService` in settings!
### If Using `<script setup>`
[`<script setup>`](https://github.com/vuejs/rfcs/pull/227) is a feature that is currently in RFC stage. To get proper IDE support for the syntax, use [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar) instead of Vetur (and disable Vetur).
## Type Support For `.vue` Imports in TS
Since TypeScript cannot handle type information for `.vue` imports, they are shimmed to be a generic Vue component type by default. In most cases this is fine if you don't really care about component prop types outside of templates. However, if you wish to get actual prop types in `.vue` imports (for example to get props validation when using manual `h(...)` calls), you can use the following:
### If Using Volar
Run `Volar: Switch TS Plugin on/off` from VSCode command palette.
### If Using Vetur
1. Install and add `@vuedx/typescript-plugin-vue` to the [plugins section](https://www.typescriptlang.org/tsconfig#plugins) in `tsconfig.json`
2. Delete `src/shims-vue.d.ts` as it is no longer needed to provide module info to Typescript
3. Open `src/main.ts` in VSCode
4. Open the VSCode command palette
5. Search and run "Select TypeScript version" -> "Use workspace version"

13
web/index.html 100644
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

20
web/package.json 100644
View File

@ -0,0 +1,20 @@
{
"name": "fej-frontend",
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"serve": "vite preview"
},
"dependencies": {
"vue": "^3.0.5",
"vue-router": "^4.0.6"
},
"devDependencies": {
"@vitejs/plugin-vue": "^1.2.1",
"@vue/compiler-sfc": "^3.0.5",
"typescript": "^4.1.3",
"vite": "^2.2.1",
"vue-tsc": "^0.0.25"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

27
web/src/App.vue 100644
View File

@ -0,0 +1,27 @@
<template>
<Nav />
<router-view />
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import Nav from './components/Nav.vue'
export default defineComponent({
name: 'App',
components: {
Nav,
},
})
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>

View File

@ -0,0 +1,32 @@
export class Street {
name: string;
city: string;
constructor(name: string, city: string) {
this.name = name;
this.city = city;
}
}
export class Ivago {
base_url: string;
constructor(url: string) {
this.base_url = url;
}
async search(search_term: string): Promise<Street[]> {
var r = await fetch(`${this.base_url}/ivago/search?` + new URLSearchParams({
q: search_term,
}));
if (!r.ok) {
return Promise.reject();
}
var json = await r.json();
return json.map((o: {name: string, city: string}) => new Street(o.name, o.city));
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -0,0 +1,51 @@
<template>
<h1>Fej Frontend</h1>
<input id="street" type="text" placeholder="street" v-model="street" />
<button :onClick="onSubmit">Search</button>
<ul>
<li v-for="res in results">{{res.name}} ({{res.city}})</li>
</ul>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { Street, Ivago } from '../api/ivago'
export default defineComponent({
name: 'HelloWorld',
data() {
return {
street: "",
results: [] as Street[],
}
},
methods: {
onSubmit() {
new Ivago(import.meta.env.VITE_ENDPOINT as string).search(this.$data.street)
.then(res => {
this.$data.results = res;
});
}
}
})
</script>
<style scoped>
a {
color: #42b983;
}
label {
margin: 0 0.5em;
font-weight: bold;
}
code {
background-color: #eee;
padding: 2px 4px;
border-radius: 4px;
color: #304455;
}
</style>

View File

@ -0,0 +1,13 @@
<template>
<h1>Fej</h1>
<p>Welcome to Fej, my frontend/backend combo.</p>
<p>If you can see this, the cicd worked!</p>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'Home',
})
</script>

View File

@ -0,0 +1,52 @@
<template>
<h1>Ivago</h1>
<input v-model="query" v-on:keyup.enter="search" type="text" placeholder="Street..." />
<div id="scroll-list">
<ul v-if="msg === ''">
<li v-for="item in items">{{ item.name }}</li>
</ul>
<p v-else>{{ msg }}</p>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { Street, Ivago } from '../api/ivago'
export default defineComponent({
name: 'Ivago',
data() {
return {
items: [] as Street[],
msg: "",
query: "",
}
},
methods: {
search() {
this.items = []
this.msg = "Loading..."
if (this.query === "") {
this.msg = ""
return
}
new Ivago(import.meta.env.VITE_ENDPOINT as string)
.search(this.query)
.then((res: Street[]) => {
this.items = res
this.msg = ""
})
}
}
})
</script>
<style scoped>
#scroll-list {
height: 200px;
overflow: hidden;
overflow-y: auto;
}
</style>

View File

@ -0,0 +1,43 @@
<template>
<div id="menu-wrapper">
<nav id="menu">
<router-link to="/">Home</router-link>
<router-link to="/ivago">Ivago</router-link>
</nav>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'Nav',
})
</script>
<style scoped>
#menu-wrapper {
position: fixed;
left: 0;
top: 0;
height: 100%;
width: 150px;
background-color: #242624;
text-align: center;
}
#menu {
margin: 20px;
}
#menu > a {
display: block;
width: 100%;
color: #5f635f;
text-decoration: none;
}
#menu > a:hover {
color: #c2ccc1;
}
</style>

8
web/src/main.ts 100644
View File

@ -0,0 +1,8 @@
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
const app = createApp(App)
app.use(router)
app.mount('#app')

21
web/src/router.ts 100644
View File

@ -0,0 +1,21 @@
import { createWebHistory, createRouter } from "vue-router";
import Home from './components/Home.vue';
import Ivago from './components/Ivago.vue';
const routes = [
{
path: "/",
component: Home,
},
{
path: "/ivago",
component: Ivago,
},
];
const router = createRouter({
history: createWebHistory(),
routes,
});
export default router;

5
web/src/shims-vue.d.ts vendored 100644
View File

@ -0,0 +1,5 @@
declare module '*.vue' {
import { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}

15
web/tsconfig.json 100644
View File

@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"moduleResolution": "node",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"lib": ["esnext", "dom"],
"types": ["vite/client"]
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}

View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()]
})