Europa docs: From local dev to CI environment doc page
The todoapp example contains a Netlify plan which uses the latest dagger additions: do & Client API. We are thinking of merging the examples repository into this one to make working with this easier. This is a step in that direction. We are not using the yarn package so that we can revert https://github.com/dagger/dagger/pull/1673 without breaking this implementation. The GitHub Action is WIP, we will continue with that tomorrow: https://github.com/dagger/dagger-for-github/issues/24 Signed-off-by: Gerhard Lazu <gerhard@lazu.co.uk>
This commit is contained in:
parent
2a6962ddc8
commit
c3f21958d2
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
|
||||
|
||||
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';
|
||||
|
||||
<Tabs defaultValue="github-actions"
|
||||
@ -13,23 +26,70 @@ values={[
|
||||
{label: 'GitHub Actions', value: 'github-actions'},
|
||||
{label: 'CircleCI', value: 'circleci'},
|
||||
{label: 'GitLab', value: 'gitlab'},
|
||||
{label: 'Jenkins', value: 'jenkins'},
|
||||
{label: 'Tekton', value: 'tekton'},
|
||||
]}>
|
||||
|
||||
<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 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 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>
|
||||
|
||||
|
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
|
||||
echo "create site failed"
|
||||
exit 1
|
||||
else
|
||||
echo "clean create site API response..."
|
||||
rm -f body
|
||||
fi
|
||||
fi
|
||||
|
||||
|
@ -67,7 +67,7 @@ module.exports = {
|
||||
{
|
||||
type: "link",
|
||||
label: "🆕 Dagger Europa 🆕",
|
||||
href: "/1201/ci-environment",
|
||||
href: "/1200/local-dev",
|
||||
},
|
||||
],
|
||||
europa: [
|
||||
|
Reference in New Issue
Block a user