Merge pull request #1693 from dagger/europa-docs-local-dev-to-ci-environment
Europa docs: From local dev to CI environment
This commit is contained in:
commit
b707e08d47
42
.github/workflows/todoapp.yml
vendored
Normal file
42
.github/workflows/todoapp.yml
vendored
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
name: todoapp
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- '.github/workflows/todoapp.yml'
|
||||||
|
- 'pkg/universe.dagger.io/examples/todoapp/**'
|
||||||
|
|
||||||
|
env:
|
||||||
|
# This needs to be unique across all of Netlify
|
||||||
|
APP_NAME: todoapp-dagger-europa
|
||||||
|
NETLIFY_TEAM: blocklayer
|
||||||
|
# https://app.netlify.com/user/applications/personal
|
||||||
|
NETLIFY_TOKEN: ${{ secrets.NETLIFY_TOKEN }}
|
||||||
|
DAGGER_LOG_FORMAT: plain
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
dagger:
|
||||||
|
name: "Deploy todoapp to Netlify"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: "Clone repository"
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
# TODO: maybe use Dagger action post 0.2.0-beta.1
|
||||||
|
- name: "Setup Go"
|
||||||
|
uses: actions/setup-go@v1
|
||||||
|
with:
|
||||||
|
go-version: 1.16
|
||||||
|
|
||||||
|
- name: "Install dev Dagger"
|
||||||
|
run: |
|
||||||
|
make dagger
|
||||||
|
|
||||||
|
- name: "Dagger"
|
||||||
|
run: |
|
||||||
|
cd pkg/universe.dagger.io/examples/todoapp
|
||||||
|
${{ github.workspace }}/cmd/dagger/dagger do deploy
|
@ -5,6 +5,19 @@ displayed_sidebar: europa
|
|||||||
|
|
||||||
# From local dev to CI environment
|
# From local dev to CI environment
|
||||||
|
|
||||||
|
Dagger can be used with any CI environment (no migration required) and has two important advantages which make the overall experience less error-prone and more efficient:
|
||||||
|
|
||||||
|
1. Instead of YAML you write CUE: typed configuration with built-in formatting
|
||||||
|
2. Configuration is executed in buildkit, the execution engine at the heart of Docker
|
||||||
|
|
||||||
|
This makes any CI environment with Docker pre-installed work with Dagger out of the box.
|
||||||
|
We started with [CI environments that you told us you are using](https://github.com/dagger/dagger/discussions/1677).
|
||||||
|
We will configure a production deployment for the same application that we covered in the previous page.
|
||||||
|
|
||||||
|
:::note
|
||||||
|
If you cannot find your CI environment below, [let us know via this GitHub discussion](https://github.com/dagger/dagger/discussions/1677).
|
||||||
|
:::
|
||||||
|
|
||||||
import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem';
|
import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem';
|
||||||
|
|
||||||
<Tabs defaultValue="github-actions"
|
<Tabs defaultValue="github-actions"
|
||||||
@ -13,23 +26,70 @@ values={[
|
|||||||
{label: 'GitHub Actions', value: 'github-actions'},
|
{label: 'GitHub Actions', value: 'github-actions'},
|
||||||
{label: 'CircleCI', value: 'circleci'},
|
{label: 'CircleCI', value: 'circleci'},
|
||||||
{label: 'GitLab', value: 'gitlab'},
|
{label: 'GitLab', value: 'gitlab'},
|
||||||
|
{label: 'Jenkins', value: 'jenkins'},
|
||||||
|
{label: 'Tekton', value: 'tekton'},
|
||||||
]}>
|
]}>
|
||||||
|
|
||||||
<TabItem value="github-actions">
|
<TabItem value="github-actions">
|
||||||
|
|
||||||
Since Dagger early access required a GitHub account, GitHub Actions seems like a reasonable starting point.
|
`.github/workflows/todoapp.yml`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: todoapp
|
||||||
|
|
||||||
|
push:
|
||||||
|
# Trigger this workflow only on commits pushed to the main branch
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
# Dagger plan gets configured via client environment variables
|
||||||
|
env:
|
||||||
|
# This needs to be unique across all of netlify.app
|
||||||
|
APP_NAME: todoapp-dagger-europa
|
||||||
|
NETLIFY_TEAM: dagger
|
||||||
|
# https://app.netlify.com/user/applications/personal
|
||||||
|
NETLIFY_TOKEN: ${{ secrets.NETLIFY_TOKEN }}
|
||||||
|
DAGGER_LOG_FORMAT: plain
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
dagger:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Clone repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Deploy to Netlify
|
||||||
|
# https://github.com/dagger/dagger-for-github
|
||||||
|
uses: dagger/dagger-for-github@v0.2
|
||||||
|
with:
|
||||||
|
workdir: pkg/universe.dagger.io/examples/todoapp
|
||||||
|
plan: .
|
||||||
|
do: deploy
|
||||||
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
||||||
<TabItem value="circleci">
|
<TabItem value="circleci">
|
||||||
|
|
||||||
If you would like us to document this CI environment next, mention it here: [dagger#1677](https://github.com/dagger/dagger/discussions/1677)
|
If you would like us to document CircleCI next, vote for it here: [dagger#1677](https://github.com/dagger/dagger/discussions/1677)
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
||||||
<TabItem value="gitlab">
|
<TabItem value="gitlab">
|
||||||
|
|
||||||
If you would like us to document this CI environment next, mention it here: [dagger#1677](https://github.com/dagger/dagger/discussions/1677)
|
If you would like us to document GitLab next, vote for it here: [dagger#1677](https://github.com/dagger/dagger/discussions/1677)
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem value="jenkins">
|
||||||
|
|
||||||
|
If you would like us to document Jenkins next, vote for it here: [dagger#1677](https://github.com/dagger/dagger/discussions/1677)
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem value="tekton">
|
||||||
|
|
||||||
|
If you would like us to document Tekton next, vote for it here: [dagger#1677](https://github.com/dagger/dagger/discussions/1677)
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
||||||
|
1
pkg/universe.dagger.io/examples/todoapp/.gitignore
vendored
Normal file
1
pkg/universe.dagger.io/examples/todoapp/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
build
|
3
pkg/universe.dagger.io/examples/todoapp/README.md
Normal file
3
pkg/universe.dagger.io/examples/todoapp/README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Todo APP
|
||||||
|
|
||||||
|
[Dagger documentation website](https://docs.dagger.io/)
|
@ -1,28 +0,0 @@
|
|||||||
// Deployment plan for Dagger's example todoapp
|
|
||||||
package todoapp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"dagger.io/dagger"
|
|
||||||
|
|
||||||
"universe.dagger.io/git"
|
|
||||||
"universe.dagger.io/yarn"
|
|
||||||
)
|
|
||||||
|
|
||||||
dagger.#Plan & {
|
|
||||||
// Build the app with yarn
|
|
||||||
actions: build: yarn.#Build
|
|
||||||
|
|
||||||
// Wire up source code to build
|
|
||||||
{
|
|
||||||
input: directories: source: _
|
|
||||||
actions: build: source: input.directories.source.contents
|
|
||||||
} | {
|
|
||||||
actions: {
|
|
||||||
pull: git.#Pull & {
|
|
||||||
remote: "https://github.com/mdn/todo-react"
|
|
||||||
ref: "master"
|
|
||||||
}
|
|
||||||
build: source: pull.output
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,40 +0,0 @@
|
|||||||
// Local dev environment for todoapp
|
|
||||||
package todoapp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"universe.dagger.io/docker"
|
|
||||||
"universe.dagger.io/nginx"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Expose todoapp web port
|
|
||||||
proxy: web: _
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
// Reference app build inherited from base config
|
|
||||||
build: _
|
|
||||||
_app: build.output
|
|
||||||
|
|
||||||
container: {
|
|
||||||
// Build a container image serving the app with nginx
|
|
||||||
build: docker.#Build & {
|
|
||||||
steps: [
|
|
||||||
nginx.#Build & {
|
|
||||||
flavor: "alpine"
|
|
||||||
},
|
|
||||||
docker.#Copy & {
|
|
||||||
contents: _app
|
|
||||||
dest: "/usr/share/nginx/html"
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the app in an ephemeral container
|
|
||||||
run: docker.#Run & {
|
|
||||||
image: build.output
|
|
||||||
ports: web: {
|
|
||||||
frontend: proxy.web.endpoint
|
|
||||||
backend: address: "localhost:5000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
94
pkg/universe.dagger.io/examples/todoapp/netlify.cue
Normal file
94
pkg/universe.dagger.io/examples/todoapp/netlify.cue
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
package netlify
|
||||||
|
|
||||||
|
import (
|
||||||
|
"dagger.io/dagger"
|
||||||
|
"universe.dagger.io/alpine"
|
||||||
|
"universe.dagger.io/bash"
|
||||||
|
"universe.dagger.io/docker"
|
||||||
|
"universe.dagger.io/netlify"
|
||||||
|
)
|
||||||
|
|
||||||
|
dagger.#Plan & {
|
||||||
|
client: {
|
||||||
|
filesystem: {
|
||||||
|
".": read: {
|
||||||
|
contents: dagger.#FS
|
||||||
|
exclude: [
|
||||||
|
"README.md",
|
||||||
|
"build",
|
||||||
|
"netlify.cue",
|
||||||
|
"node_modules",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
build: write: contents: actions.build.contents.output
|
||||||
|
}
|
||||||
|
env: {
|
||||||
|
APP_NAME: string
|
||||||
|
NETLIFY_TEAM: string
|
||||||
|
NETLIFY_TOKEN: dagger.#Secret
|
||||||
|
}
|
||||||
|
}
|
||||||
|
actions: {
|
||||||
|
deps: docker.#Build & {
|
||||||
|
steps: [
|
||||||
|
alpine.#Build & {
|
||||||
|
packages: {
|
||||||
|
bash: {}
|
||||||
|
yarn: {}
|
||||||
|
git: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
docker.#Copy & {
|
||||||
|
contents: client.filesystem.".".read.contents
|
||||||
|
dest: "/src"
|
||||||
|
},
|
||||||
|
// bash.#Run is a superset of docker.#Run
|
||||||
|
// install yarn dependencies
|
||||||
|
bash.#Run & {
|
||||||
|
workdir: "/src"
|
||||||
|
mounts: "/cache/yarn": dagger.#Mount & {
|
||||||
|
dest: "/cache/yarn"
|
||||||
|
type: "cache"
|
||||||
|
contents: dagger.#CacheDir & {
|
||||||
|
id: "todoapp-yarn-cache"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
script: contents: #"""
|
||||||
|
yarn config set cache-folder /cache/yarn
|
||||||
|
yarn install
|
||||||
|
"""#
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
test: bash.#Run & {
|
||||||
|
input: deps.output
|
||||||
|
workdir: "/src"
|
||||||
|
script: contents: #"""
|
||||||
|
yarn run test
|
||||||
|
"""#
|
||||||
|
}
|
||||||
|
|
||||||
|
build: {
|
||||||
|
run: bash.#Run & {
|
||||||
|
input: test.output
|
||||||
|
workdir: "/src"
|
||||||
|
script: contents: #"""
|
||||||
|
yarn run build
|
||||||
|
"""#
|
||||||
|
}
|
||||||
|
|
||||||
|
contents: dagger.#Subdir & {
|
||||||
|
input: run.output.rootfs
|
||||||
|
path: "/src/build"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deploy: netlify.#Deploy & {
|
||||||
|
contents: build.contents.output
|
||||||
|
site: client.env.APP_NAME
|
||||||
|
token: client.env.NETLIFY_TOKEN
|
||||||
|
team: client.env.NETLIFY_TEAM
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
38
pkg/universe.dagger.io/examples/todoapp/package.json
Normal file
38
pkg/universe.dagger.io/examples/todoapp/package.json
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"name": "moz-todo-react",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"homepage": "./",
|
||||||
|
"dependencies": {
|
||||||
|
"@testing-library/jest-dom": "^4.2.4",
|
||||||
|
"@testing-library/react": "^9.3.2",
|
||||||
|
"@testing-library/user-event": "^7.1.2",
|
||||||
|
"gh-pages": "^3.2.3",
|
||||||
|
"nanoid": "^3.1.3",
|
||||||
|
"react": "^16.13.1",
|
||||||
|
"react-dom": "^16.13.1",
|
||||||
|
"react-scripts": "3.4.1"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "react-scripts start",
|
||||||
|
"build": "react-scripts build",
|
||||||
|
"test": "react-scripts test --watchAll=false --passWithNoTests",
|
||||||
|
"gh-pages": "gh-pages -d build -u 'github-actions-bot <support+actions@github.com>'",
|
||||||
|
"eject": "react-scripts eject"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": "react-app"
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
BIN
pkg/universe.dagger.io/examples/todoapp/public/favicon.ico
Normal file
BIN
pkg/universe.dagger.io/examples/todoapp/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.1 KiB |
43
pkg/universe.dagger.io/examples/todoapp/public/index.html
Normal file
43
pkg/universe.dagger.io/examples/todoapp/public/index.html
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Web site created using create-react-app"
|
||||||
|
/>
|
||||||
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||||
|
<!--
|
||||||
|
manifest.json provides metadata used when your web app is installed on a
|
||||||
|
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||||
|
-->
|
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
|
<!--
|
||||||
|
Notice the use of %PUBLIC_URL% in the tags above.
|
||||||
|
It will be replaced with the URL of the `public` folder during the build.
|
||||||
|
Only files inside the `public` folder can be referenced from the HTML.
|
||||||
|
|
||||||
|
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||||
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
|
-->
|
||||||
|
<title>My Todo app</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
<!--
|
||||||
|
This HTML file is a template.
|
||||||
|
If you open it directly in the browser, you will see an empty page.
|
||||||
|
|
||||||
|
You can add webfonts, meta tags, or analytics to this file.
|
||||||
|
The build step will place the bundled scripts into the <body> tag.
|
||||||
|
|
||||||
|
To begin the development, run `npm start` or `yarn start`.
|
||||||
|
To create a production bundle, use `npm run build` or `yarn build`.
|
||||||
|
-->
|
||||||
|
</body>
|
||||||
|
</html>
|
BIN
pkg/universe.dagger.io/examples/todoapp/public/logo192.png
Normal file
BIN
pkg/universe.dagger.io/examples/todoapp/public/logo192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.2 KiB |
BIN
pkg/universe.dagger.io/examples/todoapp/public/logo512.png
Normal file
BIN
pkg/universe.dagger.io/examples/todoapp/public/logo512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.4 KiB |
25
pkg/universe.dagger.io/examples/todoapp/public/manifest.json
Normal file
25
pkg/universe.dagger.io/examples/todoapp/public/manifest.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"short_name": "React App",
|
||||||
|
"name": "Create React App Sample",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "favicon.ico",
|
||||||
|
"sizes": "64x64 32x32 24x24 16x16",
|
||||||
|
"type": "image/x-icon"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo192.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "192x192"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo512.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "512x512"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"theme_color": "#000000",
|
||||||
|
"background_color": "#ffffff"
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
120
pkg/universe.dagger.io/examples/todoapp/src/App.js
Normal file
120
pkg/universe.dagger.io/examples/todoapp/src/App.js
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import React, { useState, useRef, useEffect } from "react";
|
||||||
|
import Form from "./components/Form";
|
||||||
|
import FilterButton from "./components/FilterButton";
|
||||||
|
import Todo from "./components/Todo";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
|
|
||||||
|
function usePrevious(value) {
|
||||||
|
const ref = useRef();
|
||||||
|
useEffect(() => {
|
||||||
|
ref.current = value;
|
||||||
|
});
|
||||||
|
return ref.current;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FILTER_MAP = {
|
||||||
|
All: () => true,
|
||||||
|
Active: task => !task.completed,
|
||||||
|
Completed: task => task.completed
|
||||||
|
};
|
||||||
|
|
||||||
|
const FILTER_NAMES = Object.keys(FILTER_MAP);
|
||||||
|
|
||||||
|
function App(props) {
|
||||||
|
const [tasks, setTasks] = useState(props.tasks);
|
||||||
|
const [filter, setFilter] = useState('All');
|
||||||
|
|
||||||
|
function toggleTaskCompleted(id) {
|
||||||
|
const updatedTasks = tasks.map(task => {
|
||||||
|
// if this task has the same ID as the edited task
|
||||||
|
if (id === task.id) {
|
||||||
|
// use object spread to make a new obkect
|
||||||
|
// whose `completed` prop has been inverted
|
||||||
|
return {...task, completed: !task.completed}
|
||||||
|
}
|
||||||
|
return task;
|
||||||
|
});
|
||||||
|
setTasks(updatedTasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function deleteTask(id) {
|
||||||
|
const remainingTasks = tasks.filter(task => id !== task.id);
|
||||||
|
setTasks(remainingTasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function editTask(id, newName) {
|
||||||
|
const editedTaskList = tasks.map(task => {
|
||||||
|
// if this task has the same ID as the edited task
|
||||||
|
if (id === task.id) {
|
||||||
|
//
|
||||||
|
return {...task, name: newName}
|
||||||
|
}
|
||||||
|
return task;
|
||||||
|
});
|
||||||
|
setTasks(editedTaskList);
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskList = tasks
|
||||||
|
.filter(FILTER_MAP[filter])
|
||||||
|
.map(task => (
|
||||||
|
<Todo
|
||||||
|
id={task.id}
|
||||||
|
name={task.name}
|
||||||
|
completed={task.completed}
|
||||||
|
key={task.id}
|
||||||
|
toggleTaskCompleted={toggleTaskCompleted}
|
||||||
|
deleteTask={deleteTask}
|
||||||
|
editTask={editTask}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
const filterList = FILTER_NAMES.map(name => (
|
||||||
|
<FilterButton
|
||||||
|
key={name}
|
||||||
|
name={name}
|
||||||
|
isPressed={name === filter}
|
||||||
|
setFilter={setFilter}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
function addTask(name) {
|
||||||
|
const newTask = { id: "todo-" + nanoid(), name: name, completed: false };
|
||||||
|
setTasks([...tasks, newTask]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const tasksNoun = taskList.length !== 1 ? 'tasks' : 'task';
|
||||||
|
const headingText = `${taskList.length} ${tasksNoun} remaining`;
|
||||||
|
|
||||||
|
const listHeadingRef = useRef(null);
|
||||||
|
const prevTaskLength = usePrevious(tasks.length);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (tasks.length - prevTaskLength === -1) {
|
||||||
|
listHeadingRef.current.focus();
|
||||||
|
}
|
||||||
|
}, [tasks.length, prevTaskLength]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="todoapp stack-large">
|
||||||
|
<Form addTask={addTask} />
|
||||||
|
<div className="filters btn-group stack-exception">
|
||||||
|
{filterList}
|
||||||
|
</div>
|
||||||
|
<h2 id="list-heading" tabIndex="-1" ref={listHeadingRef}>
|
||||||
|
{headingText}
|
||||||
|
</h2>
|
||||||
|
<ul
|
||||||
|
className="todo-list stack-large stack-exception"
|
||||||
|
aria-labelledby="list-heading"
|
||||||
|
>
|
||||||
|
{taskList}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
@ -0,0 +1,18 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
function FilterButton(props) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn toggle-btn"
|
||||||
|
aria-pressed={props.isPressed}
|
||||||
|
onClick={() => props.setFilter(props.name)}
|
||||||
|
>
|
||||||
|
<span className="visually-hidden">Show </span>
|
||||||
|
<span>{props.name}</span>
|
||||||
|
<span className="visually-hidden"> tasks</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FilterButton;
|
@ -0,0 +1,45 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
function Form(props) {
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
|
||||||
|
|
||||||
|
function handleSubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!name.trim()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
props.addTask(name);
|
||||||
|
setName("");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function handleChange(e) {
|
||||||
|
setName(e.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<h2 className="label-wrapper">
|
||||||
|
<label htmlFor="new-todo-input" className="label__lg">
|
||||||
|
What needs to be done?
|
||||||
|
</label>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="new-todo-input"
|
||||||
|
className="input input__lg"
|
||||||
|
name="text"
|
||||||
|
autoComplete="off"
|
||||||
|
value={name}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<button type="submit" className="btn btn__primary btn__lg">
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Form;
|
113
pkg/universe.dagger.io/examples/todoapp/src/components/Todo.js
Normal file
113
pkg/universe.dagger.io/examples/todoapp/src/components/Todo.js
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
|
||||||
|
function usePrevious(value) {
|
||||||
|
const ref = useRef();
|
||||||
|
useEffect(() => {
|
||||||
|
ref.current = value;
|
||||||
|
});
|
||||||
|
return ref.current;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Todo(props) {
|
||||||
|
const [isEditing, setEditing] = useState(false);
|
||||||
|
const [newName, setNewName] = useState('');
|
||||||
|
|
||||||
|
const editFieldRef = useRef(null);
|
||||||
|
const editButtonRef = useRef(null);
|
||||||
|
|
||||||
|
const wasEditing = usePrevious(isEditing);
|
||||||
|
|
||||||
|
function handleChange(e) {
|
||||||
|
setNewName(e.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!newName.trim()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
props.editTask(props.id, newName);
|
||||||
|
setNewName("");
|
||||||
|
setEditing(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const editingTemplate = (
|
||||||
|
<form className="stack-small" onSubmit={handleSubmit}>
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="todo-label" htmlFor={props.id}>
|
||||||
|
New name for {props.name}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id={props.id}
|
||||||
|
className="todo-text"
|
||||||
|
type="text"
|
||||||
|
value={newName || props.name}
|
||||||
|
onChange={handleChange}
|
||||||
|
ref={editFieldRef}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="btn-group">
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn todo-cancel"
|
||||||
|
onClick={() => setEditing(false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
<span className="visually-hidden">renaming {props.name}</span>
|
||||||
|
</button>
|
||||||
|
<button type="submit" className="btn btn__primary todo-edit">
|
||||||
|
Save
|
||||||
|
<span className="visually-hidden">new name for {props.name}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
|
||||||
|
const viewTemplate = (
|
||||||
|
<div className="stack-small">
|
||||||
|
<div className="c-cb">
|
||||||
|
<input
|
||||||
|
id={props.id}
|
||||||
|
type="checkbox"
|
||||||
|
defaultChecked={props.completed}
|
||||||
|
onChange={() => props.toggleTaskCompleted(props.id)}
|
||||||
|
/>
|
||||||
|
<label className="todo-label" htmlFor={props.id}>
|
||||||
|
{props.name}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="btn-group">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn"
|
||||||
|
onClick={() => setEditing(true)}
|
||||||
|
ref={editButtonRef}
|
||||||
|
>
|
||||||
|
Edit <span className="visually-hidden">{props.name}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn__danger"
|
||||||
|
onClick={() => props.deleteTask(props.id)}
|
||||||
|
>
|
||||||
|
Delete <span className="visually-hidden">{props.name}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!wasEditing && isEditing) {
|
||||||
|
editFieldRef.current.focus();
|
||||||
|
}
|
||||||
|
if (wasEditing && !isEditing) {
|
||||||
|
editButtonRef.current.focus();
|
||||||
|
}
|
||||||
|
}, [wasEditing, isEditing]);
|
||||||
|
|
||||||
|
|
||||||
|
return <li className="todo">{isEditing ? editingTemplate : viewTemplate}</li>;
|
||||||
|
}
|
293
pkg/universe.dagger.io/examples/todoapp/src/index.css
Normal file
293
pkg/universe.dagger.io/examples/todoapp/src/index.css
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
|
||||||
|
/* RESETS */
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
*:focus {
|
||||||
|
outline: 3px dashed #228bec;
|
||||||
|
outline-offset: 0;
|
||||||
|
}
|
||||||
|
html {
|
||||||
|
font: 62.5% / 1.15 sans-serif;
|
||||||
|
}
|
||||||
|
h1,
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
border: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: auto;
|
||||||
|
overflow: visible;
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
line-height: normal;
|
||||||
|
-webkit-font-smoothing: inherit;
|
||||||
|
-moz-osx-font-smoothing: inherit;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
button::-moz-focus-inner {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
optgroup,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 100%;
|
||||||
|
line-height: 1.15;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
button,
|
||||||
|
input {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
input[type="text"] {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 68rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
font: 1.6rem/1.25 Arial, sans-serif;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
color: #4d4d4d;
|
||||||
|
}
|
||||||
|
@media screen and (min-width: 620px) {
|
||||||
|
body {
|
||||||
|
font-size: 1.9rem;
|
||||||
|
line-height: 1.31579;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*END RESETS*/
|
||||||
|
/* GLOBAL STYLES */
|
||||||
|
.form-group > input[type="text"] {
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 0.4rem;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
padding: 0.8rem 1rem 0.7rem;
|
||||||
|
border: 0.2rem solid #4d4d4d;
|
||||||
|
cursor: pointer;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
.btn.toggle-btn {
|
||||||
|
border-width: 1px;
|
||||||
|
border-color: #d3d3d3;
|
||||||
|
}
|
||||||
|
.btn.toggle-btn[aria-pressed="true"] {
|
||||||
|
text-decoration: underline;
|
||||||
|
border-color: #4d4d4d;
|
||||||
|
}
|
||||||
|
.btn__danger {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #ca3c3c;
|
||||||
|
border-color: #bd2130;
|
||||||
|
}
|
||||||
|
.btn__filter {
|
||||||
|
border-color: lightgrey;
|
||||||
|
}
|
||||||
|
.btn__primary {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #000;
|
||||||
|
}
|
||||||
|
.btn-group {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.btn-group > * {
|
||||||
|
flex: 1 1 49%;
|
||||||
|
}
|
||||||
|
.btn-group > * + * {
|
||||||
|
margin-left: 0.8rem;
|
||||||
|
}
|
||||||
|
.label-wrapper {
|
||||||
|
margin: 0;
|
||||||
|
flex: 0 0 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.visually-hidden {
|
||||||
|
position: absolute !important;
|
||||||
|
height: 1px;
|
||||||
|
width: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(1px 1px 1px 1px);
|
||||||
|
clip: rect(1px, 1px, 1px, 1px);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
[class*="stack"] > * {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.stack-small > * + * {
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
}
|
||||||
|
.stack-large > * + * {
|
||||||
|
margin-top: 2.5rem;
|
||||||
|
}
|
||||||
|
@media screen and (min-width: 550px) {
|
||||||
|
.stack-small > * + * {
|
||||||
|
margin-top: 1.4rem;
|
||||||
|
}
|
||||||
|
.stack-large > * + * {
|
||||||
|
margin-top: 2.8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.stack-exception {
|
||||||
|
margin-top: 1.2rem;
|
||||||
|
}
|
||||||
|
/* END GLOBAL STYLES */
|
||||||
|
.todoapp {
|
||||||
|
background: #fff;
|
||||||
|
margin: 2rem 0 4rem 0;
|
||||||
|
padding: 1rem;
|
||||||
|
position: relative;
|
||||||
|
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 2.5rem 5rem 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
@media screen and (min-width: 550px) {
|
||||||
|
.todoapp {
|
||||||
|
padding: 4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.todoapp > * {
|
||||||
|
max-width: 50rem;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
.todoapp > form {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
.todoapp > h1 {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.label__lg {
|
||||||
|
line-height: 1.01567;
|
||||||
|
font-weight: 300;
|
||||||
|
padding: 0.8rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.input__lg {
|
||||||
|
padding: 2rem;
|
||||||
|
border: 2px solid #000;
|
||||||
|
}
|
||||||
|
.input__lg:focus {
|
||||||
|
border-color: #4d4d4d;
|
||||||
|
box-shadow: inset 0 0 0 2px;
|
||||||
|
}
|
||||||
|
[class*="__lg"] {
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 1.9rem;
|
||||||
|
}
|
||||||
|
[class*="__lg"]:not(:last-child) {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
@media screen and (min-width: 620px) {
|
||||||
|
[class*="__lg"] {
|
||||||
|
font-size: 2.4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.filters {
|
||||||
|
width: 100%;
|
||||||
|
margin: unset auto;
|
||||||
|
}
|
||||||
|
/* Todo item styles */
|
||||||
|
.todo {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.todo > * {
|
||||||
|
flex: 0 0 100%;
|
||||||
|
}
|
||||||
|
.todo-text {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 4.4rem;
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
border: 2px solid #565656;
|
||||||
|
}
|
||||||
|
.todo-text:focus {
|
||||||
|
box-shadow: inset 0 0 0 2px;
|
||||||
|
}
|
||||||
|
/* CHECKBOX STYLES */
|
||||||
|
.c-cb {
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
line-height: 1.25;
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
min-height: 44px;
|
||||||
|
padding-left: 40px;
|
||||||
|
clear: left;
|
||||||
|
}
|
||||||
|
.c-cb > label::before,
|
||||||
|
.c-cb > input[type="checkbox"] {
|
||||||
|
box-sizing: border-box;
|
||||||
|
top: -2px;
|
||||||
|
left: -2px;
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
.c-cb > input[type="checkbox"] {
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
margin: 0;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.c-cb > label {
|
||||||
|
font-size: inherit;
|
||||||
|
font-family: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding: 8px 15px 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
|
.c-cb > label::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
border: 2px solid currentColor;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.c-cb > input[type="checkbox"]:focus + label::before {
|
||||||
|
border-width: 4px;
|
||||||
|
outline: 3px dashed #228bec;
|
||||||
|
}
|
||||||
|
.c-cb > label::after {
|
||||||
|
box-sizing: content-box;
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 11px;
|
||||||
|
left: 9px;
|
||||||
|
width: 18px;
|
||||||
|
height: 7px;
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
border: solid;
|
||||||
|
border-width: 0 0 5px 5px;
|
||||||
|
border-top-color: transparent;
|
||||||
|
opacity: 0;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.c-cb > input[type="checkbox"]:checked + label::after {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
18
pkg/universe.dagger.io/examples/todoapp/src/index.js
Normal file
18
pkg/universe.dagger.io/examples/todoapp/src/index.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import './index.css';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
|
||||||
|
const DATA = [
|
||||||
|
{ id: "todo-0", name: "Eat", completed: true },
|
||||||
|
{ id: "todo-1", name: "Sleep", completed: false },
|
||||||
|
{ id: "todo-2", name: "Repeat", completed: false }
|
||||||
|
];
|
||||||
|
|
||||||
|
ReactDOM.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App tasks={DATA} />
|
||||||
|
</React.StrictMode>,
|
||||||
|
document.getElementById('root')
|
||||||
|
);
|
@ -1,25 +0,0 @@
|
|||||||
// Deploy to Netlify
|
|
||||||
package todoapp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"universe.dagger.io/netlify"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Netlify API token
|
|
||||||
input: secrets: netlify: _
|
|
||||||
|
|
||||||
// Must be a valid branch/PR name
|
|
||||||
environment: string
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
|
|
||||||
// Yarn build inherited from base config
|
|
||||||
build: _
|
|
||||||
|
|
||||||
deploy: netlify.#Deploy & {
|
|
||||||
contents: build.output
|
|
||||||
token: input.secrets.netlify.contents
|
|
||||||
site: *"acme-inc-\(environment)" | string
|
|
||||||
team: *"acme-inc" | string
|
|
||||||
}
|
|
||||||
}
|
|
10778
pkg/universe.dagger.io/examples/todoapp/yarn.lock
Normal file
10778
pkg/universe.dagger.io/examples/todoapp/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@ -35,6 +35,9 @@ if [ -z "$site_id" ] ; then
|
|||||||
if [ -z "$site_id" ]; then
|
if [ -z "$site_id" ]; then
|
||||||
echo "create site failed"
|
echo "create site failed"
|
||||||
exit 1
|
exit 1
|
||||||
|
else
|
||||||
|
echo "clean create site API response..."
|
||||||
|
rm -f body
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
@ -67,7 +67,7 @@ module.exports = {
|
|||||||
{
|
{
|
||||||
type: "link",
|
type: "link",
|
||||||
label: "🆕 Dagger Europa 🆕",
|
label: "🆕 Dagger Europa 🆕",
|
||||||
href: "/1201/ci-environment",
|
href: "/1200/local-dev",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
europa: [
|
europa: [
|
||||||
|
Reference in New Issue
Block a user