From d03cd380dda3dfbc655c325ea122a19b07220b89 Mon Sep 17 00:00:00 2001 From: Anton Bracke Date: Sat, 17 Jul 2021 08:16:54 +0200 Subject: [PATCH 001/143] refactor(web): rename js files to jsx --- web/src/config/client/{inject.js => inject.jsx} | 0 web/src/index.js | 14 -------------- web/src/index.jsx | 10 ++++++++++ web/src/screens/{drone.js => drone.jsx} | 10 +++++----- .../feed/components/{index.js => index.jsx} | 0 .../screens/feed/components/{list.js => list.jsx} | 0 web/src/screens/feed/{index.js => index.jsx} | 0 web/src/screens/{layout.js => layout.jsx} | 0 .../login/screens/error/{index.js => index.jsx} | 0 .../login/screens/form/{index.js => index.jsx} | 0 .../screens/login/screens/{index.js => index.jsx} | 0 web/src/screens/{redirect.js => redirect.jsx} | 0 .../build/components/{approval.js => approval.jsx} | 0 .../build/components/{details.js => details.jsx} | 0 .../build/components/{elapsed.js => elapsed.jsx} | 0 .../build/components/{index.js => index.jsx} | 0 .../build/components/{procs.js => procs.jsx} | 0 .../repo/screens/build/{index.js => index.jsx} | 0 .../logs/components/{anchor.js => anchor.jsx} | 0 .../build/logs/components/{term.js => term.jsx} | 0 .../screens/build/logs/{index.js => index.jsx} | 0 .../repo/screens/build/{menu.js => menu.jsx} | 0 .../builds/components/{index.js => index.jsx} | 0 .../builds/components/{list.js => list.jsx} | 0 .../repo/screens/builds/{header.js => header.jsx} | 0 .../repo/screens/builds/{index.js => index.jsx} | 0 .../repo/screens/builds/{menu.js => menu.jsx} | 0 .../registry/components/{form.js => form.jsx} | 0 .../registry/components/{index.js => index.jsx} | 0 .../registry/components/{list.js => list.jsx} | 0 .../repo/screens/registry/{index.js => index.jsx} | 0 .../secrets/components/{form.js => form.jsx} | 0 .../secrets/components/{index.js => index.jsx} | 0 .../secrets/components/{list.js => list.jsx} | 0 .../repo/screens/secrets/{index.js => index.jsx} | 0 .../repo/screens/settings/{index.js => index.jsx} | 0 web/src/screens/{titles.js => titles.jsx} | 0 .../repos/components/{index.js => index.jsx} | 0 .../screens/repos/components/{list.js => list.jsx} | 0 .../repos/components/{switch.js => switch.jsx} | 0 .../user/screens/tokens/{index.js => index.jsx} | 0 .../shared/components/{avatar.js => avatar.jsx} | 0 .../components/{breadcrumb.js => breadcrumb.jsx} | 0 .../components/{build_event.js => build_event.jsx} | 0 .../components/{build_time.js => build_time.jsx} | 0 .../components/drawer/{drawer.js => drawer.jsx} | 0 .../components/{duration.js => duration.jsx} | 0 .../shared/components/icons/{back.js => back.jsx} | 0 .../components/icons/{branch.js => branch.jsx} | 0 .../components/icons/{check.js => check.jsx} | 0 .../components/icons/{clock.js => clock.jsx} | 0 .../components/icons/{close.js => close.jsx} | 0 .../components/icons/{commit.js => commit.jsx} | 0 .../components/icons/{deploy.js => deploy.jsx} | 0 .../components/icons/{expand.js => expand.jsx} | 0 .../components/icons/{index.js => index.jsx} | 0 .../components/icons/{launch.js => launch.jsx} | 0 .../shared/components/icons/{link.js => link.jsx} | 0 .../shared/components/icons/{menu.js => menu.jsx} | 0 .../components/icons/{merge.js => merge.jsx} | 0 .../components/icons/{pause.js => pause.jsx} | 0 .../shared/components/icons/{play.js => play.jsx} | 0 .../components/icons/{refresh.js => refresh.jsx} | 0 .../components/icons/{remove.js => remove.jsx} | 0 .../components/icons/{report.js => report.jsx} | 0 .../components/icons/{schedule.js => schedule.jsx} | 0 .../shared/components/icons/{star.js => star.jsx} | 0 .../shared/components/icons/{sync.js => sync.jsx} | 0 .../shared/components/icons/{tag.js => tag.jsx} | 0 .../icons/{timelapse.js => timelapse.jsx} | 0 web/src/shared/components/{logo.js => logo.jsx} | 0 web/src/shared/components/{menu.js => menu.jsx} | 0 .../components/{snackbar.js => snackbar.jsx} | 0 .../shared/components/{status.js => status.jsx} | 0 .../{status_number.js => status_number.jsx} | 0 web/src/shared/components/{sync.js => sync.jsx} | 0 76 files changed, 15 insertions(+), 19 deletions(-) rename web/src/config/client/{inject.js => inject.jsx} (100%) delete mode 100644 web/src/index.js create mode 100644 web/src/index.jsx rename web/src/screens/{drone.js => drone.jsx} (81%) rename web/src/screens/feed/components/{index.js => index.jsx} (100%) rename web/src/screens/feed/components/{list.js => list.jsx} (100%) rename web/src/screens/feed/{index.js => index.jsx} (100%) rename web/src/screens/{layout.js => layout.jsx} (100%) rename web/src/screens/login/screens/error/{index.js => index.jsx} (100%) rename web/src/screens/login/screens/form/{index.js => index.jsx} (100%) rename web/src/screens/login/screens/{index.js => index.jsx} (100%) rename web/src/screens/{redirect.js => redirect.jsx} (100%) rename web/src/screens/repo/screens/build/components/{approval.js => approval.jsx} (100%) rename web/src/screens/repo/screens/build/components/{details.js => details.jsx} (100%) rename web/src/screens/repo/screens/build/components/{elapsed.js => elapsed.jsx} (100%) rename web/src/screens/repo/screens/build/components/{index.js => index.jsx} (100%) rename web/src/screens/repo/screens/build/components/{procs.js => procs.jsx} (100%) rename web/src/screens/repo/screens/build/{index.js => index.jsx} (100%) rename web/src/screens/repo/screens/build/logs/components/{anchor.js => anchor.jsx} (100%) rename web/src/screens/repo/screens/build/logs/components/{term.js => term.jsx} (100%) rename web/src/screens/repo/screens/build/logs/{index.js => index.jsx} (100%) rename web/src/screens/repo/screens/build/{menu.js => menu.jsx} (100%) rename web/src/screens/repo/screens/builds/components/{index.js => index.jsx} (100%) rename web/src/screens/repo/screens/builds/components/{list.js => list.jsx} (100%) rename web/src/screens/repo/screens/builds/{header.js => header.jsx} (100%) rename web/src/screens/repo/screens/builds/{index.js => index.jsx} (100%) rename web/src/screens/repo/screens/builds/{menu.js => menu.jsx} (100%) rename web/src/screens/repo/screens/registry/components/{form.js => form.jsx} (100%) rename web/src/screens/repo/screens/registry/components/{index.js => index.jsx} (100%) rename web/src/screens/repo/screens/registry/components/{list.js => list.jsx} (100%) rename web/src/screens/repo/screens/registry/{index.js => index.jsx} (100%) rename web/src/screens/repo/screens/secrets/components/{form.js => form.jsx} (100%) rename web/src/screens/repo/screens/secrets/components/{index.js => index.jsx} (100%) rename web/src/screens/repo/screens/secrets/components/{list.js => list.jsx} (100%) rename web/src/screens/repo/screens/secrets/{index.js => index.jsx} (100%) rename web/src/screens/repo/screens/settings/{index.js => index.jsx} (100%) rename web/src/screens/{titles.js => titles.jsx} (100%) rename web/src/screens/user/screens/repos/components/{index.js => index.jsx} (100%) rename web/src/screens/user/screens/repos/components/{list.js => list.jsx} (100%) rename web/src/screens/user/screens/repos/components/{switch.js => switch.jsx} (100%) rename web/src/screens/user/screens/tokens/{index.js => index.jsx} (100%) rename web/src/shared/components/{avatar.js => avatar.jsx} (100%) rename web/src/shared/components/{breadcrumb.js => breadcrumb.jsx} (100%) rename web/src/shared/components/{build_event.js => build_event.jsx} (100%) rename web/src/shared/components/{build_time.js => build_time.jsx} (100%) rename web/src/shared/components/drawer/{drawer.js => drawer.jsx} (100%) rename web/src/shared/components/{duration.js => duration.jsx} (100%) rename web/src/shared/components/icons/{back.js => back.jsx} (100%) rename web/src/shared/components/icons/{branch.js => branch.jsx} (100%) rename web/src/shared/components/icons/{check.js => check.jsx} (100%) rename web/src/shared/components/icons/{clock.js => clock.jsx} (100%) rename web/src/shared/components/icons/{close.js => close.jsx} (100%) rename web/src/shared/components/icons/{commit.js => commit.jsx} (100%) rename web/src/shared/components/icons/{deploy.js => deploy.jsx} (100%) rename web/src/shared/components/icons/{expand.js => expand.jsx} (100%) rename web/src/shared/components/icons/{index.js => index.jsx} (100%) rename web/src/shared/components/icons/{launch.js => launch.jsx} (100%) rename web/src/shared/components/icons/{link.js => link.jsx} (100%) rename web/src/shared/components/icons/{menu.js => menu.jsx} (100%) rename web/src/shared/components/icons/{merge.js => merge.jsx} (100%) rename web/src/shared/components/icons/{pause.js => pause.jsx} (100%) rename web/src/shared/components/icons/{play.js => play.jsx} (100%) rename web/src/shared/components/icons/{refresh.js => refresh.jsx} (100%) rename web/src/shared/components/icons/{remove.js => remove.jsx} (100%) rename web/src/shared/components/icons/{report.js => report.jsx} (100%) rename web/src/shared/components/icons/{schedule.js => schedule.jsx} (100%) rename web/src/shared/components/icons/{star.js => star.jsx} (100%) rename web/src/shared/components/icons/{sync.js => sync.jsx} (100%) rename web/src/shared/components/icons/{tag.js => tag.jsx} (100%) rename web/src/shared/components/icons/{timelapse.js => timelapse.jsx} (100%) rename web/src/shared/components/{logo.js => logo.jsx} (100%) rename web/src/shared/components/{menu.js => menu.jsx} (100%) rename web/src/shared/components/{snackbar.js => snackbar.jsx} (100%) rename web/src/shared/components/{status.js => status.jsx} (100%) rename web/src/shared/components/{status_number.js => status_number.jsx} (100%) rename web/src/shared/components/{sync.js => sync.jsx} (100%) diff --git a/web/src/config/client/inject.js b/web/src/config/client/inject.jsx similarity index 100% rename from web/src/config/client/inject.js rename to web/src/config/client/inject.jsx diff --git a/web/src/index.js b/web/src/index.js deleted file mode 100644 index 92a72f0232..0000000000 --- a/web/src/index.js +++ /dev/null @@ -1,14 +0,0 @@ -import "babel-polyfill"; -import React from "react"; -import { render } from "react-dom"; - -let root; - -function init() { - let App = require("./screens/drone").default; - root = render(, document.body, root); -} - -init(); - -if (module.hot) module.hot.accept("./screens/drone", init); diff --git a/web/src/index.jsx b/web/src/index.jsx new file mode 100644 index 0000000000..0626c17db2 --- /dev/null +++ b/web/src/index.jsx @@ -0,0 +1,10 @@ +import React from "react"; +import ReactDOM from "react-dom"; +import App from "./screens/drone"; + +ReactDOM.render( + + + , + document.getElementById("root"), +); diff --git a/web/src/screens/drone.js b/web/src/screens/drone.jsx similarity index 81% rename from web/src/screens/drone.js rename to web/src/screens/drone.jsx index 574848f08a..9f463af7c4 100644 --- a/web/src/screens/drone.js +++ b/web/src/screens/drone.jsx @@ -1,14 +1,14 @@ import React, { Component } from "react"; import { root } from "baobab-react/higher-order"; -import tree from "config/state"; -import client from "config/client"; -import { drone } from "config/client/inject"; -import { LoginForm, LoginError } from "screens/login/screens"; +import tree from "~/config/state"; +import client from "~/config/client"; +import { drone } from "~/config/client/inject"; +import { LoginForm, LoginError } from "~/screens/login/screens"; import Title from "./titles"; import Layout from "./layout"; import RedirectRoot from "./redirect"; -import { fetchFeedOnce, subscribeToFeedOnce } from "shared/utils/feed"; +import { fetchFeedOnce, subscribeToFeedOnce } from "~/shared/utils/feed"; import { BrowserRouter, Route, Switch } from "react-router-dom"; diff --git a/web/src/screens/feed/components/index.js b/web/src/screens/feed/components/index.jsx similarity index 100% rename from web/src/screens/feed/components/index.js rename to web/src/screens/feed/components/index.jsx diff --git a/web/src/screens/feed/components/list.js b/web/src/screens/feed/components/list.jsx similarity index 100% rename from web/src/screens/feed/components/list.js rename to web/src/screens/feed/components/list.jsx diff --git a/web/src/screens/feed/index.js b/web/src/screens/feed/index.jsx similarity index 100% rename from web/src/screens/feed/index.js rename to web/src/screens/feed/index.jsx diff --git a/web/src/screens/layout.js b/web/src/screens/layout.jsx similarity index 100% rename from web/src/screens/layout.js rename to web/src/screens/layout.jsx diff --git a/web/src/screens/login/screens/error/index.js b/web/src/screens/login/screens/error/index.jsx similarity index 100% rename from web/src/screens/login/screens/error/index.js rename to web/src/screens/login/screens/error/index.jsx diff --git a/web/src/screens/login/screens/form/index.js b/web/src/screens/login/screens/form/index.jsx similarity index 100% rename from web/src/screens/login/screens/form/index.js rename to web/src/screens/login/screens/form/index.jsx diff --git a/web/src/screens/login/screens/index.js b/web/src/screens/login/screens/index.jsx similarity index 100% rename from web/src/screens/login/screens/index.js rename to web/src/screens/login/screens/index.jsx diff --git a/web/src/screens/redirect.js b/web/src/screens/redirect.jsx similarity index 100% rename from web/src/screens/redirect.js rename to web/src/screens/redirect.jsx diff --git a/web/src/screens/repo/screens/build/components/approval.js b/web/src/screens/repo/screens/build/components/approval.jsx similarity index 100% rename from web/src/screens/repo/screens/build/components/approval.js rename to web/src/screens/repo/screens/build/components/approval.jsx diff --git a/web/src/screens/repo/screens/build/components/details.js b/web/src/screens/repo/screens/build/components/details.jsx similarity index 100% rename from web/src/screens/repo/screens/build/components/details.js rename to web/src/screens/repo/screens/build/components/details.jsx diff --git a/web/src/screens/repo/screens/build/components/elapsed.js b/web/src/screens/repo/screens/build/components/elapsed.jsx similarity index 100% rename from web/src/screens/repo/screens/build/components/elapsed.js rename to web/src/screens/repo/screens/build/components/elapsed.jsx diff --git a/web/src/screens/repo/screens/build/components/index.js b/web/src/screens/repo/screens/build/components/index.jsx similarity index 100% rename from web/src/screens/repo/screens/build/components/index.js rename to web/src/screens/repo/screens/build/components/index.jsx diff --git a/web/src/screens/repo/screens/build/components/procs.js b/web/src/screens/repo/screens/build/components/procs.jsx similarity index 100% rename from web/src/screens/repo/screens/build/components/procs.js rename to web/src/screens/repo/screens/build/components/procs.jsx diff --git a/web/src/screens/repo/screens/build/index.js b/web/src/screens/repo/screens/build/index.jsx similarity index 100% rename from web/src/screens/repo/screens/build/index.js rename to web/src/screens/repo/screens/build/index.jsx diff --git a/web/src/screens/repo/screens/build/logs/components/anchor.js b/web/src/screens/repo/screens/build/logs/components/anchor.jsx similarity index 100% rename from web/src/screens/repo/screens/build/logs/components/anchor.js rename to web/src/screens/repo/screens/build/logs/components/anchor.jsx diff --git a/web/src/screens/repo/screens/build/logs/components/term.js b/web/src/screens/repo/screens/build/logs/components/term.jsx similarity index 100% rename from web/src/screens/repo/screens/build/logs/components/term.js rename to web/src/screens/repo/screens/build/logs/components/term.jsx diff --git a/web/src/screens/repo/screens/build/logs/index.js b/web/src/screens/repo/screens/build/logs/index.jsx similarity index 100% rename from web/src/screens/repo/screens/build/logs/index.js rename to web/src/screens/repo/screens/build/logs/index.jsx diff --git a/web/src/screens/repo/screens/build/menu.js b/web/src/screens/repo/screens/build/menu.jsx similarity index 100% rename from web/src/screens/repo/screens/build/menu.js rename to web/src/screens/repo/screens/build/menu.jsx diff --git a/web/src/screens/repo/screens/builds/components/index.js b/web/src/screens/repo/screens/builds/components/index.jsx similarity index 100% rename from web/src/screens/repo/screens/builds/components/index.js rename to web/src/screens/repo/screens/builds/components/index.jsx diff --git a/web/src/screens/repo/screens/builds/components/list.js b/web/src/screens/repo/screens/builds/components/list.jsx similarity index 100% rename from web/src/screens/repo/screens/builds/components/list.js rename to web/src/screens/repo/screens/builds/components/list.jsx diff --git a/web/src/screens/repo/screens/builds/header.js b/web/src/screens/repo/screens/builds/header.jsx similarity index 100% rename from web/src/screens/repo/screens/builds/header.js rename to web/src/screens/repo/screens/builds/header.jsx diff --git a/web/src/screens/repo/screens/builds/index.js b/web/src/screens/repo/screens/builds/index.jsx similarity index 100% rename from web/src/screens/repo/screens/builds/index.js rename to web/src/screens/repo/screens/builds/index.jsx diff --git a/web/src/screens/repo/screens/builds/menu.js b/web/src/screens/repo/screens/builds/menu.jsx similarity index 100% rename from web/src/screens/repo/screens/builds/menu.js rename to web/src/screens/repo/screens/builds/menu.jsx diff --git a/web/src/screens/repo/screens/registry/components/form.js b/web/src/screens/repo/screens/registry/components/form.jsx similarity index 100% rename from web/src/screens/repo/screens/registry/components/form.js rename to web/src/screens/repo/screens/registry/components/form.jsx diff --git a/web/src/screens/repo/screens/registry/components/index.js b/web/src/screens/repo/screens/registry/components/index.jsx similarity index 100% rename from web/src/screens/repo/screens/registry/components/index.js rename to web/src/screens/repo/screens/registry/components/index.jsx diff --git a/web/src/screens/repo/screens/registry/components/list.js b/web/src/screens/repo/screens/registry/components/list.jsx similarity index 100% rename from web/src/screens/repo/screens/registry/components/list.js rename to web/src/screens/repo/screens/registry/components/list.jsx diff --git a/web/src/screens/repo/screens/registry/index.js b/web/src/screens/repo/screens/registry/index.jsx similarity index 100% rename from web/src/screens/repo/screens/registry/index.js rename to web/src/screens/repo/screens/registry/index.jsx diff --git a/web/src/screens/repo/screens/secrets/components/form.js b/web/src/screens/repo/screens/secrets/components/form.jsx similarity index 100% rename from web/src/screens/repo/screens/secrets/components/form.js rename to web/src/screens/repo/screens/secrets/components/form.jsx diff --git a/web/src/screens/repo/screens/secrets/components/index.js b/web/src/screens/repo/screens/secrets/components/index.jsx similarity index 100% rename from web/src/screens/repo/screens/secrets/components/index.js rename to web/src/screens/repo/screens/secrets/components/index.jsx diff --git a/web/src/screens/repo/screens/secrets/components/list.js b/web/src/screens/repo/screens/secrets/components/list.jsx similarity index 100% rename from web/src/screens/repo/screens/secrets/components/list.js rename to web/src/screens/repo/screens/secrets/components/list.jsx diff --git a/web/src/screens/repo/screens/secrets/index.js b/web/src/screens/repo/screens/secrets/index.jsx similarity index 100% rename from web/src/screens/repo/screens/secrets/index.js rename to web/src/screens/repo/screens/secrets/index.jsx diff --git a/web/src/screens/repo/screens/settings/index.js b/web/src/screens/repo/screens/settings/index.jsx similarity index 100% rename from web/src/screens/repo/screens/settings/index.js rename to web/src/screens/repo/screens/settings/index.jsx diff --git a/web/src/screens/titles.js b/web/src/screens/titles.jsx similarity index 100% rename from web/src/screens/titles.js rename to web/src/screens/titles.jsx diff --git a/web/src/screens/user/screens/repos/components/index.js b/web/src/screens/user/screens/repos/components/index.jsx similarity index 100% rename from web/src/screens/user/screens/repos/components/index.js rename to web/src/screens/user/screens/repos/components/index.jsx diff --git a/web/src/screens/user/screens/repos/components/list.js b/web/src/screens/user/screens/repos/components/list.jsx similarity index 100% rename from web/src/screens/user/screens/repos/components/list.js rename to web/src/screens/user/screens/repos/components/list.jsx diff --git a/web/src/screens/user/screens/repos/components/switch.js b/web/src/screens/user/screens/repos/components/switch.jsx similarity index 100% rename from web/src/screens/user/screens/repos/components/switch.js rename to web/src/screens/user/screens/repos/components/switch.jsx diff --git a/web/src/screens/user/screens/tokens/index.js b/web/src/screens/user/screens/tokens/index.jsx similarity index 100% rename from web/src/screens/user/screens/tokens/index.js rename to web/src/screens/user/screens/tokens/index.jsx diff --git a/web/src/shared/components/avatar.js b/web/src/shared/components/avatar.jsx similarity index 100% rename from web/src/shared/components/avatar.js rename to web/src/shared/components/avatar.jsx diff --git a/web/src/shared/components/breadcrumb.js b/web/src/shared/components/breadcrumb.jsx similarity index 100% rename from web/src/shared/components/breadcrumb.js rename to web/src/shared/components/breadcrumb.jsx diff --git a/web/src/shared/components/build_event.js b/web/src/shared/components/build_event.jsx similarity index 100% rename from web/src/shared/components/build_event.js rename to web/src/shared/components/build_event.jsx diff --git a/web/src/shared/components/build_time.js b/web/src/shared/components/build_time.jsx similarity index 100% rename from web/src/shared/components/build_time.js rename to web/src/shared/components/build_time.jsx diff --git a/web/src/shared/components/drawer/drawer.js b/web/src/shared/components/drawer/drawer.jsx similarity index 100% rename from web/src/shared/components/drawer/drawer.js rename to web/src/shared/components/drawer/drawer.jsx diff --git a/web/src/shared/components/duration.js b/web/src/shared/components/duration.jsx similarity index 100% rename from web/src/shared/components/duration.js rename to web/src/shared/components/duration.jsx diff --git a/web/src/shared/components/icons/back.js b/web/src/shared/components/icons/back.jsx similarity index 100% rename from web/src/shared/components/icons/back.js rename to web/src/shared/components/icons/back.jsx diff --git a/web/src/shared/components/icons/branch.js b/web/src/shared/components/icons/branch.jsx similarity index 100% rename from web/src/shared/components/icons/branch.js rename to web/src/shared/components/icons/branch.jsx diff --git a/web/src/shared/components/icons/check.js b/web/src/shared/components/icons/check.jsx similarity index 100% rename from web/src/shared/components/icons/check.js rename to web/src/shared/components/icons/check.jsx diff --git a/web/src/shared/components/icons/clock.js b/web/src/shared/components/icons/clock.jsx similarity index 100% rename from web/src/shared/components/icons/clock.js rename to web/src/shared/components/icons/clock.jsx diff --git a/web/src/shared/components/icons/close.js b/web/src/shared/components/icons/close.jsx similarity index 100% rename from web/src/shared/components/icons/close.js rename to web/src/shared/components/icons/close.jsx diff --git a/web/src/shared/components/icons/commit.js b/web/src/shared/components/icons/commit.jsx similarity index 100% rename from web/src/shared/components/icons/commit.js rename to web/src/shared/components/icons/commit.jsx diff --git a/web/src/shared/components/icons/deploy.js b/web/src/shared/components/icons/deploy.jsx similarity index 100% rename from web/src/shared/components/icons/deploy.js rename to web/src/shared/components/icons/deploy.jsx diff --git a/web/src/shared/components/icons/expand.js b/web/src/shared/components/icons/expand.jsx similarity index 100% rename from web/src/shared/components/icons/expand.js rename to web/src/shared/components/icons/expand.jsx diff --git a/web/src/shared/components/icons/index.js b/web/src/shared/components/icons/index.jsx similarity index 100% rename from web/src/shared/components/icons/index.js rename to web/src/shared/components/icons/index.jsx diff --git a/web/src/shared/components/icons/launch.js b/web/src/shared/components/icons/launch.jsx similarity index 100% rename from web/src/shared/components/icons/launch.js rename to web/src/shared/components/icons/launch.jsx diff --git a/web/src/shared/components/icons/link.js b/web/src/shared/components/icons/link.jsx similarity index 100% rename from web/src/shared/components/icons/link.js rename to web/src/shared/components/icons/link.jsx diff --git a/web/src/shared/components/icons/menu.js b/web/src/shared/components/icons/menu.jsx similarity index 100% rename from web/src/shared/components/icons/menu.js rename to web/src/shared/components/icons/menu.jsx diff --git a/web/src/shared/components/icons/merge.js b/web/src/shared/components/icons/merge.jsx similarity index 100% rename from web/src/shared/components/icons/merge.js rename to web/src/shared/components/icons/merge.jsx diff --git a/web/src/shared/components/icons/pause.js b/web/src/shared/components/icons/pause.jsx similarity index 100% rename from web/src/shared/components/icons/pause.js rename to web/src/shared/components/icons/pause.jsx diff --git a/web/src/shared/components/icons/play.js b/web/src/shared/components/icons/play.jsx similarity index 100% rename from web/src/shared/components/icons/play.js rename to web/src/shared/components/icons/play.jsx diff --git a/web/src/shared/components/icons/refresh.js b/web/src/shared/components/icons/refresh.jsx similarity index 100% rename from web/src/shared/components/icons/refresh.js rename to web/src/shared/components/icons/refresh.jsx diff --git a/web/src/shared/components/icons/remove.js b/web/src/shared/components/icons/remove.jsx similarity index 100% rename from web/src/shared/components/icons/remove.js rename to web/src/shared/components/icons/remove.jsx diff --git a/web/src/shared/components/icons/report.js b/web/src/shared/components/icons/report.jsx similarity index 100% rename from web/src/shared/components/icons/report.js rename to web/src/shared/components/icons/report.jsx diff --git a/web/src/shared/components/icons/schedule.js b/web/src/shared/components/icons/schedule.jsx similarity index 100% rename from web/src/shared/components/icons/schedule.js rename to web/src/shared/components/icons/schedule.jsx diff --git a/web/src/shared/components/icons/star.js b/web/src/shared/components/icons/star.jsx similarity index 100% rename from web/src/shared/components/icons/star.js rename to web/src/shared/components/icons/star.jsx diff --git a/web/src/shared/components/icons/sync.js b/web/src/shared/components/icons/sync.jsx similarity index 100% rename from web/src/shared/components/icons/sync.js rename to web/src/shared/components/icons/sync.jsx diff --git a/web/src/shared/components/icons/tag.js b/web/src/shared/components/icons/tag.jsx similarity index 100% rename from web/src/shared/components/icons/tag.js rename to web/src/shared/components/icons/tag.jsx diff --git a/web/src/shared/components/icons/timelapse.js b/web/src/shared/components/icons/timelapse.jsx similarity index 100% rename from web/src/shared/components/icons/timelapse.js rename to web/src/shared/components/icons/timelapse.jsx diff --git a/web/src/shared/components/logo.js b/web/src/shared/components/logo.jsx similarity index 100% rename from web/src/shared/components/logo.js rename to web/src/shared/components/logo.jsx diff --git a/web/src/shared/components/menu.js b/web/src/shared/components/menu.jsx similarity index 100% rename from web/src/shared/components/menu.js rename to web/src/shared/components/menu.jsx diff --git a/web/src/shared/components/snackbar.js b/web/src/shared/components/snackbar.jsx similarity index 100% rename from web/src/shared/components/snackbar.js rename to web/src/shared/components/snackbar.jsx diff --git a/web/src/shared/components/status.js b/web/src/shared/components/status.jsx similarity index 100% rename from web/src/shared/components/status.js rename to web/src/shared/components/status.jsx diff --git a/web/src/shared/components/status_number.js b/web/src/shared/components/status_number.jsx similarity index 100% rename from web/src/shared/components/status_number.js rename to web/src/shared/components/status_number.jsx diff --git a/web/src/shared/components/sync.js b/web/src/shared/components/sync.jsx similarity index 100% rename from web/src/shared/components/sync.js rename to web/src/shared/components/sync.jsx From 45604475c6bff13f8e15c485673c053cad5e7a3b Mon Sep 17 00:00:00 2001 From: Anton Bracke Date: Sat, 17 Jul 2021 18:48:36 +0200 Subject: [PATCH 002/143] feat: add new ui --- .gitignore | 3 +- web-new/.eslintrc.js | 33 + web-new/.gitignore | 5 + web-new/.prettierignore | 7 + web-new/.prettierrc.js | 8 + web-new/README.md | 27 + web-new/index.html | 13 + web-new/package.json | 24 + web-new/pecker.html | 158 +++++ web-new/pnpm-lock.yaml | 868 +++++++++++++++++++++++ web-new/public/favicon.svg | 1 + web-new/src/App.vue | 27 + web-new/src/assets/logo.svg | 1 + web-new/src/components/HelloWorld.vue | 70 ++ web-new/src/components/Navbar.vue | 29 + web-new/src/compositions/useApiClient.ts | 15 + web-new/src/lib/api/client.ts | 117 +++ web-new/src/lib/api/index.ts | 116 +++ web-new/src/lib/api/types/index.ts | 2 + web-new/src/lib/api/types/repo.ts | 55 ++ web-new/src/lib/api/types/user.ts | 20 + web-new/src/main.ts | 11 + web-new/src/router.ts | 43 ++ web-new/src/shims-vue.d.ts | 6 + web-new/src/views/Home.vue | 11 + web-new/src/views/Login.vue | 28 + web-new/src/views/NotFound.vue | 14 + web-new/src/vite-env.d.ts | 1 + web-new/tsconfig.json | 17 + web-new/vite.config.ts | 23 + web-new/windi.config.ts | 13 + 31 files changed, 1765 insertions(+), 1 deletion(-) create mode 100644 web-new/.eslintrc.js create mode 100644 web-new/.gitignore create mode 100644 web-new/.prettierignore create mode 100644 web-new/.prettierrc.js create mode 100644 web-new/README.md create mode 100644 web-new/index.html create mode 100644 web-new/package.json create mode 100644 web-new/pecker.html create mode 100644 web-new/pnpm-lock.yaml create mode 100644 web-new/public/favicon.svg create mode 100644 web-new/src/App.vue create mode 100644 web-new/src/assets/logo.svg create mode 100644 web-new/src/components/HelloWorld.vue create mode 100644 web-new/src/components/Navbar.vue create mode 100644 web-new/src/compositions/useApiClient.ts create mode 100644 web-new/src/lib/api/client.ts create mode 100644 web-new/src/lib/api/index.ts create mode 100644 web-new/src/lib/api/types/index.ts create mode 100644 web-new/src/lib/api/types/repo.ts create mode 100644 web-new/src/lib/api/types/user.ts create mode 100644 web-new/src/main.ts create mode 100644 web-new/src/router.ts create mode 100644 web-new/src/shims-vue.d.ts create mode 100644 web-new/src/views/Home.vue create mode 100644 web-new/src/views/Login.vue create mode 100644 web-new/src/views/NotFound.vue create mode 100644 web-new/src/vite-env.d.ts create mode 100644 web-new/tsconfig.json create mode 100644 web-new/vite.config.ts create mode 100644 web-new/windi.config.ts diff --git a/.gitignore b/.gitignore index d32958fdab..7eef96efde 100644 --- a/.gitignore +++ b/.gitignore @@ -22,9 +22,10 @@ vendor/ ### Frontend ### web/node_modules -web/dist/files +web/dist web/*.log web/.env +!web/src/screens/repo/screen/build ### Docker ### docker-compose.yml diff --git a/web-new/.eslintrc.js b/web-new/.eslintrc.js new file mode 100644 index 0000000000..235507f919 --- /dev/null +++ b/web-new/.eslintrc.js @@ -0,0 +1,33 @@ +module.exports = { + extends: [ + "standard", + "plugin:jest/recommended", + "plugin:react/recommended", + "prettier", + "prettier/react" + ], + plugins: ["react", "jest", "prettier"], + parser: "babel-eslint", + parserOptions: { + ecmaVersion: 2016, + sourceType: "module", + ecmaFeatures: { + jsx: true + } + }, + env: { + es6: true, + browser: true, + node: true, + "jest/globals": true + }, + rules: { + "react/prop-types": 1, + "prettier/prettier": [ + "error", + { + trailingComma: "all", + } + ] + } +}; diff --git a/web-new/.gitignore b/web-new/.gitignore new file mode 100644 index 0000000000..d451ff16c1 --- /dev/null +++ b/web-new/.gitignore @@ -0,0 +1,5 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local diff --git a/web-new/.prettierignore b/web-new/.prettierignore new file mode 100644 index 0000000000..ab53934313 --- /dev/null +++ b/web-new/.prettierignore @@ -0,0 +1,7 @@ +pnpm-lock.yaml +pnpm-workspace.yaml +dist +coverage/ +storybook-static/ +.pnpm-store/ +.ci/charts/ diff --git a/web-new/.prettierrc.js b/web-new/.prettierrc.js new file mode 100644 index 0000000000..b651096fd4 --- /dev/null +++ b/web-new/.prettierrc.js @@ -0,0 +1,8 @@ +module.exports = { + semi: true, + trailingComma: 'all', + singleQuote: true, + printWidth: 120, + tabWidth: 2, + endOfLine: 'lf', +}; diff --git a/web-new/README.md b/web-new/README.md new file mode 100644 index 0000000000..a797a275d0 --- /dev/null +++ b/web-new/README.md @@ -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 ` + + diff --git a/web-new/package.json b/web-new/package.json new file mode 100644 index 0000000000..dbcbe9f651 --- /dev/null +++ b/web-new/package.json @@ -0,0 +1,24 @@ +{ + "version": "0.0.0", + "scripts": { + "start": "vite", + "build": "vue-tsc --noEmit && vite build", + "serve": "vite preview", + "lint": "eslint src/", + "format": "prettier --write src/**/*.js" + }, + "dependencies": { + "vue": "^3.0.5", + "vue-router": "4.0.10" + }, + "devDependencies": { + "@types/node": "^16.3.3", + "@vitejs/plugin-vue": "^1.2.5", + "@vue/compiler-sfc": "^3.0.5", + "typescript": "^4.3.2", + "vite": "^2.4.2", + "vite-plugin-windicss": "^1.2.4", + "vue-tsc": "^0.0.24", + "windicss": "3.1.0" + } +} diff --git a/web-new/pecker.html b/web-new/pecker.html new file mode 100644 index 0000000000..05cce16911 --- /dev/null +++ b/web-new/pecker.html @@ -0,0 +1,158 @@ + + + + + + Page Title + + + + +
+ + + + + + + + + + + + +
+ + diff --git a/web-new/pnpm-lock.yaml b/web-new/pnpm-lock.yaml new file mode 100644 index 0000000000..8c81b2d80f --- /dev/null +++ b/web-new/pnpm-lock.yaml @@ -0,0 +1,868 @@ +lockfileVersion: 5.3 + +specifiers: + '@types/node': ^16.3.3 + '@vitejs/plugin-vue': ^1.2.5 + '@vue/compiler-sfc': ^3.0.5 + typescript: ^4.3.2 + vite: ^2.4.2 + vite-plugin-windicss: ^1.2.4 + vue: ^3.0.5 + vue-router: 4.0.10 + vue-tsc: ^0.0.24 + windicss: 3.1.0 + +dependencies: + vue: 3.1.5 + vue-router: 4.0.10_vue@3.1.5 + +devDependencies: + '@types/node': 16.3.3 + '@vitejs/plugin-vue': 1.2.5_@vue+compiler-sfc@3.1.5 + '@vue/compiler-sfc': 3.1.5_vue@3.1.5 + typescript: 4.3.5 + vite: 2.4.2 + vite-plugin-windicss: 1.2.4_vite@2.4.2 + vue-tsc: 0.0.24 + windicss: 3.1.0 + +packages: + + /@antfu/utils/0.2.4: + resolution: {integrity: sha512-2bZNkVfL9IZESmvE26UKi8SzyvSoaIsGXDcnbHFMtmGMqUiB1fXpAJ1ijGf+tSqKRQ5yagck2U1Qk0p+705/kw==} + dependencies: + '@types/throttle-debounce': 2.1.0 + dev: true + + /@babel/helper-validator-identifier/7.14.5: + resolution: {integrity: sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg==} + engines: {node: '>=6.9.0'} + + /@babel/parser/7.14.7: + resolution: {integrity: sha512-X67Z5y+VBJuHB/RjwECp8kSl5uYi0BvRbNeWqkaJCVh+LiTPl19WBUfG627psSgp9rSf6ojuXghQM3ha6qHHdA==} + engines: {node: '>=6.0.0'} + hasBin: true + + /@babel/types/7.14.5: + resolution: {integrity: sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.14.5 + to-fast-properties: 2.0.0 + + /@nodelib/fs.scandir/2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + dev: true + + /@nodelib/fs.stat/2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + dev: true + + /@nodelib/fs.walk/1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.11.1 + dev: true + + /@types/estree/0.0.48: + resolution: {integrity: sha512-LfZwXoGUDo0C3me81HXgkBg5CTQYb6xzEl+fNmbO4JdRiSKQ8A0GD1OBBvKAIsbCUgoyAty7m99GqqMQe784ew==} + dev: true + + /@types/node/16.3.3: + resolution: {integrity: sha512-8h7k1YgQKxKXWckzFCMfsIwn0Y61UK6tlD6y2lOb3hTOIMlK3t9/QwHOhc81TwU+RMf0As5fj7NPjroERCnejQ==} + dev: true + + /@types/throttle-debounce/2.1.0: + resolution: {integrity: sha512-5eQEtSCoESnh2FsiLTxE121IiE60hnMqcb435fShf4bpLRjEu1Eoekht23y6zXS9Ts3l+Szu3TARnTsA0GkOkQ==} + dev: true + + /@vitejs/plugin-vue/1.2.5_@vue+compiler-sfc@3.1.5: + resolution: {integrity: sha512-GIR31mdXTEfvElmBUaRhDc5v7lfdkEdawWQqJRiaRL/5qKsH+xusukglkvJz5y7+c6dEpxgmvcATv2BbB7+fzQ==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@vue/compiler-sfc': ^3.0.8 + dependencies: + '@vue/compiler-sfc': 3.1.5_vue@3.1.5 + dev: true + + /@vue/compiler-core/3.1.5: + resolution: {integrity: sha512-TXBhFinoBaXKDykJzY26UEuQU1K07FOp/0Ie+OXySqqk0bS0ZO7Xvl7UmiTUPYcLrWbxWBR7Bs/y55AI0MNc2Q==} + dependencies: + '@babel/parser': 7.14.7 + '@babel/types': 7.14.5 + '@vue/shared': 3.1.5 + estree-walker: 2.0.2 + source-map: 0.6.1 + + /@vue/compiler-dom/3.1.5: + resolution: {integrity: sha512-ZsL3jqJ52OjGU/YiT/9XiuZAmWClKInZM2aFJh9gnsAPqOrj2JIELMbkIFpVKR/CrVO/f2VxfPiiQdQTr65jcQ==} + dependencies: + '@vue/compiler-core': 3.1.5 + '@vue/shared': 3.1.5 + + /@vue/compiler-sfc/3.1.5_vue@3.1.5: + resolution: {integrity: sha512-mtMY6xMvZeSRx9MTa1+NgJWndrkzVTdJ1pQAmAKQuxyb5LsHVvrgP7kcQFvxPHVpLVTORbTJWHaiqoKrJvi1iA==} + peerDependencies: + vue: 3.1.5 + dependencies: + '@babel/parser': 7.14.7 + '@babel/types': 7.14.5 + '@types/estree': 0.0.48 + '@vue/compiler-core': 3.1.5 + '@vue/compiler-dom': 3.1.5 + '@vue/compiler-ssr': 3.1.5 + '@vue/shared': 3.1.5 + consolidate: 0.16.0 + estree-walker: 2.0.2 + hash-sum: 2.0.0 + lru-cache: 5.1.1 + magic-string: 0.25.7 + merge-source-map: 1.1.0 + postcss: 8.3.5 + postcss-modules: 4.1.3_postcss@8.3.5 + postcss-selector-parser: 6.0.6 + source-map: 0.6.1 + vue: 3.1.5 + dev: true + + /@vue/compiler-ssr/3.1.5: + resolution: {integrity: sha512-CU5N7Di/a4lyJ18LGJxJYZS2a8PlLdWpWHX9p/XcsjT2TngMpj3QvHVRkuik2u8QrIDZ8OpYmTyj1WDNsOV+Dg==} + dependencies: + '@vue/compiler-dom': 3.1.5 + '@vue/shared': 3.1.5 + dev: true + + /@vue/devtools-api/6.0.0-beta.15: + resolution: {integrity: sha512-quBx4Jjpexo6KDiNUGFr/zF/2A4srKM9S9v2uHgMXSU//hjgq1eGzqkIFql8T9gfX5ZaVOUzYBP3jIdIR3PKIA==} + dev: false + + /@vue/reactivity/3.1.5: + resolution: {integrity: sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg==} + dependencies: + '@vue/shared': 3.1.5 + dev: false + + /@vue/runtime-core/3.1.5: + resolution: {integrity: sha512-YQbG5cBktN1RowQDKA22itmvQ+b40f0WgQ6CXK4VYoYICAiAfu6Cc14777ve8zp1rJRGtk5oIeS149TOculrTg==} + dependencies: + '@vue/reactivity': 3.1.5 + '@vue/shared': 3.1.5 + dev: false + + /@vue/runtime-dom/3.1.5: + resolution: {integrity: sha512-tNcf3JhVR0RfW0kw1p8xZgv30nvX8Y9rsz7eiQ0dHe273sfoCngAG0y4GvMaY4Xd8FsjUwFedd4suQ8Lu8meXg==} + dependencies: + '@vue/runtime-core': 3.1.5 + '@vue/shared': 3.1.5 + csstype: 2.6.17 + dev: false + + /@vue/shared/3.1.5: + resolution: {integrity: sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==} + + /@windicss/config/1.2.4: + resolution: {integrity: sha512-81oah5NF7m/ltHrP2VpKu12qejP03erelNckH9ho86vuItsSd/tNehW3oJqOThZYsbuROCzIU55QDfe7MBd+Iw==} + dependencies: + debug: 4.3.2 + jiti: 1.10.1 + windicss: 3.1.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@windicss/plugin-utils/1.2.4: + resolution: {integrity: sha512-0G0zPx+gLfnP2isVMrsY+C0BDXASgFwtiT7g+YL+0LCTxjPxGOMnu5lCaAoYq+1LAskliPagqnu5mgKUYMyf2Q==} + dependencies: + '@antfu/utils': 0.2.4 + '@windicss/config': 1.2.4 + debug: 4.3.2 + fast-glob: 3.2.7 + magic-string: 0.25.7 + micromatch: 4.0.4 + windicss: 3.1.5 + transitivePeerDependencies: + - supports-color + dev: true + + /ansi-styles/4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + dev: true + + /balanced-match/1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: true + + /big-integer/1.6.48: + resolution: {integrity: sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w==} + engines: {node: '>=0.6'} + dev: true + + /big.js/5.2.2: + resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} + dev: true + + /binary/0.3.0: + resolution: {integrity: sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=} + dependencies: + buffers: 0.1.1 + chainsaw: 0.1.0 + dev: true + + /bluebird/3.4.7: + resolution: {integrity: sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=} + dev: true + + /bluebird/3.7.2: + resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + dev: true + + /brace-expansion/1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + dev: true + + /braces/3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.0.1 + dev: true + + /buffer-indexof-polyfill/1.0.2: + resolution: {integrity: sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==} + engines: {node: '>=0.10'} + dev: true + + /buffers/0.1.1: + resolution: {integrity: sha1-skV5w77U1tOWru5tmorn9Ugqt7s=} + engines: {node: '>=0.2.0'} + dev: true + + /chainsaw/0.1.0: + resolution: {integrity: sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=} + dependencies: + traverse: 0.3.9 + dev: true + + /chalk/4.1.1: + resolution: {integrity: sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: true + + /color-convert/2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + dev: true + + /color-name/1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + dev: true + + /colorette/1.2.2: + resolution: {integrity: sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==} + dev: true + + /concat-map/0.0.1: + resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} + dev: true + + /consolidate/0.16.0: + resolution: {integrity: sha512-Nhl1wzCslqXYTJVDyJCu3ODohy9OfBMB5uD2BiBTzd7w+QY0lBzafkR8y8755yMYHAaMD4NuzbAw03/xzfw+eQ==} + engines: {node: '>= 0.10.0'} + dependencies: + bluebird: 3.7.2 + dev: true + + /core-util-is/1.0.2: + resolution: {integrity: sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=} + dev: true + + /cssesc/3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /csstype/2.6.17: + resolution: {integrity: sha512-u1wmTI1jJGzCJzWndZo8mk4wnPTZd1eOIYTYvuEyOQGfmDl3TrabCCfKnOC86FZwW/9djqTl933UF/cS425i9A==} + dev: false + + /debug/4.3.2: + resolution: {integrity: sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: true + + /duplexer2/0.1.4: + resolution: {integrity: sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=} + dependencies: + readable-stream: 2.3.7 + dev: true + + /emojis-list/3.0.0: + resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==} + engines: {node: '>= 4'} + dev: true + + /esbuild/0.12.15: + resolution: {integrity: sha512-72V4JNd2+48eOVCXx49xoSWHgC3/cCy96e7mbXKY+WOWghN00cCmlGnwVLRhRHorvv0dgCyuMYBZlM2xDM5OQw==} + hasBin: true + requiresBuild: true + dev: true + + /estree-walker/2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + /fast-glob/3.2.7: + resolution: {integrity: sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==} + engines: {node: '>=8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.4 + dev: true + + /fastq/1.11.1: + resolution: {integrity: sha512-HOnr8Mc60eNYl1gzwp6r5RoUyAn5/glBolUzP/Ez6IFVPMPirxn/9phgL6zhOtaTy7ISwPvQ+wT+hfcRZh/bzw==} + dependencies: + reusify: 1.0.4 + dev: true + + /fill-range/7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + dev: true + + /fs.realpath/1.0.0: + resolution: {integrity: sha1-FQStJSMVjKpA20onh8sBQRmU6k8=} + dev: true + + /fsevents/2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + dev: true + optional: true + + /fstream/1.0.12: + resolution: {integrity: sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==} + engines: {node: '>=0.6'} + dependencies: + graceful-fs: 4.2.6 + inherits: 2.0.4 + mkdirp: 0.5.5 + rimraf: 2.7.1 + dev: true + + /function-bind/1.1.1: + resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + dev: true + + /generic-names/2.0.1: + resolution: {integrity: sha512-kPCHWa1m9wGG/OwQpeweTwM/PYiQLrUIxXbt/P4Nic3LbGjCP0YwrALHW1uNLKZ0LIMg+RF+XRlj2ekT9ZlZAQ==} + dependencies: + loader-utils: 1.4.0 + dev: true + + /glob-parent/5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.1 + dev: true + + /glob/7.1.7: + resolution: {integrity: sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.0.4 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: true + + /graceful-fs/4.2.6: + resolution: {integrity: sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==} + dev: true + + /has-flag/4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + dev: true + + /has/1.0.3: + resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} + engines: {node: '>= 0.4.0'} + dependencies: + function-bind: 1.1.1 + dev: true + + /hash-sum/2.0.0: + resolution: {integrity: sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==} + dev: true + + /icss-replace-symbols/1.1.0: + resolution: {integrity: sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=} + dev: true + + /icss-utils/5.1.0_postcss@8.3.5: + resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + dependencies: + postcss: 8.3.5 + dev: true + + /inflight/1.0.6: + resolution: {integrity: sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=} + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + dev: true + + /inherits/2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: true + + /is-core-module/2.5.0: + resolution: {integrity: sha512-TXCMSDsEHMEEZ6eCA8rwRDbLu55MRGmrctljsBX/2v1d9/GzqHOxW5c5oPSgrUt2vBFXebu9rGqckXGPWOlYpg==} + dependencies: + has: 1.0.3 + dev: true + + /is-extglob/2.1.1: + resolution: {integrity: sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=} + engines: {node: '>=0.10.0'} + dev: true + + /is-glob/4.0.1: + resolution: {integrity: sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-number/7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + dev: true + + /isarray/1.0.0: + resolution: {integrity: sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=} + dev: true + + /jiti/1.10.1: + resolution: {integrity: sha512-qux9juDtAC8HlZxAk/fku73ak4TWNLigRFTNzFShE/kw4bXVFsVu538vLXAxvNyPszXgpX4YxkXfwTYEi+zf5A==} + hasBin: true + dev: true + + /json5/1.0.1: + resolution: {integrity: sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==} + hasBin: true + dependencies: + minimist: 1.2.5 + dev: true + + /listenercount/1.0.1: + resolution: {integrity: sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=} + dev: true + + /loader-utils/1.4.0: + resolution: {integrity: sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==} + engines: {node: '>=4.0.0'} + dependencies: + big.js: 5.2.2 + emojis-list: 3.0.0 + json5: 1.0.1 + dev: true + + /lodash.camelcase/4.3.0: + resolution: {integrity: sha1-soqmKIorn8ZRA1x3EfZathkDMaY=} + dev: true + + /lru-cache/5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + dependencies: + yallist: 3.1.1 + dev: true + + /magic-string/0.25.7: + resolution: {integrity: sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==} + dependencies: + sourcemap-codec: 1.4.8 + dev: true + + /merge-source-map/1.1.0: + resolution: {integrity: sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==} + dependencies: + source-map: 0.6.1 + dev: true + + /merge2/1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + dev: true + + /micromatch/4.0.4: + resolution: {integrity: sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.2 + picomatch: 2.3.0 + dev: true + + /minimatch/3.0.4: + resolution: {integrity: sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==} + dependencies: + brace-expansion: 1.1.11 + dev: true + + /minimist/1.2.5: + resolution: {integrity: sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==} + dev: true + + /mkdirp/0.5.5: + resolution: {integrity: sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==} + hasBin: true + dependencies: + minimist: 1.2.5 + dev: true + + /ms/2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: true + + /nanoid/3.1.23: + resolution: {integrity: sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: true + + /once/1.4.0: + resolution: {integrity: sha1-WDsap3WWHUsROsF9nFC6753Xa9E=} + dependencies: + wrappy: 1.0.2 + dev: true + + /path-is-absolute/1.0.1: + resolution: {integrity: sha1-F0uSaHNVNP+8es5r9TpanhtcX18=} + engines: {node: '>=0.10.0'} + dev: true + + /path-parse/1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + dev: true + + /picomatch/2.3.0: + resolution: {integrity: sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==} + engines: {node: '>=8.6'} + dev: true + + /postcss-modules-extract-imports/3.0.0_postcss@8.3.5: + resolution: {integrity: sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + dependencies: + postcss: 8.3.5 + dev: true + + /postcss-modules-local-by-default/4.0.0_postcss@8.3.5: + resolution: {integrity: sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + dependencies: + icss-utils: 5.1.0_postcss@8.3.5 + postcss: 8.3.5 + postcss-selector-parser: 6.0.6 + postcss-value-parser: 4.1.0 + dev: true + + /postcss-modules-scope/3.0.0_postcss@8.3.5: + resolution: {integrity: sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + dependencies: + postcss: 8.3.5 + postcss-selector-parser: 6.0.6 + dev: true + + /postcss-modules-values/4.0.0_postcss@8.3.5: + resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + dependencies: + icss-utils: 5.1.0_postcss@8.3.5 + postcss: 8.3.5 + dev: true + + /postcss-modules/4.1.3_postcss@8.3.5: + resolution: {integrity: sha512-dBT39hrXe4OAVYJe/2ZuIZ9BzYhOe7t+IhedYeQ2OxKwDpAGlkEN/fR0fGnrbx4BvgbMReRX4hCubYK9cE/pJQ==} + peerDependencies: + postcss: ^8.0.0 + dependencies: + generic-names: 2.0.1 + icss-replace-symbols: 1.1.0 + lodash.camelcase: 4.3.0 + postcss: 8.3.5 + postcss-modules-extract-imports: 3.0.0_postcss@8.3.5 + postcss-modules-local-by-default: 4.0.0_postcss@8.3.5 + postcss-modules-scope: 3.0.0_postcss@8.3.5 + postcss-modules-values: 4.0.0_postcss@8.3.5 + string-hash: 1.1.3 + dev: true + + /postcss-selector-parser/6.0.6: + resolution: {integrity: sha512-9LXrvaaX3+mcv5xkg5kFwqSzSH1JIObIx51PrndZwlmznwXRfxMddDvo9gve3gVR8ZTKgoFDdWkbRFmEhT4PMg==} + engines: {node: '>=4'} + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + dev: true + + /postcss-value-parser/4.1.0: + resolution: {integrity: sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==} + dev: true + + /postcss/8.3.5: + resolution: {integrity: sha512-NxTuJocUhYGsMiMFHDUkmjSKT3EdH4/WbGF6GCi1NDGk+vbcUTun4fpbOqaPtD8IIsztA2ilZm2DhYCuyN58gA==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + colorette: 1.2.2 + nanoid: 3.1.23 + source-map-js: 0.6.2 + dev: true + + /process-nextick-args/2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + dev: true + + /queue-microtask/1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + dev: true + + /readable-stream/2.3.7: + resolution: {integrity: sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==} + dependencies: + core-util-is: 1.0.2 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + dev: true + + /resolve/1.20.0: + resolution: {integrity: sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==} + dependencies: + is-core-module: 2.5.0 + path-parse: 1.0.7 + dev: true + + /reusify/1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + dev: true + + /rimraf/2.7.1: + resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + hasBin: true + dependencies: + glob: 7.1.7 + dev: true + + /rollup/2.53.2: + resolution: {integrity: sha512-1CtEYuS5CRCzFZ7SNW5528SlDlk4VDXIRGwbm/2POQxA/G4+7/crIqJwkmnj8Q/74hGx4oVlNvh4E1CJQ5hZ6w==} + engines: {node: '>=10.0.0'} + hasBin: true + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /run-parallel/1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + dev: true + + /safe-buffer/5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + dev: true + + /setimmediate/1.0.5: + resolution: {integrity: sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=} + dev: true + + /source-map-js/0.6.2: + resolution: {integrity: sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==} + engines: {node: '>=0.10.0'} + dev: true + + /source-map/0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + /sourcemap-codec/1.4.8: + resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} + dev: true + + /string-hash/1.1.3: + resolution: {integrity: sha1-6Kr8CsGFW0Zmkp7X3RJ1311sgRs=} + dev: true + + /string_decoder/1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + dependencies: + safe-buffer: 5.1.2 + dev: true + + /supports-color/7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + dev: true + + /to-fast-properties/2.0.0: + resolution: {integrity: sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=} + engines: {node: '>=4'} + + /to-regex-range/5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + dev: true + + /traverse/0.3.9: + resolution: {integrity: sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=} + dev: true + + /typescript/4.3.5: + resolution: {integrity: sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==} + engines: {node: '>=4.2.0'} + hasBin: true + dev: true + + /unzipper/0.10.11: + resolution: {integrity: sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw==} + dependencies: + big-integer: 1.6.48 + binary: 0.3.0 + bluebird: 3.4.7 + buffer-indexof-polyfill: 1.0.2 + duplexer2: 0.1.4 + fstream: 1.0.12 + graceful-fs: 4.2.6 + listenercount: 1.0.1 + readable-stream: 2.3.7 + setimmediate: 1.0.5 + dev: true + + /util-deprecate/1.0.2: + resolution: {integrity: sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=} + dev: true + + /vite-plugin-windicss/1.2.4_vite@2.4.2: + resolution: {integrity: sha512-U+mW8AiPRgC5wbUqjtvEIbZR3LzOwhNU0wnYQueT2SjjTfjlP74vcQg37yrULxycKibpdTYVHZuDuW4QkglPng==} + peerDependencies: + vite: ^2.0.1 + dependencies: + '@windicss/plugin-utils': 1.2.4 + chalk: 4.1.1 + debug: 4.3.2 + vite: 2.4.2 + windicss: 3.1.5 + transitivePeerDependencies: + - supports-color + dev: true + + /vite/2.4.2: + resolution: {integrity: sha512-2MifxD2I9fjyDmmEzbULOo3kOUoqX90A58cT6mECxoVQlMYFuijZsPQBuA14mqSwvV3ydUsqnq+BRWXyO9Qa+w==} + engines: {node: '>=12.0.0'} + hasBin: true + dependencies: + esbuild: 0.12.15 + postcss: 8.3.5 + resolve: 1.20.0 + rollup: 2.53.2 + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /vue-router/4.0.10_vue@3.1.5: + resolution: {integrity: sha512-YbPf6QnZpyyWfnk7CUt2Bme+vo7TLfg1nGZNkvYqKYh4vLaFw6Gn8bPGdmt5m4qrGnKoXLqc4htAsd3dIukICA==} + peerDependencies: + vue: ^3.0.0 + dependencies: + '@vue/devtools-api': 6.0.0-beta.15 + vue: 3.1.5 + dev: false + + /vue-tsc/0.0.24: + resolution: {integrity: sha512-Qx0V7jkWMtvddtaWa1SA8YKkBCRmjq9zZUB2UIMZiso6JSH538oHD2VumSzkoDnAfFbY3t0/j1mB2abpA0bGWA==} + hasBin: true + dependencies: + unzipper: 0.10.11 + dev: true + + /vue/3.1.5: + resolution: {integrity: sha512-Ho7HNb1nfDoO+HVb6qYZgeaobt1XbY6KXFe4HGs1b9X6RhkWG/113n4/SrtM1LUclM6OrP/Se5aPHHvAPG1iVQ==} + dependencies: + '@vue/compiler-dom': 3.1.5 + '@vue/runtime-dom': 3.1.5 + '@vue/shared': 3.1.5 + dev: false + + /windicss/3.1.0: + resolution: {integrity: sha512-z49xITq4X1ltHIZyL4NwFTR2LXPJ0rbOOrhDXfLX+OfG4Au7+GAzqvNlzUfAaIbA8HSpnI04alQHUWH24KfNYA==} + engines: {node: '>= 12'} + hasBin: true + dev: true + + /windicss/3.1.5: + resolution: {integrity: sha512-sGi2YiN6Bp/vWJS0jlpJhMmh7O2TBQ+yTbL6oqoRXz32q1VKELOUMfF+5pdtVL5uoVnfKf4gOUQs2XKNgU30Lw==} + engines: {node: '>= 12'} + hasBin: true + dev: true + + /wrappy/1.0.2: + resolution: {integrity: sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=} + dev: true + + /yallist/3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + dev: true diff --git a/web-new/public/favicon.svg b/web-new/public/favicon.svg new file mode 100644 index 0000000000..1f4eb14f74 --- /dev/null +++ b/web-new/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web-new/src/App.vue b/web-new/src/App.vue new file mode 100644 index 0000000000..2fbdd9645a --- /dev/null +++ b/web-new/src/App.vue @@ -0,0 +1,27 @@ + + + + + diff --git a/web-new/src/assets/logo.svg b/web-new/src/assets/logo.svg new file mode 100644 index 0000000000..b9c8b6c6cd --- /dev/null +++ b/web-new/src/assets/logo.svg @@ -0,0 +1 @@ + diff --git a/web-new/src/components/HelloWorld.vue b/web-new/src/components/HelloWorld.vue new file mode 100644 index 0000000000..8e73cc6d4a --- /dev/null +++ b/web-new/src/components/HelloWorld.vue @@ -0,0 +1,70 @@ + + + + + diff --git a/web-new/src/components/Navbar.vue b/web-new/src/components/Navbar.vue new file mode 100644 index 0000000000..1e715a6f26 --- /dev/null +++ b/web-new/src/components/Navbar.vue @@ -0,0 +1,29 @@ + + + diff --git a/web-new/src/compositions/useApiClient.ts b/web-new/src/compositions/useApiClient.ts new file mode 100644 index 0000000000..7a8d5b6dff --- /dev/null +++ b/web-new/src/compositions/useApiClient.ts @@ -0,0 +1,15 @@ +import WoodpeckerClient from '~/lib/api'; + +let apiClient: WoodpeckerClient | undefined; + +export default (): WoodpeckerClient => { + if (!apiClient) { + const server = 'http://localhost:8000'; + const token = ''; + const csrf = ''; + + apiClient = new WoodpeckerClient(server, token, csrf); + } + + return apiClient; +}; diff --git a/web-new/src/lib/api/client.ts b/web-new/src/lib/api/client.ts new file mode 100644 index 0000000000..7ad5f8e66f --- /dev/null +++ b/web-new/src/lib/api/client.ts @@ -0,0 +1,117 @@ +export type ApiError = { + status: number; + message: string; +}; + +export function encodeQueryString(params: Record): string { + return params + ? Object.keys(params) + .sort() + .map((key) => { + const val = params[key]; + return encodeURIComponent(key) + '=' + encodeURIComponent(val); + }) + .join('&') + : ''; +} + +export default class ApiClient { + server: string; + token: string; + csrf: string; + onerror: (err: ApiError) => void; + + constructor(server: string, token: string, csrf: string) { + this.server = server || ''; + this.token = token; + this.csrf = csrf; + } + + private _request(method: string, path: string, data: unknown): Promise { + var endpoint = `${this.server}${path}`; + var xhr = new XMLHttpRequest(); + xhr.open(method, endpoint, true); + + if (this.token) { + xhr.setRequestHeader('Authorization', 'Bearer ' + this.token); + } + + if (method !== 'GET' && this.csrf) { + xhr.setRequestHeader('X-CSRF-TOKEN', this.csrf); + } + + return new Promise((resolve, reject) => { + xhr.onload = () => { + if (xhr.readyState === 4) { + if (xhr.status >= 300) { + const error: ApiError = { + status: xhr.status, + message: xhr.response, + }; + if (this.onerror) { + this.onerror(error); + } + reject(error); + return; + } + const contentType = xhr.getResponseHeader('Content-Type'); + if (contentType && contentType.startsWith('application/json')) { + resolve(JSON.parse(xhr.response)); + } else { + resolve(xhr.response); + } + } + }; + + xhr.onerror = (e) => { + reject(e); + }; + + if (data) { + xhr.setRequestHeader('Content-Type', 'application/json'); + xhr.send(JSON.stringify(data)); + } else { + xhr.send(); + } + }); + } + + _get(path: string) { + return this._request('GET', path, null); + } + + _post(path: string, data?: unknown) { + return this._request('POST', path, data); + } + + _patch(path: string, data?: unknown) { + return this._request('PATCH', path, data); + } + + _delete(path: string) { + return this._request('DELETE', path, null); + } + + _subscribe(path: string, callback, opts) { + var query = encodeQueryString({ + access_token: this.token, + }); + path = this.server ? this.server + path : path; + path = this.token ? path + '?' + query : path; + + var events = new EventSource(path); + events.onmessage = (event) => { + var data = JSON.parse(event.data); + callback(data); + }; + + if (!opts.reconnect) { + events.onerror = (err) => { + if (err.data === 'eof') { + events.close(); + } + }; + } + return events; + } +} diff --git a/web-new/src/lib/api/index.ts b/web-new/src/lib/api/index.ts new file mode 100644 index 0000000000..4c2e5c38c8 --- /dev/null +++ b/web-new/src/lib/api/index.ts @@ -0,0 +1,116 @@ +import ApiClient, { encodeQueryString } from './client'; +import { Repo } from './types'; + +export default class WoodpeckerClient extends ApiClient { + constructor(server: string, token: string, csrf: string) { + super(server, token, csrf); + } + + getRepoList(opts: Record): Promise { + var query = encodeQueryString(opts); + return this._get('/api/user/repos?' + query); + } + + getRepo(owner: string, repo: string): Promise { + return this._get('/api/repos/' + owner + '/' + repo); + } + + activateRepo(owner: string, repo: string) { + return this._post('/api/repos/' + owner + '/' + repo); + } + + updateRepo(owner: string, repo: string, data) { + return this._patch('/api/repos/' + owner + '/' + repo, data); + } + + deleteRepo(owner: string, repo: string) { + return this._delete('/api/repos/' + owner + '/' + repo); + } + + getBuildList(owner: string, repo: string, opts) { + var query = encodeQueryString(opts); + return this._get('/api/repos/' + owner + '/' + repo + '/builds?' + query); + } + + getBuild(owner: string, repo: string, number: number) { + return this._get('/api/repos/' + owner + '/' + repo + '/builds/' + number); + } + + getBuildFeed(opts) { + var query = encodeQueryString(opts); + return this._get('/api/user/feed?' + query); + } + + cancelBuild(owner: string, repo: string, number: number, ppid: number) { + return this._delete('/api/repos/' + owner + '/' + repo + '/builds/' + number + '/' + ppid); + } + + approveBuild(owner: string, repo: string, build: string) { + return this._post('/api/repos/' + owner + '/' + repo + '/builds/' + build + '/approve'); + } + + declineBuild(owner: string, repo: string, build: string) { + return this._post('/api/repos/' + owner + '/' + repo + '/builds/' + build + '/decline'); + } + + restartBuild(owner: string, repo: string, build: string, opts) { + var query = encodeQueryString(opts); + return this._post('/api/repos/' + owner + '/' + repo + '/builds/' + build + '?' + query); + } + + getLogs(owner: string, repo: string, build: string, proc: string) { + return this._get('/api/repos/' + owner + '/' + repo + '/logs/' + build + '/' + proc); + } + + getArtifact(owner: string, repo: string, build: string, proc: string, file: string) { + return this._get('/api/repos/' + owner + '/' + repo + '/files/' + build + '/' + proc + '/' + file + '?raw=true'); + } + + getArtifactList(owner: string, repo: string, build: string) { + return this._get('/api/repos/' + owner + '/' + repo + '/files/' + build); + } + + getSecretList(owner: string, repo: string) { + return this._get('/api/repos/' + owner + '/' + repo + '/secrets'); + } + + createSecret(owner: string, repo: string, secret: string) { + return this._post('/api/repos/' + owner + '/' + repo + '/secrets', secret); + } + + deleteSecret(owner: string, repo: string, secret: string) { + return this._delete('/api/repos/' + owner + '/' + repo + '/secrets/' + secret); + } + + getRegistryList(owner: string, repo: string) { + return this._get('/api/repos/' + owner + '/' + repo + '/registry'); + } + + createRegistry(owner: string, repo: string, registry: string) { + return this._post('/api/repos/' + owner + '/' + repo + '/registry', registry); + } + + deleteRegistry(owner: string, repo: string, address: string) { + return this._delete('/api/repos/' + owner + '/' + repo + '/registry/' + address); + } + + getSelf() { + return this._get('/api/user'); + } + + getToken() { + return this._post('/api/user/token'); + } + + on(callback) { + return this._subscribe('/stream/events', callback, { + reconnect: true, + }); + } + + stream(owner: string, repo: string, build: string, proc: string, callback) { + return this._subscribe('/stream/logs/' + owner + '/' + repo + '/' + build + '/' + proc, callback, { + reconnect: false, + }); + } +} diff --git a/web-new/src/lib/api/types/index.ts b/web-new/src/lib/api/types/index.ts new file mode 100644 index 0000000000..f6b2da600e --- /dev/null +++ b/web-new/src/lib/api/types/index.ts @@ -0,0 +1,2 @@ +export * from './repo'; +export * from './user'; diff --git a/web-new/src/lib/api/types/repo.ts b/web-new/src/lib/api/types/repo.ts new file mode 100644 index 0000000000..9205961694 --- /dev/null +++ b/web-new/src/lib/api/types/repo.ts @@ -0,0 +1,55 @@ +// A version control repository. +export type Repo = { + id: number; + // The unique identifier for the repository. + + scm: string; + // The source control management being used. + // Currently this is either 'git' or 'hg' (Mercurial). + + owner: string; + // The owner of the repository. + + name: string; + // The name of the repository. + + full_name: string; + // The full name of the repository. + // This is created from the owner and name of the repository. + + avatar_url: string; + // The url for the avatar image. + + link_url: string; + // The link to view the repository. + + clone_url: string; + // The url used to clone the repository. + + default_branch: string; + // The default branch of the repository. + + private: boolean; + // Whether the repository is publicly visible. + + trusted: boolean; + // Whether the repository has trusted access for builds. + // If the repository is trusted then the host network can be used and + // volumes can be created. + + timeout: number; + // x-dart-type: Duration + // The amount of time in minutes before the build is killed. + + allow_pr: boolean; + // Whether pull requests should trigger a build. + + allow_push: boolean; + // Whether push events should trigger a build. + + allow_deploys: boolean; + // Whether deployment events should trigger a build. + + allow_tags: boolean; + // Whether tags should trigger a build. +}; diff --git a/web-new/src/lib/api/types/user.ts b/web-new/src/lib/api/types/user.ts new file mode 100644 index 0000000000..c4eab242d2 --- /dev/null +++ b/web-new/src/lib/api/types/user.ts @@ -0,0 +1,20 @@ +// The user account. +export type User = { + id: number; + // The unique identifier for the account. + + login: string; + // The login name for the account. + + email: string; + // The email address for the account. + + avatar_url: string; + // The url for the avatar image. + + admin: boolean; + // Whether the account has administrative priviledges. + + active: boolean; + // Whether the account is currently active. +}; diff --git a/web-new/src/main.ts b/web-new/src/main.ts new file mode 100644 index 0000000000..0a370a00e0 --- /dev/null +++ b/web-new/src/main.ts @@ -0,0 +1,11 @@ +import 'windi.css'; + +import { createApp } from 'vue'; + +import App from '~/App.vue'; +import router from '~/router'; + +const app = createApp(App); + +app.use(router); +app.mount('#app'); diff --git a/web-new/src/router.ts b/web-new/src/router.ts new file mode 100644 index 0000000000..8a6bbcccd4 --- /dev/null +++ b/web-new/src/router.ts @@ -0,0 +1,43 @@ +import { Component } from 'vue'; +import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'; + +const routes: RouteRecordRaw[] = [ + { + path: '/', + name: 'home', + component: (): Component => import('~/views/Home.vue'), + }, + { + path: '/projects', + name: 'projects', + component: (): Component => import('~/views/Home.vue'), + meta: { authentication: 'required' }, + }, + { + path: '/do-login/:origin?', + name: 'login', + component: (): Component => import('~/views/Login.vue'), + props: true, + }, + { + path: '/:pathMatch(.*)*', + name: 'not-found', + component: (): Component => import('~/views/NotFound.vue'), + }, +]; + +const router = createRouter({ + history: createWebHistory(), + routes, +}); + +router.beforeEach(async (to, _, next) => { + if (to.meta.authentication === 'required') { + next({ name: 'login', params: { origin: to.fullPath } }); + return; + } + + next(); +}); + +export default router; diff --git a/web-new/src/shims-vue.d.ts b/web-new/src/shims-vue.d.ts new file mode 100644 index 0000000000..c458ab5a39 --- /dev/null +++ b/web-new/src/shims-vue.d.ts @@ -0,0 +1,6 @@ +declare module '*.vue' { + import { DefineComponent } from 'vue' + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types + const component: DefineComponent<{}, {}, any> + export default component +} diff --git a/web-new/src/views/Home.vue b/web-new/src/views/Home.vue new file mode 100644 index 0000000000..0b938e68f2 --- /dev/null +++ b/web-new/src/views/Home.vue @@ -0,0 +1,11 @@ + + + diff --git a/web-new/src/views/Login.vue b/web-new/src/views/Login.vue new file mode 100644 index 0000000000..8cd801ddd5 --- /dev/null +++ b/web-new/src/views/Login.vue @@ -0,0 +1,28 @@ + + + diff --git a/web-new/src/views/NotFound.vue b/web-new/src/views/NotFound.vue new file mode 100644 index 0000000000..1ac2d494e4 --- /dev/null +++ b/web-new/src/views/NotFound.vue @@ -0,0 +1,14 @@ + + + diff --git a/web-new/src/vite-env.d.ts b/web-new/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/web-new/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/web-new/tsconfig.json b/web-new/tsconfig.json new file mode 100644 index 0000000000..f36acdc04f --- /dev/null +++ b/web-new/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "moduleResolution": "node", + "strict": true, + "jsx": "preserve", + "sourceMap": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "lib": ["esnext", "dom"], + "paths": { + "~/*": ["./src/*"] + } + }, + "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"] +} diff --git a/web-new/vite.config.ts b/web-new/vite.config.ts new file mode 100644 index 0000000000..05906068dc --- /dev/null +++ b/web-new/vite.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from 'vite'; +import path from 'path'; +import vue from '@vitejs/plugin-vue'; +import WindiCSS from 'vite-plugin-windicss'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [vue(), WindiCSS()], + resolve: { + alias: { + '~/': `${path.resolve(__dirname, 'src')}/`, + }, + }, + server: { + proxy: { + '/api': { + target: 'http://localhost:4000', + ws: true, + changeOrigin: true, + }, + }, + }, +}); diff --git a/web-new/windi.config.ts b/web-new/windi.config.ts new file mode 100644 index 0000000000..84377135b6 --- /dev/null +++ b/web-new/windi.config.ts @@ -0,0 +1,13 @@ +import colors from 'windicss/colors'; +import { defineConfig } from 'windicss/helpers'; +import typography from 'windicss/plugin/typography'; + +export default defineConfig({ + darkMode: 'class', + theme: { + colors: { + green: '#4caf50', + }, + }, + plugins: [typography], +}); From a52ec0637047d8c83780bb150b0aedd389a03aca Mon Sep 17 00:00:00 2001 From: Anton Bracke Date: Sat, 17 Jul 2021 18:51:04 +0200 Subject: [PATCH 003/143] revert: undo changes to web --- web/src/config/client/inject.js | 36 +++ web/src/index.js | 14 + web/src/screens/drone.js | 52 ++++ web/src/screens/feed/components/index.js | 3 + web/src/screens/feed/components/list.js | 55 ++++ web/src/screens/feed/index.js | 192 +++++++++++++ web/src/screens/layout.js | 224 +++++++++++++++ web/src/screens/login/screens/error/index.js | 34 +++ web/src/screens/login/screens/form/index.js | 21 ++ web/src/screens/login/screens/index.js | 4 + web/src/screens/redirect.js | 41 +++ .../repo/screens/build/components/approval.js | 10 + .../repo/screens/build/components/details.js | 47 ++++ .../repo/screens/build/components/elapsed.js | 63 +++++ .../repo/screens/build/components/index.js | 5 + .../repo/screens/build/components/procs.js | 100 +++++++ web/src/screens/repo/screens/build/index.js | 257 ++++++++++++++++++ .../screens/build/logs/components/anchor.js | 15 + .../screens/build/logs/components/term.js | 93 +++++++ .../screens/repo/screens/build/logs/index.js | 143 ++++++++++ web/src/screens/repo/screens/build/menu.js | 78 ++++++ .../repo/screens/builds/components/index.js | 3 + .../repo/screens/builds/components/list.js | 55 ++++ web/src/screens/repo/screens/builds/header.js | 20 ++ web/src/screens/repo/screens/builds/index.js | 153 +++++++++++ web/src/screens/repo/screens/builds/menu.js | 15 + .../repo/screens/registry/components/form.js | 80 ++++++ .../repo/screens/registry/components/index.js | 4 + .../repo/screens/registry/components/list.js | 15 + .../screens/repo/screens/registry/index.js | 103 +++++++ .../repo/screens/secrets/components/form.js | 140 ++++++++++ .../repo/screens/secrets/components/index.js | 4 + .../repo/screens/secrets/components/list.js | 20 ++ web/src/screens/repo/screens/secrets/index.js | 99 +++++++ .../screens/repo/screens/settings/index.js | 244 +++++++++++++++++ web/src/screens/titles.js | 29 ++ .../user/screens/repos/components/index.js | 3 + .../user/screens/repos/components/list.js | 40 +++ .../user/screens/repos/components/switch.js | 13 + web/src/screens/user/screens/tokens/index.js | 59 ++++ web/src/shared/components/avatar.js | 12 + web/src/shared/components/breadcrumb.js | 21 ++ web/src/shared/components/build_event.js | 73 +++++ web/src/shared/components/build_time.js | 37 +++ web/src/shared/components/drawer/drawer.js | 62 +++++ web/src/shared/components/duration.js | 10 + web/src/shared/components/icons/back.js | 17 ++ web/src/shared/components/icons/branch.js | 11 + web/src/shared/components/icons/check.js | 17 ++ web/src/shared/components/icons/clock.js | 17 ++ web/src/shared/components/icons/close.js | 17 ++ web/src/shared/components/icons/commit.js | 16 ++ web/src/shared/components/icons/deploy.js | 11 + web/src/shared/components/icons/expand.js | 17 ++ web/src/shared/components/icons/index.js | 45 +++ web/src/shared/components/icons/launch.js | 12 + web/src/shared/components/icons/link.js | 17 ++ web/src/shared/components/icons/menu.js | 17 ++ web/src/shared/components/icons/merge.js | 16 ++ web/src/shared/components/icons/pause.js | 17 ++ web/src/shared/components/icons/play.js | 17 ++ web/src/shared/components/icons/refresh.js | 17 ++ web/src/shared/components/icons/remove.js | 17 ++ web/src/shared/components/icons/report.js | 12 + web/src/shared/components/icons/schedule.js | 18 ++ web/src/shared/components/icons/star.js | 20 ++ web/src/shared/components/icons/sync.js | 12 + web/src/shared/components/icons/tag.js | 11 + web/src/shared/components/icons/timelapse.js | 17 ++ web/src/shared/components/logo.js | 17 ++ web/src/shared/components/menu.js | 32 +++ web/src/shared/components/snackbar.js | 47 ++++ web/src/shared/components/status.js | 100 +++++++ web/src/shared/components/status_number.js | 12 + web/src/shared/components/sync.js | 16 ++ 75 files changed, 3413 insertions(+) create mode 100644 web/src/config/client/inject.js create mode 100644 web/src/index.js create mode 100644 web/src/screens/drone.js create mode 100644 web/src/screens/feed/components/index.js create mode 100644 web/src/screens/feed/components/list.js create mode 100644 web/src/screens/feed/index.js create mode 100644 web/src/screens/layout.js create mode 100644 web/src/screens/login/screens/error/index.js create mode 100644 web/src/screens/login/screens/form/index.js create mode 100644 web/src/screens/login/screens/index.js create mode 100644 web/src/screens/redirect.js create mode 100644 web/src/screens/repo/screens/build/components/approval.js create mode 100644 web/src/screens/repo/screens/build/components/details.js create mode 100644 web/src/screens/repo/screens/build/components/elapsed.js create mode 100644 web/src/screens/repo/screens/build/components/index.js create mode 100644 web/src/screens/repo/screens/build/components/procs.js create mode 100644 web/src/screens/repo/screens/build/index.js create mode 100644 web/src/screens/repo/screens/build/logs/components/anchor.js create mode 100644 web/src/screens/repo/screens/build/logs/components/term.js create mode 100644 web/src/screens/repo/screens/build/logs/index.js create mode 100644 web/src/screens/repo/screens/build/menu.js create mode 100644 web/src/screens/repo/screens/builds/components/index.js create mode 100644 web/src/screens/repo/screens/builds/components/list.js create mode 100644 web/src/screens/repo/screens/builds/header.js create mode 100644 web/src/screens/repo/screens/builds/index.js create mode 100644 web/src/screens/repo/screens/builds/menu.js create mode 100644 web/src/screens/repo/screens/registry/components/form.js create mode 100644 web/src/screens/repo/screens/registry/components/index.js create mode 100644 web/src/screens/repo/screens/registry/components/list.js create mode 100644 web/src/screens/repo/screens/registry/index.js create mode 100644 web/src/screens/repo/screens/secrets/components/form.js create mode 100644 web/src/screens/repo/screens/secrets/components/index.js create mode 100644 web/src/screens/repo/screens/secrets/components/list.js create mode 100644 web/src/screens/repo/screens/secrets/index.js create mode 100644 web/src/screens/repo/screens/settings/index.js create mode 100644 web/src/screens/titles.js create mode 100644 web/src/screens/user/screens/repos/components/index.js create mode 100644 web/src/screens/user/screens/repos/components/list.js create mode 100644 web/src/screens/user/screens/repos/components/switch.js create mode 100644 web/src/screens/user/screens/tokens/index.js create mode 100644 web/src/shared/components/avatar.js create mode 100644 web/src/shared/components/breadcrumb.js create mode 100644 web/src/shared/components/build_event.js create mode 100644 web/src/shared/components/build_time.js create mode 100644 web/src/shared/components/drawer/drawer.js create mode 100644 web/src/shared/components/duration.js create mode 100644 web/src/shared/components/icons/back.js create mode 100644 web/src/shared/components/icons/branch.js create mode 100644 web/src/shared/components/icons/check.js create mode 100644 web/src/shared/components/icons/clock.js create mode 100644 web/src/shared/components/icons/close.js create mode 100644 web/src/shared/components/icons/commit.js create mode 100644 web/src/shared/components/icons/deploy.js create mode 100644 web/src/shared/components/icons/expand.js create mode 100644 web/src/shared/components/icons/index.js create mode 100644 web/src/shared/components/icons/launch.js create mode 100644 web/src/shared/components/icons/link.js create mode 100644 web/src/shared/components/icons/menu.js create mode 100644 web/src/shared/components/icons/merge.js create mode 100644 web/src/shared/components/icons/pause.js create mode 100644 web/src/shared/components/icons/play.js create mode 100644 web/src/shared/components/icons/refresh.js create mode 100644 web/src/shared/components/icons/remove.js create mode 100644 web/src/shared/components/icons/report.js create mode 100644 web/src/shared/components/icons/schedule.js create mode 100644 web/src/shared/components/icons/star.js create mode 100644 web/src/shared/components/icons/sync.js create mode 100644 web/src/shared/components/icons/tag.js create mode 100644 web/src/shared/components/icons/timelapse.js create mode 100644 web/src/shared/components/logo.js create mode 100644 web/src/shared/components/menu.js create mode 100644 web/src/shared/components/snackbar.js create mode 100644 web/src/shared/components/status.js create mode 100644 web/src/shared/components/status_number.js create mode 100644 web/src/shared/components/sync.js diff --git a/web/src/config/client/inject.js b/web/src/config/client/inject.js new file mode 100644 index 0000000000..e5f3f95132 --- /dev/null +++ b/web/src/config/client/inject.js @@ -0,0 +1,36 @@ +import React from "react"; + +export const drone = (client, Component) => { + // @see https://github.com/yannickcr/eslint-plugin-react/issues/512 + // eslint-disable-next-line react/display-name + const component = class extends React.Component { + getChildContext() { + return { + drone: client, + }; + } + + render() { + return ; + } + }; + + component.childContextTypes = { + drone: (props, propName) => {}, + }; + + return component; +}; + +export const inject = Component => { + // @see https://github.com/yannickcr/eslint-plugin-react/issues/512 + // eslint-disable-next-line react/display-name + const component = class extends React.Component { + render() { + this.props.drone = this.context.drone; + return ; + } + }; + + return component; +}; diff --git a/web/src/index.js b/web/src/index.js new file mode 100644 index 0000000000..92a72f0232 --- /dev/null +++ b/web/src/index.js @@ -0,0 +1,14 @@ +import "babel-polyfill"; +import React from "react"; +import { render } from "react-dom"; + +let root; + +function init() { + let App = require("./screens/drone").default; + root = render(, document.body, root); +} + +init(); + +if (module.hot) module.hot.accept("./screens/drone", init); diff --git a/web/src/screens/drone.js b/web/src/screens/drone.js new file mode 100644 index 0000000000..574848f08a --- /dev/null +++ b/web/src/screens/drone.js @@ -0,0 +1,52 @@ +import React, { Component } from "react"; + +import { root } from "baobab-react/higher-order"; +import tree from "config/state"; +import client from "config/client"; +import { drone } from "config/client/inject"; +import { LoginForm, LoginError } from "screens/login/screens"; +import Title from "./titles"; +import Layout from "./layout"; +import RedirectRoot from "./redirect"; +import { fetchFeedOnce, subscribeToFeedOnce } from "shared/utils/feed"; + +import { BrowserRouter, Route, Switch } from "react-router-dom"; + +// eslint-disable-next-line no-unused-vars +import styles from "./drone.less"; + +if (module.hot) { + require("preact/devtools"); +} + +class App extends Component { + render() { + return ( + +
+ + <Switch> + <Route path="/" exact={true} component={RedirectRoot} /> + <Route path="/login/form" exact={true} component={LoginForm} /> + <Route path="/login/error" exact={true} component={LoginError} /> + <Route path="/" exact={false} component={Layout} /> + </Switch> + </div> + </BrowserRouter> + ); + } +} + +if (tree.exists(["user", "data"])) { + fetchFeedOnce(tree, client); + subscribeToFeedOnce(tree, client); +} + +client.onerror = error => { + console.error(error); + if (error.status === 401) { + tree.unset(["user", "data"]); + } +}; + +export default root(tree, drone(client, App)); diff --git a/web/src/screens/feed/components/index.js b/web/src/screens/feed/components/index.js new file mode 100644 index 0000000000..4ad6b4fdab --- /dev/null +++ b/web/src/screens/feed/components/index.js @@ -0,0 +1,3 @@ +import { List, Item } from "./list"; + +export { List, Item }; diff --git a/web/src/screens/feed/components/list.js b/web/src/screens/feed/components/list.js new file mode 100644 index 0000000000..4f7ab5e03f --- /dev/null +++ b/web/src/screens/feed/components/list.js @@ -0,0 +1,55 @@ +import React, { Component } from "react"; + +import Status from "shared/components/status"; +import BuildTime from "shared/components/build_time"; + +import styles from "./list.less"; + +import { StarIcon } from "shared/components/icons/index"; + +export const List = ({ children }) => ( + <div className={styles.list}>{children}</div> +); + +export class Item extends Component { + constructor(props) { + super(props); + + this.handleFave = this.handleFave.bind(this); + } + + handleFave(e) { + e.preventDefault(); + this.props.onFave(this.props.item.full_name); + } + + render() { + const { item, faved } = this.props; + return ( + <div className={styles.item}> + <div onClick={this.handleFave}> + <StarIcon filled={faved} size={16} className={styles.star} /> + </div> + <div className={styles.header}> + <div className={styles.title}>{item.full_name}</div> + <div className={styles.icon}> + {item.status ? <Status status={item.status} /> : <noscript />} + </div> + </div> + + <div className={styles.body}> + <BuildTime + start={item.started_at || item.created_at} + finish={item.finished_at} + /> + </div> + </div> + ); + } + + shouldComponentUpdate(nextProps, nextState) { + return ( + this.props.item !== nextProps.item || this.props.faved !== nextProps.faved + ); + } +} diff --git a/web/src/screens/feed/index.js b/web/src/screens/feed/index.js new file mode 100644 index 0000000000..70891049c5 --- /dev/null +++ b/web/src/screens/feed/index.js @@ -0,0 +1,192 @@ +import React, { Component } from "react"; +import { Link } from "react-router-dom"; + +import { compareFeedItem } from "shared/utils/feed"; + +import { branch } from "baobab-react/higher-order"; +import { inject } from "config/client/inject"; + +import DroneIcon from "shared/components/logo"; +import { List, Item } from "./components"; + +import style from "./index.less"; + +import Collapsible from "react-collapsible"; + +const binding = (props, context) => { + return { feed: ["feed"] }; +}; + +@inject +@branch(binding) +export default class Sidebar extends Component { + constructor(props, context) { + super(props, context); + + this.setState({ + starred: JSON.parse(localStorage.getItem("starred") || "[]"), + starredOpen: (localStorage.getItem("starredOpen") || "true") === "true", + reposOpen: (localStorage.getItem("reposOpen") || "true") === "true", + }); + + this.handleFilter = this.handleFilter.bind(this); + this.toggleStarred = this.toggleItem.bind(this, "starredOpen"); + this.toggleAll = this.toggleItem.bind(this, "reposOpen"); + } + + shouldComponentUpdate(nextProps, nextState) { + return ( + this.props.feed !== nextProps.feed || + this.state.filter !== nextState.filter || + this.state.starred.length !== nextState.starred.length + ); + } + + handleFilter(e) { + this.setState({ + filter: e.target.value, + }); + } + + toggleItem = item => { + this.setState(state => { + return { [item]: !state[item] }; + }); + + localStorage.setItem(item, this.state[item]); + }; + + renderFeed = (list, renderStarred) => { + return ( + <div> + <List>{list.map(item => this.renderItem(item, renderStarred))}</List> + </div> + ); + }; + + renderItem = (item, renderStarred) => { + const starred = this.state.starred; + if (renderStarred && !starred.includes(item.full_name)) { + return null; + } + return ( + <Link to={`/${item.full_name}`} key={item.full_name}> + <Item + item={item} + onFave={this.onFave} + faved={starred.includes(item.full_name)} + /> + </Link> + ); + }; + + onFave = fullName => { + if (!this.state.starred.includes(fullName)) { + this.setState(state => { + const list = state.starred.concat(fullName); + return { starred: list }; + }); + } else { + this.setState(state => { + const list = state.starred.filter(v => v !== fullName); + return { starred: list }; + }); + } + + localStorage.setItem("starred", JSON.stringify(this.state.starred)); + }; + + render() { + const { feed } = this.props; + const { filter } = this.state; + + const list = feed.data ? Object.values(feed.data) : []; + + const filterFunc = item => { + return !filter || item.full_name.indexOf(filter) !== -1; + }; + + const filtered = list.filter(filterFunc).sort(compareFeedItem); + const starredOpen = this.state.starredOpen; + const reposOpen = this.state.reposOpen; + return ( + <div className={style.feed}> + {LOGO} + <Collapsible + trigger="Starred" + triggerTagName="div" + transitionTime={200} + open={starredOpen} + onOpen={this.toggleStarred} + onClose={this.toggleStarred} + triggerOpenedClassName={style.Collapsible__trigger} + triggerClassName={style.Collapsible__trigger} + > + {feed.loaded === false ? ( + LOADING + ) : feed.error ? ( + ERROR + ) : list.length === 0 ? ( + EMPTY + ) : ( + this.renderFeed(list, true) + )} + </Collapsible> + <Collapsible + trigger="Repos" + triggerTagName="div" + transitionTime={200} + open={reposOpen} + onOpen={this.toggleAll} + onClose={this.toggleAll} + triggerOpenedClassName={style.Collapsible__trigger} + triggerClassName={style.Collapsible__trigger} + > + <input + type="text" + placeholder="Search …" + onChange={this.handleFilter} + /> + {feed.loaded === false ? ( + LOADING + ) : feed.error ? ( + ERROR + ) : list.length === 0 ? ( + EMPTY + ) : filtered.length > 0 ? ( + this.renderFeed(filtered.sort(compareFeedItem), false) + ) : ( + NO_MATCHES + )} + </Collapsible> + </div> + ); + } +} + +const LOGO = ( + <div className={style.brand}> + <DroneIcon /> + <p> + Woodpecker<span style="margin-left: 4px;">{window.DRONE_VERSION}</span> + <br /> + <span> + <a href={window.DRONE_DOCS} target="_blank" rel="noopener noreferrer"> + Docs + </a> + </span> + </p> + </div> +); + +const LOADING = <div className={style.message}>Loading</div>; + +const EMPTY = <div className={style.message}>Your build feed is empty</div>; + +const NO_MATCHES = <div className={style.message}>No results found</div>; + +const ERROR = ( + <div className={style.message}> + Oops. It looks like there was a problem loading your feed + </div> +); diff --git a/web/src/screens/layout.js b/web/src/screens/layout.js new file mode 100644 index 0000000000..785792ef00 --- /dev/null +++ b/web/src/screens/layout.js @@ -0,0 +1,224 @@ +import React, { Component } from "react"; +import classnames from "classnames"; +import { Route, Switch, Link } from "react-router-dom"; +import { connectScreenSize } from "react-screen-size"; + +import { branch } from "baobab-react/higher-order"; +import { inject } from "config/client/inject"; + +import MenuIcon from "shared/components/icons/menu"; + +import Feed from "screens/feed"; +import RepoRegistry from "screens/repo/screens/registry"; +import RepoSecrets from "screens/repo/screens/secrets"; +import RepoSettings from "screens/repo/screens/settings"; +import RepoBuilds from "screens/repo/screens/builds"; +import UserRepos, { UserRepoTitle } from "screens/user/screens/repos"; +import UserTokens from "screens/user/screens/tokens"; +import RedirectRoot from "./redirect"; + +import RepoHeader from "screens/repo/screens/builds/header"; + +import UserReposMenu from "screens/user/screens/repos/menu"; +import BuildLogs, { BuildLogsTitle } from "screens/repo/screens/build"; +import BuildMenu from "screens/repo/screens/build/menu"; +import RepoMenu from "screens/repo/screens/builds/menu"; + +import { Snackbar } from "shared/components/snackbar"; +import { Drawer, DOCK_RIGHT } from "shared/components/drawer/drawer"; + +import styles from "./layout.less"; + +const binding = (props, context) => { + return { + user: ["user"], + message: ["message"], + sidebar: ["sidebar"], + menu: ["menu"], + }; +}; + +const mapScreenSizeToProps = screenSize => { + return { + isTablet: screenSize["small"], + isMobile: screenSize["mobile"], + isDesktop: screenSize["> small"], + }; +}; + +@inject +@branch(binding) +@connectScreenSize(mapScreenSizeToProps) +export default class Default extends Component { + constructor(props, context) { + super(props, context); + this.state = { + menu: false, + feed: false, + }; + + this.openMenu = this.openMenu.bind(this); + this.closeMenu = this.closeMenu.bind(this); + this.closeSnackbar = this.closeSnackbar.bind(this); + } + + componentWillReceiveProps(nextProps) { + if (nextProps.location !== this.props.location) { + this.closeMenu(true); + } + } + + openMenu() { + this.props.dispatch(tree => { + tree.set(["menu"], true); + }); + } + + closeMenu() { + this.props.dispatch(tree => { + tree.set(["menu"], false); + }); + } + + render() { + const { user, message, menu } = this.props; + + const classes = classnames(!user || !user.data ? styles.guest : null); + return ( + <div className={classes}> + <div className={styles.left}> + <Switch> + <Route path={"/"} component={Feed} /> + </Switch> + </div> + <div className={styles.center}> + {!user || !user.data ? ( + <a + href={"/login?url=" + window.location.href} + target="_self" + className={styles.login} + > + Click to Login + </a> + ) : ( + <noscript /> + )} + <div className={styles.title}> + <Switch> + <Route path="/account/repos" component={UserRepoTitle} /> + <Route + path="/:owner/:repo/:build(\d*)/:proc(\d*)" + exact={true} + component={BuildLogsTitle} + /> + <Route + path="/:owner/:repo/:build(\d*)" + component={BuildLogsTitle} + /> + <Route path="/:owner/:repo" component={RepoHeader} /> + </Switch> + {user && user.data ? ( + <div className={styles.avatar}> + <img src={user.data.avatar_url} /> + </div> + ) : ( + undefined + )} + {user && user.data ? ( + <button onClick={this.openMenu}> + <MenuIcon /> + </button> + ) : ( + <noscript /> + )} + </div> + + <div className={styles.menu}> + <Switch> + <Route + path="/account/repos" + exact={true} + component={UserReposMenu} + /> + <Route path="/account/" exact={false} component={undefined} /> + BuildMenu + <Route + path="/:owner/:repo/:build(\d*)/:proc(\d*)" + exact={true} + component={BuildMenu} + /> + <Route + path="/:owner/:repo/:build(\d*)" + exact={true} + component={BuildMenu} + /> + <Route path="/:owner/:repo" exact={false} component={RepoMenu} /> + </Switch> + </div> + + <Switch> + <Route path="/account/token" exact={true} component={UserTokens} /> + <Route path="/account/repos" exact={true} component={UserRepos} /> + <Route + path="/:owner/:repo/settings/secrets" + exact={true} + component={RepoSecrets} + /> + <Route + path="/:owner/:repo/settings/registry" + exact={true} + component={RepoRegistry} + /> + <Route + path="/:owner/:repo/settings" + exact={true} + component={RepoSettings} + /> + <Route + path="/:owner/:repo/:build(\d*)" + exact={true} + component={BuildLogs} + /> + <Route + path="/:owner/:repo/:build(\d*)/:proc(\d*)" + exact={true} + component={BuildLogs} + /> + <Route path="/:owner/:repo" exact={true} component={RepoBuilds} /> + <Route path="/" exact={true} component={RedirectRoot} /> + </Switch> + </div> + + <Snackbar message={message.text} onClose={this.closeSnackbar} /> + + <Drawer onClick={this.closeMenu} position={DOCK_RIGHT} open={menu}> + <section> + <ul> + <li> + <Link to="/account/repos">Repositories</Link> + </li> + <li> + <Link to="/account/token">Token</Link> + </li> + </ul> + </section> + <section> + <ul> + <li> + <a href="/logout" target="_self"> + Logout + </a> + </li> + </ul> + </section> + </Drawer> + </div> + ); + } + + closeSnackbar() { + this.props.dispatch(tree => { + tree.unset(["message", "text"]); + }); + } +} diff --git a/web/src/screens/login/screens/error/index.js b/web/src/screens/login/screens/error/index.js new file mode 100644 index 0000000000..d7a65bb97f --- /dev/null +++ b/web/src/screens/login/screens/error/index.js @@ -0,0 +1,34 @@ +import React, { Component } from "react"; +import queryString from "query-string"; +import Icon from "shared/components/icons/report"; + +import styles from "./index.less"; + +const DEFAULT_ERROR = "The system failed to process your Login request."; + +class Error extends Component { + render() { + const parsed = queryString.parse(window.location.search); + let error = DEFAULT_ERROR; + + switch (parsed.code || parsed.error) { + case "oauth_error": + break; + case "access_denied": + break; + } + + return ( + <div className={styles.root}> + <div className={styles.alert}> + <div> + <Icon /> + </div> + <div>{error}</div> + </div> + </div> + ); + } +} + +export default Error; diff --git a/web/src/screens/login/screens/form/index.js b/web/src/screens/login/screens/form/index.js new file mode 100644 index 0000000000..7434ccdb65 --- /dev/null +++ b/web/src/screens/login/screens/form/index.js @@ -0,0 +1,21 @@ +import React from "react"; + +import styles from "./index.less"; + +const LoginForm = props => ( + <div className={styles.login}> + <form method="post" action="/authorize"> + <p>Login with your version control system username and password.</p> + <input + placeholder="Username" + name="username" + type="text" + spellCheck="false" + /> + <input placeholder="Password" name="password" type="password" /> + <input value="Login" type="submit" /> + </form> + </div> +); + +export default LoginForm; diff --git a/web/src/screens/login/screens/index.js b/web/src/screens/login/screens/index.js new file mode 100644 index 0000000000..07b139db48 --- /dev/null +++ b/web/src/screens/login/screens/index.js @@ -0,0 +1,4 @@ +import LoginForm from "./form"; +import LoginError from "./error"; + +export { LoginForm, LoginError }; diff --git a/web/src/screens/redirect.js b/web/src/screens/redirect.js new file mode 100644 index 0000000000..1b36f7d38e --- /dev/null +++ b/web/src/screens/redirect.js @@ -0,0 +1,41 @@ +import React, { Component } from "react"; +import { Redirect } from "react-router-dom"; +import { branch } from "baobab-react/higher-order"; +import { Message } from "shared/components/sync"; + +const binding = (props, context) => { + return { + feed: ["feed"], + user: ["user", "data"], + syncing: ["user", "syncing"], + }; +}; + +@branch(binding) +export default class RedirectRoot extends Component { + componentWillReceiveProps(nextProps) { + const { user } = nextProps; + if (!user && window) { + window.location.href = "/login?url=" + window.location.href; + } + } + + render() { + const { user, syncing } = this.props; + const { latest, loaded } = this.props.feed; + + return !loaded && syncing ? ( + <Message /> + ) : !loaded ? ( + undefined + ) : !user ? ( + undefined + ) : !latest ? ( + <Redirect to="/account/repos" /> + ) : !latest.number ? ( + <Redirect to={`/${latest.full_name}`} /> + ) : ( + <Redirect to={`/${latest.full_name}/${latest.number}`} /> + ); + } +} diff --git a/web/src/screens/repo/screens/build/components/approval.js b/web/src/screens/repo/screens/build/components/approval.js new file mode 100644 index 0000000000..9936cfc4b5 --- /dev/null +++ b/web/src/screens/repo/screens/build/components/approval.js @@ -0,0 +1,10 @@ +import React from "react"; +import style from "./approval.less"; + +export const Approval = ({ onapprove, ondecline }) => ( + <div className={style.root}> + <p>Pipeline execution is blocked pending administrator approval</p> + <button onClick={onapprove}>Approve</button> + <button onClick={ondecline}>Decline</button> + </div> +); diff --git a/web/src/screens/repo/screens/build/components/details.js b/web/src/screens/repo/screens/build/components/details.js new file mode 100644 index 0000000000..ff137d1674 --- /dev/null +++ b/web/src/screens/repo/screens/build/components/details.js @@ -0,0 +1,47 @@ +import React, { Component } from "react"; + +import BuildMeta from "shared/components/build_event"; +import BuildTime from "shared/components/build_time"; +import { StatusLabel } from "shared/components/status"; + +import styles from "./details.less"; + +export class Details extends Component { + render() { + const { build } = this.props; + + return ( + <div className={styles.info}> + <StatusLabel status={build.status} /> + + <section className={styles.message} style={{ whiteSpace: "pre-line" }}> + {build.message} + </section> + + <section> + <div className={styles.author}> + <img src={build.author_avatar} /> + <span>{build.author}</span> + </div> + + <BuildTime + start={build.started_at || build.created_at} + finish={build.finished_at} + /> + </section> + + <section> + <BuildMeta + link={build.link_url} + event={build.event} + commit={build.commit} + branch={build.branch} + target={build.deploy_to} + refspec={build.refspec} + refs={build.ref} + /> + </section> + </div> + ); + } +} diff --git a/web/src/screens/repo/screens/build/components/elapsed.js b/web/src/screens/repo/screens/build/components/elapsed.js new file mode 100644 index 0000000000..e3b7ec18ca --- /dev/null +++ b/web/src/screens/repo/screens/build/components/elapsed.js @@ -0,0 +1,63 @@ +import React, { Component } from "react"; + +export class Elapsed extends Component { + constructor(props, context) { + super(props); + + this.state = { + elapsed: 0, + }; + + this.tick = this.tick.bind(this); + } + + componentDidMount() { + this.timer = setInterval(this.tick, 1000); + } + + componentWillUnmount() { + clearInterval(this.timer); + } + + tick() { + const { start } = this.props; + const stop = ~~(Date.now() / 1000); + this.setState({ + elapsed: stop - start, + }); + } + + render() { + const { elapsed } = this.state; + const date = new Date(null); + date.setSeconds(elapsed); + return ( + <time> + {!elapsed ? ( + undefined + ) : elapsed > 3600 ? ( + date.toISOString().substr(11, 8) + ) : ( + date.toISOString().substr(14, 5) + )} + </time> + ); + } +} + +/* + * Returns the duration in hh:mm:ss format. + * + * @param {number} from - The start time in secnds + * @param {number} to - The end time in seconds + * @return {string} + */ +export const formatTime = (end, start) => { + const diff = end - start; + const date = new Date(null); + date.setSeconds(diff); + + return diff > 3600 + ? date.toISOString().substr(11, 8) + : date.toISOString().substr(14, 5); +}; diff --git a/web/src/screens/repo/screens/build/components/index.js b/web/src/screens/repo/screens/build/components/index.js new file mode 100644 index 0000000000..a7fc1de149 --- /dev/null +++ b/web/src/screens/repo/screens/build/components/index.js @@ -0,0 +1,5 @@ +import { Approval } from "./approval"; +import { Details } from "./details"; +import { ProcList, ProcListItem } from "./procs"; + +export { Approval, Details, ProcList, ProcListItem }; diff --git a/web/src/screens/repo/screens/build/components/procs.js b/web/src/screens/repo/screens/build/components/procs.js new file mode 100644 index 0000000000..4da793acd0 --- /dev/null +++ b/web/src/screens/repo/screens/build/components/procs.js @@ -0,0 +1,100 @@ +import React, { Component } from "react"; +import { Link } from "react-router-dom"; +import classnames from "classnames"; + +import { Elapsed, formatTime } from "./elapsed"; +import { default as Status, StatusText } from "shared/components/status"; + +import styles from "./procs.less"; + +const renderEnviron = data => { + return ( + <div> + {data[0]}={data[1]} + </div> + ); +}; + +class ProcListHolder extends Component { + constructor(props, context) { + super(props, context); + this.state = { open: !this.props.renderName }; + } + + toggleOpen = () => { + this.setState({ open: !this.state.open }); + }; + + render() { + const { vars, renderName, children } = this.props; + const groupExpandStatus = this.state.open + ? styles.collapsed + : styles.expanded; + + return ( + <div className={styles.list}> + {renderName && vars.name !== "drone" ? ( + <div + onClick={this.toggleOpen} + className={`${styles.group} ${groupExpandStatus}`} + > + <StatusText status={vars.state} text={vars.name} /> + </div> + ) : null} + {vars.environ ? ( + <div + onClick={this.toggleOpen} + className={`${styles.group} ${groupExpandStatus}`} + > + <StatusText + status={vars.state} + text={Object.entries(vars.environ).map(renderEnviron)} + /> + </div> + ) : null} + <div className={!this.state.open ? styles.hide : ""}>{children}</div> + </div> + ); + } +} + +export class ProcList extends Component { + render() { + const { repo, build, rootProc, selectedProc, renderName } = this.props; + return ( + <ProcListHolder vars={rootProc} renderName={renderName}> + {this.props.rootProc.children.map(function(child) { + return ( + <Link + to={`/${repo.full_name}/${build.number}/${child.pid}`} + key={`${repo.full_name}-${build.number}-${child.pid}`} + > + <ProcListItem + key={child.pid} + name={child.name} + start={child.start_time} + finish={child.end_time} + state={child.state} + selected={child.pid === selectedProc.pid} + /> + </Link> + ); + })} + </ProcListHolder> + ); + } +} + +export const ProcListItem = ({ name, start, finish, state, selected }) => ( + <div className={classnames(styles.item, selected ? styles.selected : null)}> + <h3>{name}</h3> + {finish ? ( + <time>{formatTime(finish, start)}</time> + ) : ( + <Elapsed start={start} /> + )} + <div> + <Status status={state} /> + </div> + </div> +); diff --git a/web/src/screens/repo/screens/build/index.js b/web/src/screens/repo/screens/build/index.js new file mode 100644 index 0000000000..f5ab148e7a --- /dev/null +++ b/web/src/screens/repo/screens/build/index.js @@ -0,0 +1,257 @@ +import React, { Component } from "react"; +import { Link } from "react-router-dom"; + +import { fetchBuild, approveBuild, declineBuild } from "shared/utils/build"; +import { + STATUS_BLOCKED, + STATUS_DECLINED, + STATUS_ERROR, +} from "shared/constants/status"; + +import { findChildProcess } from "shared/utils/proc"; +import { fetchRepository } from "shared/utils/repository"; + +import Breadcrumb, { SEPARATOR } from "shared/components/breadcrumb"; + +import { Approval, Details, ProcList } from "./components"; + +import { branch } from "baobab-react/higher-order"; +import { inject } from "config/client/inject"; + +import Output from "./logs"; + +import styles from "./index.less"; + +const binding = (props, context) => { + const { owner, repo, build } = props.match.params; + const slug = `${owner}/${repo}`; + const number = parseInt(build); + + return { + repo: ["repos", "data", slug], + build: ["builds", "data", slug, number], + }; +}; + +@inject +@branch(binding) +export default class BuildLogs extends Component { + constructor(props, context) { + super(props, context); + + this.handleApprove = this.handleApprove.bind(this); + this.handleDecline = this.handleDecline.bind(this); + } + + componentWillMount() { + this.synchronize(this.props); + } + + handleApprove() { + const { repo, build, drone } = this.props; + this.props.dispatch( + approveBuild, + drone, + repo.owner, + repo.name, + build.number, + ); + } + + handleDecline() { + const { repo, build, drone } = this.props; + this.props.dispatch( + declineBuild, + drone, + repo.owner, + repo.name, + build.number, + ); + } + + componentWillUpdate(nextProps) { + if (this.props.match.url !== nextProps.match.url) { + this.synchronize(nextProps); + } + } + + synchronize(props) { + if (!props.repo) { + this.props.dispatch( + fetchRepository, + props.drone, + props.match.params.owner, + props.match.params.repo, + ); + } + if (!props.build || !props.build.procs) { + this.props.dispatch( + fetchBuild, + props.drone, + props.match.params.owner, + props.match.params.repo, + props.match.params.build, + ); + } + } + + shouldComponentUpdate(nextProps, nextState) { + return this.props !== nextProps; + } + + render() { + const { repo, build } = this.props; + + if (!build || !repo) { + return this.renderLoading(); + } + + if (build.status === STATUS_DECLINED || build.status === STATUS_ERROR) { + return this.renderError(); + } + + if (build.status === STATUS_BLOCKED) { + return this.renderBlocked(); + } + + if (!build.procs) { + return this.renderLoading(); + } + + return this.renderSimple(); + } + + renderLoading() { + return ( + <div className={styles.host}> + <div className={styles.columns}> + <div className={styles.right}>Loading ...</div> + </div> + </div> + ); + } + + renderBlocked() { + const { build } = this.props; + return ( + <div className={styles.host}> + <div className={styles.columns}> + <div className={styles.right}> + <Details build={build} /> + </div> + <div className={styles.left}> + <Approval + onapprove={this.handleApprove} + ondecline={this.handleDecline} + /> + </div> + </div> + </div> + ); + } + + renderError() { + const { build } = this.props; + return ( + <div className={styles.host}> + <div className={styles.columns}> + <div className={styles.right}> + <Details build={build} /> + </div> + <div className={styles.left}> + <div className={styles.logerror}> + {build.status === STATUS_ERROR ? ( + build.error + ) : ( + "Pipeline execution was declined" + )} + </div> + </div> + </div> + </div> + ); + } + + highlightedLine() { + if (location.hash.startsWith("#L")) { + return parseInt(location.hash.substr(2)) - 1; + } + + return undefined; + } + + renderSimple() { + // if (nextProps.build.procs[0].children !== undefined){ + // return null; + // } + + const { repo, build, match } = this.props; + const selectedProc = match.params.proc + ? findChildProcess(build.procs, match.params.proc) + : build.procs[0].children[0]; + const selectedProcParent = findChildProcess(build.procs, selectedProc.ppid); + const highlighted = this.highlightedLine(); + + return ( + <div className={styles.host}> + <div className={styles.columns}> + <div className={styles.right}> + <Details build={build} /> + <section className={styles.sticky}> + {build.procs.map(function(rootProc) { + return ( + <div style="padding-bottom: 20px;" key={rootProc.pid}> + <ProcList + key={rootProc.pid} + repo={repo} + build={build} + rootProc={rootProc} + selectedProc={selectedProc} + renderName={build.procs.length > 1} + /> + </div> + ); + })} + </section> + </div> + <div className={styles.left}> + {selectedProc && selectedProc.error ? ( + <div className={styles.logerror}>{selectedProc.error}</div> + ) : null} + {selectedProcParent && selectedProcParent.error ? ( + <div className={styles.logerror}>{selectedProcParent.error}</div> + ) : null} + <Output + match={this.props.match} + build={this.props.build} + proc={selectedProc} + highlighted={highlighted} + /> + </div> + </div> + </div> + ); + } +} + +export class BuildLogsTitle extends Component { + render() { + const { owner, repo, build } = this.props.match.params; + return ( + <Breadcrumb + elements={[ + <Link to={`/${owner}/${repo}`} key={`${owner}-${repo}`}> + {owner} / {repo} + </Link>, + SEPARATOR, + <Link + to={`/${owner}/${repo}/${build}`} + key={`${owner}-${repo}-${build}`} + > + {build} + </Link>, + ]} + /> + ); + } +} diff --git a/web/src/screens/repo/screens/build/logs/components/anchor.js b/web/src/screens/repo/screens/build/logs/components/anchor.js new file mode 100644 index 0000000000..17370fcd8d --- /dev/null +++ b/web/src/screens/repo/screens/build/logs/components/anchor.js @@ -0,0 +1,15 @@ +import React from "react"; + +import styles from "./anchor.less"; + +export const Top = () => <div className={styles.top} />; + +export const Bottom = () => <div className={styles.bottom} />; + +export const scrollToTop = () => { + document.querySelector(`.${styles.top}`).scrollIntoView(); +}; + +export const scrollToBottom = () => { + document.querySelector(`.${styles.bottom}`).scrollIntoView(); +}; diff --git a/web/src/screens/repo/screens/build/logs/components/term.js b/web/src/screens/repo/screens/build/logs/components/term.js new file mode 100644 index 0000000000..6fc74d5be5 --- /dev/null +++ b/web/src/screens/repo/screens/build/logs/components/term.js @@ -0,0 +1,93 @@ +import React, { Component } from "react"; +import AnsiUp from "ansi_up"; +import style from "./term.less"; +import { Link } from "react-router-dom"; + +let formatter = new AnsiUp(); +formatter.use_classes = true; + +class Term extends Component { + render() { + const { lines, exitcode, highlighted } = this.props; + return ( + <div className={style.term}> + {lines.map(line => renderTermLine(line, highlighted))} + {exitcode !== undefined ? renderExitCode(exitcode) : undefined} + </div> + ); + } + + shouldComponentUpdate(nextProps, nextState) { + return ( + this.props.lines !== nextProps.lines || + this.props.exitcode !== nextProps.exitcode || + this.props.highlighted !== nextProps.highlighted + ); + } +} + +class TermLine extends Component { + render() { + const { line, highlighted } = this.props; + return ( + <div + className={highlighted === line.pos ? style.highlight : style.line} + key={line.pos} + ref={highlighted === line.pos ? ref => (this.ref = ref) : null} + > + <div> + <Link to={`#L${line.pos + 1}`} key={line.pos + 1}> + {line.pos + 1} + </Link> + </div> + <div dangerouslySetInnerHTML={{ __html: this.colored }} /> + <div>{line.time || 0}s</div> + </div> + ); + } + + componentDidMount() { + if (this.ref !== undefined) { + scrollToRef(this.ref); + } + } + + get colored() { + return formatter.ansi_to_html(this.props.line.out || ""); + } + + shouldComponentUpdate(nextProps, nextState) { + return ( + this.props.line.out !== nextProps.line.out || + this.props.highlighted !== nextProps.highlighted + ); + } +} + +const renderTermLine = (line, highlighted) => { + return <TermLine line={line} highlighted={highlighted} />; +}; + +const renderExitCode = code => { + return <div className={style.exitcode}>exit code {code}</div>; +}; + +const TermError = () => { + return ( + <div className={style.error}> + Oops. There was a problem loading the logs. + </div> + ); +}; + +const TermLoading = () => { + return <div className={style.loading}>Loading ...</div>; +}; + +const scrollToRef = ref => window.scrollTo(0, ref.offsetTop - 100); + +Term.Line = TermLine; +Term.Error = TermError; +Term.Loading = TermLoading; + +export default Term; diff --git a/web/src/screens/repo/screens/build/logs/index.js b/web/src/screens/repo/screens/build/logs/index.js new file mode 100644 index 0000000000..384ab3af20 --- /dev/null +++ b/web/src/screens/repo/screens/build/logs/index.js @@ -0,0 +1,143 @@ +import React, { Component } from "react"; +import { inject } from "config/client/inject"; +import { branch } from "baobab-react/higher-order"; +import { repositorySlug } from "shared/utils/repository"; +import { assertProcFinished, assertProcRunning } from "shared/utils/proc"; +import { fetchLogs, subscribeToLogs, toggleLogs } from "shared/utils/logs"; + +import Term from "./components/term"; + +import { Top, Bottom, scrollToTop, scrollToBottom } from "./components/anchor"; + +import { ExpandIcon, PauseIcon, PlayIcon } from "shared/components/icons/index"; + +import styles from "./index.less"; + +const binding = (props, context) => { + const { owner, repo, build } = props.match.params; + const slug = repositorySlug(owner, repo); + const number = parseInt(build); + const pid = parseInt(props.proc.pid); + + return { + logs: ["logs", "data", slug, number, pid, "data"], + eof: ["logs", "data", slug, number, pid, "eof"], + loading: ["logs", "data", slug, number, pid, "loading"], + error: ["logs", "data", slug, number, pid, "error"], + follow: ["logs", "follow"], + }; +}; + +@inject +@branch(binding) +export default class Output extends Component { + constructor(props, context) { + super(props, context); + this.handleFollow = this.handleFollow.bind(this); + } + + componentWillMount() { + if (this.props.proc) { + this.componentWillUpdate(this.props); + } + } + + componentWillUpdate(nextProps) { + const { loading, logs, eof, error } = nextProps; + const routeChange = this.props.match.url !== nextProps.match.url; + + if (loading || error || (logs && eof)) { + return; + } + + if (assertProcFinished(nextProps.proc)) { + return this.props.dispatch( + fetchLogs, + nextProps.drone, + nextProps.match.params.owner, + nextProps.match.params.repo, + nextProps.build.number, + nextProps.proc.pid, + ); + } + + if (assertProcRunning(nextProps.proc) && (!logs || routeChange)) { + this.props.dispatch( + subscribeToLogs, + nextProps.drone, + nextProps.match.params.owner, + nextProps.match.params.repo, + nextProps.build.number, + nextProps.proc, + ); + } + } + + componentDidUpdate() { + if (this.props.follow) { + scrollToBottom(); + } + } + + handleFollow() { + this.props.dispatch(toggleLogs, !this.props.follow); + } + + render() { + const { logs, error, proc, loading, follow, highlighted } = this.props; + + if (loading || !proc) { + return <Term.Loading />; + } + + if (error) { + return <Term.Error />; + } + + return ( + <div> + <Top /> + <Term + lines={logs || []} + highlighted={highlighted} + exitcode={assertProcFinished(proc) ? proc.exit_code : undefined} + /> + <Bottom /> + <Actions + running={assertProcRunning(proc)} + following={follow} + onfollow={this.handleFollow} + onunfollow={this.handleFollow} + /> + </div> + ); + } +} + +/** + * Component renders floating log actions. These can be used + * to follow, unfollow, scroll to top and scroll to bottom. + */ +const Actions = ({ following, running, onfollow, onunfollow }) => ( + <div className={styles.actions}> + {running && !following ? ( + <button onClick={onfollow} className={styles.follow}> + <PlayIcon /> + </button> + ) : null} + + {running && following ? ( + <button onClick={onunfollow} className={styles.unfollow}> + <PauseIcon /> + </button> + ) : null} + + <button onClick={scrollToTop} className={styles.bottom}> + <ExpandIcon /> + </button> + + <button onClick={scrollToBottom} className={styles.top}> + <ExpandIcon /> + </button> + </div> +); diff --git a/web/src/screens/repo/screens/build/menu.js b/web/src/screens/repo/screens/build/menu.js new file mode 100644 index 0000000000..51780d0553 --- /dev/null +++ b/web/src/screens/repo/screens/build/menu.js @@ -0,0 +1,78 @@ +import React, { Component } from "react"; +import RepoMenu from "../builds/menu"; +import { RefreshIcon, CloseIcon } from "shared/components/icons"; + +import { cancelBuild, restartBuild } from "shared/utils/build"; +import { findChildProcess } from "shared/utils/proc"; +import { repositorySlug } from "shared/utils/repository"; + +import { branch } from "baobab-react/higher-order"; +import { inject } from "config/client/inject"; + +const binding = (props, context) => { + const { owner, repo, build } = props.match.params; + const slug = repositorySlug(owner, repo); + const number = parseInt(build); + return { + repo: ["repos", "data", slug], + build: ["builds", "data", slug, number], + }; +}; + +@inject +@branch(binding) +export default class BuildMenu extends Component { + constructor(props, context) { + super(props, context); + + this.handleCancel = this.handleCancel.bind(this); + this.handleRestart = this.handleRestart.bind(this); + } + + handleRestart() { + const { dispatch, drone, repo, build } = this.props; + dispatch(restartBuild, drone, repo.owner, repo.name, build.number); + } + + handleCancel() { + const { dispatch, drone, repo, build, match } = this.props; + const proc = findChildProcess(build.procs, match.params.proc || 2); + + dispatch( + cancelBuild, + drone, + repo.owner, + repo.name, + build.number, + proc.ppid, + ); + } + + render() { + const { build } = this.props; + + const rightSide = !build ? ( + undefined + ) : ( + <section> + {build.status === "pending" || build.status === "running" ? ( + <button onClick={this.handleCancel}> + <CloseIcon /> + <span>Cancel</span> + </button> + ) : ( + <button onClick={this.handleRestart}> + <RefreshIcon /> + <span>Restart Build</span> + </button> + )} + </section> + ); + + return ( + <div> + <RepoMenu {...this.props} right={rightSide} /> + </div> + ); + } +} diff --git a/web/src/screens/repo/screens/builds/components/index.js b/web/src/screens/repo/screens/builds/components/index.js new file mode 100644 index 0000000000..4ad6b4fdab --- /dev/null +++ b/web/src/screens/repo/screens/builds/components/index.js @@ -0,0 +1,3 @@ +import { List, Item } from "./list"; + +export { List, Item }; diff --git a/web/src/screens/repo/screens/builds/components/list.js b/web/src/screens/repo/screens/builds/components/list.js new file mode 100644 index 0000000000..8ffec50d06 --- /dev/null +++ b/web/src/screens/repo/screens/builds/components/list.js @@ -0,0 +1,55 @@ +import React, { Component } from "react"; + +import Status from "shared/components/status"; +import StatusNumber from "shared/components/status_number"; +import BuildTime from "shared/components/build_time"; +import BuildMeta from "shared/components/build_event"; + +import styles from "./list.less"; + +export const List = ({ children }) => ( + <div className={styles.list}>{children}</div> +); + +export class Item extends Component { + render() { + const { build } = this.props; + return ( + <div className={styles.item}> + <div className={styles.icon}> + <img src={build.author_avatar} title={build.author} /> + </div> + + <div className={styles.body}> + <h3>{build.message.split("\n")[0]}</h3> + </div> + + <div className={styles.meta}> + <BuildMeta + link={build.link_url} + event={build.event} + commit={build.commit} + branch={build.branch} + target={build.deploy_to} + refspec={build.refspec} + refs={build.ref} + /> + </div> + + <div className={styles.break} /> + + <div className={styles.time}> + <BuildTime + start={build.started_at || build.created_at} + finish={build.finished_at} + /> + </div> + + <div className={styles.status}> + <StatusNumber status={build.status} number={build.number} /> + <Status status={build.status} /> + </div> + </div> + ); + } +} diff --git a/web/src/screens/repo/screens/builds/header.js b/web/src/screens/repo/screens/builds/header.js new file mode 100644 index 0000000000..e82650c6f4 --- /dev/null +++ b/web/src/screens/repo/screens/builds/header.js @@ -0,0 +1,20 @@ +import React, { Component } from "react"; +import { Link } from "react-router-dom"; +import Breadcrumb from "shared/components/breadcrumb"; + +export default class Header extends Component { + render() { + const { owner, repo } = this.props.match.params; + return ( + <div> + <Breadcrumb + elements={[ + <Link to={`/${owner}/${repo}`} key={`${owner}-${repo}`}> + {owner} / {repo} + </Link>, + ]} + /> + </div> + ); + } +} diff --git a/web/src/screens/repo/screens/builds/index.js b/web/src/screens/repo/screens/builds/index.js new file mode 100644 index 0000000000..cf3f7b0348 --- /dev/null +++ b/web/src/screens/repo/screens/builds/index.js @@ -0,0 +1,153 @@ +import React, { Component } from "react"; +import { Link } from "react-router-dom"; +import { List, Item } from "./components"; + +import { fetchBuildList, compareBuild } from "shared/utils/build"; +import { fetchRepository, repositorySlug } from "shared/utils/repository"; + +import { branch } from "baobab-react/higher-order"; +import { inject } from "config/client/inject"; + +import styles from "./index.less"; + +const binding = (props, context) => { + const { owner, repo } = props.match.params; + const slug = repositorySlug(owner, repo); + return { + repo: ["repos", "data", slug], + builds: ["builds", "data", slug], + loaded: ["builds", "loaded"], + error: ["builds", "error"], + }; +}; + +@inject +@branch(binding) +export default class Main extends Component { + constructor(props, context) { + super(props, context); + + this.fetchNextBuildPage = this.fetchNextBuildPage.bind(this); + this.selectBranch = this.selectBranch.bind(this); + } + + componentWillMount() { + this.synchronize(this.props); + } + + shouldComponentUpdate(nextProps, nextState) { + return ( + this.props.repo !== nextProps.repo || + (nextProps.builds !== undefined && + this.props.builds !== nextProps.builds) || + this.props.error !== nextProps.error || + this.props.loaded !== nextProps.loaded || + this.state.branch !== nextState.branch + ); + } + + componentWillUpdate(nextProps) { + if (this.props.match.url !== nextProps.match.url) { + this.synchronize(nextProps); + } + } + + componentDidUpdate(prevProps) { + if (this.props.location !== prevProps.location) { + window.scrollTo(0, 0); + } + } + + synchronize(props) { + const { drone, dispatch, match, repo } = props; + + if (!repo) { + dispatch(fetchRepository, drone, match.params.owner, match.params.repo); + } + + dispatch(fetchBuildList, drone, match.params.owner, match.params.repo); + } + + fetchNextBuildPage(buildList) { + const { drone, dispatch, match } = this.props; + const page = Math.floor(buildList.length / 50) + 1; + + dispatch( + fetchBuildList, + drone, + match.params.owner, + match.params.repo, + page, + ); + } + + selectBranch(branch) { + this.setState({ + branch: branch, + }); + } + + render() { + const { repo, builds, loaded, error } = this.props; + const { branch } = this.state; + const list = Object.values(builds || {}); + + function renderBuild(build) { + return ( + <Link to={`/${repo.full_name}/${build.number}`} key={build.number}> + <Item build={build} /> + </Link> + ); + } + + const filterBranch = build => { + return !branch || build.branch === branch; + }; + + if (error) { + return <div>Not Found</div>; + } + + if (!loaded && list.length === 0) { + return <div>Loading</div>; + } + + if (!repo) { + return <div>Loading</div>; + } + + if (list.length === 0) { + return <div>Build list is empty</div>; + } + + return ( + <div className={styles.root}> + <div className={styles.right}> + {!branch ? ( + <button onClick={() => this.selectBranch(repo.default_branch)}> + Show {repo.default_branch} branch only + </button> + ) : ( + <button onClick={() => this.selectBranch(undefined)}> + Show all branches + </button> + )} + </div> + <List> + {list + .sort(compareBuild) + .filter(filterBranch) + .map(renderBuild)} + </List> + {list.length < repo.last_build && ( + <button + onClick={() => this.fetchNextBuildPage(list)} + className={styles.more} + > + Show more builds + </button> + )} + </div> + ); + } +} diff --git a/web/src/screens/repo/screens/builds/menu.js b/web/src/screens/repo/screens/builds/menu.js new file mode 100644 index 0000000000..71788860df --- /dev/null +++ b/web/src/screens/repo/screens/builds/menu.js @@ -0,0 +1,15 @@ +import React, { Component } from "react"; +import Menu from "shared/components/menu"; + +export default class RepoMenu extends Component { + render() { + const { owner, repo } = this.props.match.params; + const menu = [ + { to: `/${owner}/${repo}`, label: "Builds" }, + { to: `/${owner}/${repo}/settings/secrets`, label: "Secrets" }, + { to: `/${owner}/${repo}/settings/registry`, label: "Registry" }, + { to: `/${owner}/${repo}/settings`, label: "Settings" }, + ]; + return <Menu items={menu} {...this.props} />; + } +} diff --git a/web/src/screens/repo/screens/registry/components/form.js b/web/src/screens/repo/screens/registry/components/form.js new file mode 100644 index 0000000000..b9e71592fe --- /dev/null +++ b/web/src/screens/repo/screens/registry/components/form.js @@ -0,0 +1,80 @@ +import React, { Component } from "react"; +import styles from "./form.less"; + +export class Form extends Component { + constructor(props, context) { + super(props, context); + + this.state = { + address: "", + username: "", + password: "", + }; + + this._handleAddressChange = this._handleAddressChange.bind(this); + this._handleUsernameChange = this._handleUsernameChange.bind(this); + this._handlePasswordChange = this._handlePasswordChange.bind(this); + this._handleSubmit = this._handleSubmit.bind(this); + + this.clear = this.clear.bind(this); + } + + _handleAddressChange(event) { + this.setState({ address: event.target.value }); + } + + _handleUsernameChange(event) { + this.setState({ username: event.target.value }); + } + + _handlePasswordChange(event) { + this.setState({ password: event.target.value }); + } + + _handleSubmit() { + const { onsubmit } = this.props; + + const detail = { + address: this.state.address, + username: this.state.username, + password: this.state.password, + }; + + onsubmit({ detail }); + this.clear(); + } + + clear() { + this.setState({ address: "" }); + this.setState({ username: "" }); + this.setState({ password: "" }); + } + + render() { + return ( + <div className={styles.form}> + <input + type="text" + value={this.state.address} + onChange={this._handleAddressChange} + placeholder="Registry Address (e.g. docker.io)" + /> + <input + type="text" + value={this.state.username} + onChange={this._handleUsernameChange} + placeholder="Registry Username" + /> + <textarea + rows="1" + value={this.state.password} + onChange={this._handlePasswordChange} + placeholder="Registry Password" + /> + <div className={styles.actions}> + <button onClick={this._handleSubmit}>Save</button> + </div> + </div> + ); + } +} diff --git a/web/src/screens/repo/screens/registry/components/index.js b/web/src/screens/repo/screens/registry/components/index.js new file mode 100644 index 0000000000..ecd6b3500f --- /dev/null +++ b/web/src/screens/repo/screens/registry/components/index.js @@ -0,0 +1,4 @@ +import { Form } from "./form"; +import { List, Item } from "./list"; + +export { Form, List, Item }; diff --git a/web/src/screens/repo/screens/registry/components/list.js b/web/src/screens/repo/screens/registry/components/list.js new file mode 100644 index 0000000000..db084782bb --- /dev/null +++ b/web/src/screens/repo/screens/registry/components/list.js @@ -0,0 +1,15 @@ +import React from "react"; +import styles from "./list.less"; + +export const List = ({ children }) => ( + <div className={styles.list}>{children}</div> +); + +export const Item = props => ( + <div className={styles.item} key={props.name}> + <div>{props.name}</div> + <div> + <button onClick={props.ondelete}>delete</button> + </div> + </div> +); diff --git a/web/src/screens/repo/screens/registry/index.js b/web/src/screens/repo/screens/registry/index.js new file mode 100644 index 0000000000..fe6fbd0e0f --- /dev/null +++ b/web/src/screens/repo/screens/registry/index.js @@ -0,0 +1,103 @@ +import React, { Component } from "react"; + +import { repositorySlug } from "shared/utils/repository"; +import { + fetchRegistryList, + createRegistry, + deleteRegistry, +} from "shared/utils/registry"; + +import { branch } from "baobab-react/higher-order"; +import { inject } from "config/client/inject"; + +import { List, Item, Form } from "./components"; + +import styles from "./index.less"; + +const binding = (props, context) => { + const { owner, repo } = props.match.params; + const slug = repositorySlug(owner, repo); + return { + loaded: ["registry", "loaded"], + registries: ["registry", "data", slug], + }; +}; + +@inject +@branch(binding) +export default class RepoRegistry extends Component { + constructor(props, context) { + super(props, context); + + this.handleDelete = this.handleDelete.bind(this); + this.handleSave = this.handleSave.bind(this); + } + + shouldComponentUpdate(nextProps, nextState) { + return this.props.registries !== nextProps.registries; + } + + componentWillMount() { + const { dispatch, drone, match } = this.props; + const { owner, repo } = match.params; + dispatch(fetchRegistryList, drone, owner, repo); + } + + handleSave(e) { + const { dispatch, drone, match } = this.props; + const { owner, repo } = match.params; + const registry = { + address: e.detail.address, + username: e.detail.username, + password: e.detail.password, + }; + + dispatch(createRegistry, drone, owner, repo, registry); + } + + handleDelete(registry) { + const { dispatch, drone, match } = this.props; + const { owner, repo } = match.params; + dispatch(deleteRegistry, drone, owner, repo, registry.address); + } + + render() { + const { registries, loaded } = this.props; + + if (!loaded) { + return LOADING; + } + + return ( + <div className={styles.root}> + <div className={styles.left}> + {Object.keys(registries || {}).length === 0 ? EMPTY : undefined} + <List> + {Object.values(registries || {}).map(renderRegistry.bind(this))} + </List> + </div> + + <div className={styles.right}> + <Form onsubmit={this.handleSave} /> + </div> + </div> + ); + } +} + +function renderRegistry(registry) { + return ( + <Item + name={registry.address} + ondelete={this.handleDelete.bind(this, registry)} + /> + ); +} + +const LOADING = <div className={styles.loading}>Loading</div>; + +const EMPTY = ( + <div className={styles.empty}> + There are no registry credentials for this repository. + </div> +); diff --git a/web/src/screens/repo/screens/secrets/components/form.js b/web/src/screens/repo/screens/secrets/components/form.js new file mode 100644 index 0000000000..183e7520da --- /dev/null +++ b/web/src/screens/repo/screens/secrets/components/form.js @@ -0,0 +1,140 @@ +import React, { Component } from "react"; + +import { + EVENT_PUSH, + EVENT_TAG, + EVENT_PULL_REQUEST, + EVENT_DEPLOY, +} from "shared/constants/events"; + +import styles from "./form.less"; + +export class Form extends Component { + constructor(props, context) { + super(props, context); + + this.state = { + name: "", + value: "", + event: [EVENT_PUSH, EVENT_TAG, EVENT_DEPLOY], + }; + + this._handleNameChange = this._handleNameChange.bind(this); + this._handleValueChange = this._handleValueChange.bind(this); + this._handleEventChange = this._handleEventChange.bind(this); + this._handleSubmit = this._handleSubmit.bind(this); + + this.clear = this.clear.bind(this); + } + + _handleNameChange(event) { + this.setState({ name: event.target.value }); + } + + _handleValueChange(event) { + this.setState({ value: event.target.value }); + } + + _handleEventChange(event) { + const selected = this.state.event; + let index; + + if (event.target.checked) { + selected.push(event.target.value); + } else { + index = selected.indexOf(event.target.value); + selected.splice(index, 1); + } + + this.setState({ event: selected }); + } + + _handleSubmit() { + const { onsubmit } = this.props; + + const detail = { + name: this.state.name, + value: this.state.value, + event: this.state.event, + }; + + onsubmit({ detail }); + this.clear(); + } + + clear() { + this.setState({ name: "" }); + this.setState({ value: "" }); + this.setState({ event: [EVENT_PUSH, EVENT_TAG, EVENT_DEPLOY] }); + } + + render() { + let checked = this.state.event.reduce((map, event) => { + map[event] = true; + return map; + }, {}); + + return ( + <div className={styles.form}> + <input + type="text" + name="name" + value={this.state.name} + placeholder="Secret Name" + onChange={this._handleNameChange} + /> + <textarea + rows="1" + name="value" + value={this.state.value} + placeholder="Secret Value" + onChange={this._handleValueChange} + /> + <section> + <h2>Events</h2> + <div> + <label> + <input + type="checkbox" + checked={checked[EVENT_PUSH]} + value={EVENT_PUSH} + onChange={this._handleEventChange} + /> + <span>push</span> + </label> + <label> + <input + type="checkbox" + checked={checked[EVENT_TAG]} + value={EVENT_TAG} + onChange={this._handleEventChange} + /> + <span>tag</span> + </label> + <label> + <input + type="checkbox" + checked={checked[EVENT_PULL_REQUEST]} + value={EVENT_PULL_REQUEST} + onChange={this._handleEventChange} + /> + <span>pull request</span> + </label> + <label> + <input + type="checkbox" + checked={checked[EVENT_DEPLOY]} + value={EVENT_DEPLOY} + onChange={this._handleEventChange} + /> + <span>deploy</span> + </label> + </div> + </section> + <div className={styles.actions}> + <button onClick={this._handleSubmit}>Save</button> + </div> + </div> + ); + } +} diff --git a/web/src/screens/repo/screens/secrets/components/index.js b/web/src/screens/repo/screens/secrets/components/index.js new file mode 100644 index 0000000000..ecd6b3500f --- /dev/null +++ b/web/src/screens/repo/screens/secrets/components/index.js @@ -0,0 +1,4 @@ +import { Form } from "./form"; +import { List, Item } from "./list"; + +export { Form, List, Item }; diff --git a/web/src/screens/repo/screens/secrets/components/list.js b/web/src/screens/repo/screens/secrets/components/list.js new file mode 100644 index 0000000000..7fb80ba78e --- /dev/null +++ b/web/src/screens/repo/screens/secrets/components/list.js @@ -0,0 +1,20 @@ +import React from "react"; +import styles from "./list.less"; + +export const List = ({ children }) => <div>{children}</div>; + +export const Item = props => ( + <div className={styles.item} key={props.name}> + <div> + {props.name} + <ul>{props.event ? props.event.map(renderEvent) : null}</ul> + </div> + <div> + <button onClick={props.ondelete}>delete</button> + </div> + </div> +); + +const renderEvent = event => { + return <li>{event}</li>; +}; diff --git a/web/src/screens/repo/screens/secrets/index.js b/web/src/screens/repo/screens/secrets/index.js new file mode 100644 index 0000000000..c07dddac12 --- /dev/null +++ b/web/src/screens/repo/screens/secrets/index.js @@ -0,0 +1,99 @@ +import React, { Component } from "react"; + +import { repositorySlug } from "shared/utils/repository"; +import { + fetchSecretList, + createSecret, + deleteSecret, +} from "shared/utils/secrets"; + +import { branch } from "baobab-react/higher-order"; +import { inject } from "config/client/inject"; + +import { List, Item, Form } from "./components"; + +import styles from "./index.less"; + +const binding = (props, context) => { + const { owner, repo } = props.match.params; + const slug = repositorySlug(owner, repo); + return { + loaded: ["secrets", "loaded"], + secrets: ["secrets", "data", slug], + }; +}; + +@inject +@branch(binding) +export default class RepoSecrets extends Component { + constructor(props, context) { + super(props, context); + + this.handleSave = this.handleSave.bind(this); + } + + shouldComponentUpdate(nextProps, nextState) { + return this.props.secrets !== nextProps.secrets; + } + + componentWillMount() { + const { owner, repo } = this.props.match.params; + this.props.dispatch(fetchSecretList, this.props.drone, owner, repo); + } + + handleSave(e) { + const { dispatch, drone, match } = this.props; + const { owner, repo } = match.params; + const secret = { + name: e.detail.name, + value: e.detail.value, + event: e.detail.event, + }; + + dispatch(createSecret, drone, owner, repo, secret); + } + + handleDelete(secret) { + const { dispatch, drone, match } = this.props; + const { owner, repo } = match.params; + dispatch(deleteSecret, drone, owner, repo, secret.name); + } + + render() { + const { secrets, loaded } = this.props; + + if (!loaded) { + return LOADING; + } + + return ( + <div className={styles.root}> + <div className={styles.left}> + {Object.keys(secrets || {}).length === 0 ? EMPTY : undefined} + <List> + {Object.values(secrets || {}).map(renderSecret.bind(this))} + </List> + </div> + <div className={styles.right}> + <Form onsubmit={this.handleSave} /> + </div> + </div> + ); + } +} + +function renderSecret(secret) { + return ( + <Item + name={secret.name} + event={secret.event} + ondelete={this.handleDelete.bind(this, secret)} + /> + ); +} + +const LOADING = <div className={styles.loading}>Loading</div>; + +const EMPTY = ( + <div className={styles.empty}>There are no secrets for this repository.</div> +); diff --git a/web/src/screens/repo/screens/settings/index.js b/web/src/screens/repo/screens/settings/index.js new file mode 100644 index 0000000000..a7acc8a16e --- /dev/null +++ b/web/src/screens/repo/screens/settings/index.js @@ -0,0 +1,244 @@ +import React, { Component } from "react"; + +import { branch } from "baobab-react/higher-order"; +import { inject } from "config/client/inject"; + +import { + fetchRepository, + updateRepository, + repositorySlug, +} from "shared/utils/repository"; + +import { + VISIBILITY_PUBLIC, + VISIBILITY_PRIVATE, + VISIBILITY_INTERNAL, +} from "shared/constants/visibility"; + +import styles from "./index.less"; + +const binding = (props, context) => { + const { owner, repo } = props.match.params; + const slug = repositorySlug(owner, repo); + return { + user: ["user", "data"], + repo: ["repos", "data", slug], + }; +}; + +@inject +@branch(binding) +export default class Settings extends Component { + constructor(props, context) { + super(props, context); + + this.handlePushChange = this.handlePushChange.bind(this); + this.handlePullChange = this.handlePullChange.bind(this); + this.handleTagChange = this.handleTagChange.bind(this); + this.handleDeployChange = this.handleDeployChange.bind(this); + this.handleTrustedChange = this.handleTrustedChange.bind(this); + this.handleProtectedChange = this.handleProtectedChange.bind(this); + this.handleVisibilityChange = this.handleVisibilityChange.bind(this); + this.handleTimeoutChange = this.handleTimeoutChange.bind(this); + this.handlePathChange = this.handlePathChange.bind(this); + this.handleFallbackChange = this.handleFallbackChange.bind(this); + this.handleChange = this.handleChange.bind(this); + } + + shouldComponentUpdate(nextProps, nextState) { + return this.props.repo !== nextProps.repo; + } + + componentWillMount() { + const { drone, dispatch, match, repo } = this.props; + + if (!repo) { + dispatch(fetchRepository, drone, match.params.owner, match.params.repo); + } + } + + render() { + const { repo } = this.props; + + if (!repo) { + return undefined; + } + + return ( + <div className={styles.root}> + <section> + <h2>Pipeline Path</h2> + <div> + <input + type="text" + value={repo.config_file} + onBlur={this.handlePathChange} + /> + <label> + <input + type="checkbox" + checked={repo.fallback} + onChange={this.handleFallbackChange} + /> + <span>Fallback to .drone.yml if path not exists</span> + </label> + </div> + </section> + <section> + <h2>Repository Hooks</h2> + <div> + <label> + <input + type="checkbox" + checked={repo.allow_push} + onChange={this.handlePushChange} + /> + <span>push</span> + </label> + <label> + <input + type="checkbox" + checked={repo.allow_pr} + onChange={this.handlePullChange} + /> + <span>pull request</span> + </label> + <label> + <input + type="checkbox" + checked={repo.allow_tags} + onChange={this.handleTagChange} + /> + <span>tag</span> + </label> + <label> + <input + type="checkbox" + checked={repo.allow_deploys} + onChange={this.handleDeployChange} + /> + <span>deployment</span> + </label> + </div> + </section> + + <section> + <h2>Project Settings</h2> + <div> + <label> + <input + type="checkbox" + checked={repo.gated} + onChange={this.handleProtectedChange} + /> + <span>Protected</span> + </label> + <label> + <input + type="checkbox" + checked={repo.trusted} + onChange={this.handleTrustedChange} + /> + <span>Trusted</span> + </label> + </div> + </section> + + <section> + <h2>Project Visibility</h2> + <div> + <label> + <input + type="radio" + name="visibility" + value="public" + checked={repo.visibility === VISIBILITY_PUBLIC} + onChange={this.handleVisibilityChange} + /> + <span>Public</span> + </label> + <label> + <input + type="radio" + name="visibility" + value="private" + checked={repo.visibility === VISIBILITY_PRIVATE} + onChange={this.handleVisibilityChange} + /> + <span>Private</span> + </label> + <label> + <input + type="radio" + name="visibility" + value="internal" + checked={repo.visibility === VISIBILITY_INTERNAL} + onChange={this.handleVisibilityChange} + /> + <span>Internal</span> + </label> + </div> + </section> + + <section> + <h2>Timeout</h2> + <div> + <input + type="number" + value={repo.timeout} + onBlur={this.handleTimeoutChange} + /> + <span className={styles.minutes}>minutes</span> + </div> + </section> + </div> + ); + } + + handlePushChange(e) { + this.handleChange("allow_push", e.target.checked); + } + + handlePullChange(e) { + this.handleChange("allow_pr", e.target.checked); + } + + handleTagChange(e) { + this.handleChange("allow_tag", e.target.checked); + } + + handleDeployChange(e) { + this.handleChange("allow_deploy", e.target.checked); + } + + handleTrustedChange(e) { + this.handleChange("trusted", e.target.checked); + } + + handleProtectedChange(e) { + this.handleChange("gated", e.target.checked); + } + + handleVisibilityChange(e) { + this.handleChange("visibility", e.target.value); + } + + handleTimeoutChange(e) { + this.handleChange("timeout", parseInt(e.target.value)); + } + + handlePathChange(e) { + this.handleChange("config_file", e.target.value); + } + + handleFallbackChange(e) { + this.handleChange("fallback", e.target.checked); + } + + handleChange(prop, value) { + const { dispatch, drone, repo } = this.props; + let data = {}; + data[prop] = value; + dispatch(updateRepository, drone, repo.owner, repo.name, data); + } +} diff --git a/web/src/screens/titles.js b/web/src/screens/titles.js new file mode 100644 index 0000000000..3f0db7c606 --- /dev/null +++ b/web/src/screens/titles.js @@ -0,0 +1,29 @@ +import React from "react"; +import { Route, Switch } from "react-router-dom"; +import Title from "react-title-component"; + +// @see https://github.com/yannickcr/eslint-plugin-react/issues/512 +// eslint-disable-next-line react/display-name +export default function() { + return ( + <Switch> + <Route path="/account/tokens" exact={true} component={accountTitle} /> + <Route path="/account/repos" exact={true} component={accountRepos} /> + <Route path="/login" exact={false} component={loginTitle} /> + <Route path="/:owner/:repo" exact={false} component={repoTitle} /> + <Route path="/" exact={false} component={defautTitle} /> + </Switch> + ); +} + +const accountTitle = () => <Title render="Tokens | woodpecker" />; + +const accountRepos = () => <Title render="Repositories | woodpecker" />; + +const loginTitle = () => <Title render="Login | woodpecker" />; + +const repoTitle = ({ match }) => ( + <Title render={`${match.params.owner}/${match.params.repo} | woodpecker`} /> +); + +const defautTitle = () => <Title render="Welcome | woodpecker" />; diff --git a/web/src/screens/user/screens/repos/components/index.js b/web/src/screens/user/screens/repos/components/index.js new file mode 100644 index 0000000000..4ad6b4fdab --- /dev/null +++ b/web/src/screens/user/screens/repos/components/index.js @@ -0,0 +1,3 @@ +import { List, Item } from "./list"; + +export { List, Item }; diff --git a/web/src/screens/user/screens/repos/components/list.js b/web/src/screens/user/screens/repos/components/list.js new file mode 100644 index 0000000000..36f2a2485b --- /dev/null +++ b/web/src/screens/user/screens/repos/components/list.js @@ -0,0 +1,40 @@ +import React, { Component } from "react"; +import { Link } from "react-router-dom"; + +import { LaunchIcon } from "shared/components/icons"; +import { Switch } from "./switch"; + +import styles from "./list.less"; + +export const List = ({ children }) => ( + <div className={styles.list}>{children}</div> +); + +export class Item extends Component { + render() { + const { owner, name, active, link, onchange } = this.props; + return ( + <div className={styles.item}> + <div> + {owner}/{name} + </div> + <div className={active ? styles.active : styles.inactive}> + <Link to={link}> + <LaunchIcon /> + </Link> + </div> + <div> + <Switch onchange={onchange} checked={active} /> + </div> + </div> + ); + } + + shouldComponentUpdate(nextProps) { + return ( + this.props.owner !== nextProps.owner || + this.props.name !== nextProps.name || + this.props.active !== nextProps.active + ); + } +} diff --git a/web/src/screens/user/screens/repos/components/switch.js b/web/src/screens/user/screens/repos/components/switch.js new file mode 100644 index 0000000000..b656295485 --- /dev/null +++ b/web/src/screens/user/screens/repos/components/switch.js @@ -0,0 +1,13 @@ +import React, { Component } from "react"; +import styles from "./switch.less"; + +export class Switch extends Component { + render() { + const { checked, onchange } = this.props; + return ( + <label className={styles.switch}> + <input type="checkbox" checked={checked} onChange={onchange} /> + </label> + ); + } +} diff --git a/web/src/screens/user/screens/tokens/index.js b/web/src/screens/user/screens/tokens/index.js new file mode 100644 index 0000000000..c6b3e12b35 --- /dev/null +++ b/web/src/screens/user/screens/tokens/index.js @@ -0,0 +1,59 @@ +import React, { Component } from "react"; + +import { generateToken } from "shared/utils/users"; +import { branch } from "baobab-react/higher-order"; +import { inject } from "config/client/inject"; +import styles from "./index.less"; + +const binding = (props, context) => { + return { + location: ["location"], + token: ["token"], + }; +}; + +@inject +@branch(binding) +export default class Tokens extends Component { + shouldComponentUpdate(nextProps, nextState) { + return ( + this.props.location !== nextProps.location || + this.props.token !== nextProps.token + ); + } + + componentWillMount() { + const { drone, dispatch } = this.props; + + dispatch(generateToken, drone); + } + + render() { + const { location, token } = this.props; + + if (!location || !token) { + return <div>Loading</div>; + } + return ( + <div className={styles.root}> + <h2>Your Personal Token:</h2> + <pre>{token}</pre> + <h2>Example API Usage:</h2> + <pre>{usageWithCURL(location, token)}</pre> + <h2>Example CLI Usage:</h2> + <pre>{usageWithCLI(location, token)}</pre> + </div> + ); + } +} + +const usageWithCURL = (location, token) => { + return `curl -i ${location.protocol}//${location.host}/api/user -H "Authorization: Bearer ${token}"`; +}; + +const usageWithCLI = (location, token) => { + return `export DRONE_SERVER=${location.protocol}//${location.host} + export DRONE_TOKEN=${token} + + drone info`; +}; diff --git a/web/src/shared/components/avatar.js b/web/src/shared/components/avatar.js new file mode 100644 index 0000000000..4d38e24618 --- /dev/null +++ b/web/src/shared/components/avatar.js @@ -0,0 +1,12 @@ +import React, { Component } from "react"; +import styles from "./avatar.less"; + +export default class Avatar extends Component { + render() { + const image = this.props.image; + const style = { + backgroundImage: `url(${image})`, + }; + return <div className={styles.avatar} style={style} />; + } +} diff --git a/web/src/shared/components/breadcrumb.js b/web/src/shared/components/breadcrumb.js new file mode 100644 index 0000000000..736b18d302 --- /dev/null +++ b/web/src/shared/components/breadcrumb.js @@ -0,0 +1,21 @@ +import React, { Component } from "react"; +import { ExpandIcon, BackIcon } from "shared/components/icons/index"; +import style from "./breadcrumb.less"; + +// breadcrumb separater icon. +export const SEPARATOR = <ExpandIcon size={18} className={style.separator} />; + +// breadcrumb back button. +export const BACK_BUTTON = <BackIcon size={18} className={style.back} />; + +// helper function to render a list item. +const renderItem = (element, index) => { + return <li key={index}>{element}</li>; +}; + +export default class Breadcrumb extends Component { + render() { + const { elements } = this.props; + return <ol className={style.breadcrumb}>{elements.map(renderItem)}</ol>; + } +} diff --git a/web/src/shared/components/build_event.js b/web/src/shared/components/build_event.js new file mode 100644 index 0000000000..0d62e09388 --- /dev/null +++ b/web/src/shared/components/build_event.js @@ -0,0 +1,73 @@ +import React, { Component } from "react"; +import { + BranchIcon, + CommitIcon, + DeployIcon, + LaunchIcon, + MergeIcon, + TagIcon, +} from "shared/components/icons/index"; +import { + EVENT_TAG, + EVENT_PULL_REQUEST, + EVENT_DEPLOY, +} from "shared/constants/events"; + +import styles from "./build_event.less"; + +export default class BuildEvent extends Component { + render() { + const { event, branch, commit, refs, refspec, link, target } = this.props; + + return ( + <div className={styles.host}> + <div className={styles.row}> + <div> + <CommitIcon /> + </div> + <div>{commit && commit.substr(0, 10)}</div> + </div> + <div className={styles.row}> + <div> + {event === EVENT_TAG ? ( + <TagIcon /> + ) : event === EVENT_PULL_REQUEST ? ( + <MergeIcon /> + ) : event === EVENT_DEPLOY ? ( + <DeployIcon /> + ) : ( + <BranchIcon /> + )} + </div> + <div> + {event === EVENT_TAG && refs ? ( + trimTagRef(refs) + ) : event === EVENT_PULL_REQUEST && refspec ? ( + trimMergeRef(refs) + ) : event === EVENT_DEPLOY && target ? ( + target + ) : ( + branch + )} + </div> + </div> + <a href={link} target="_blank"> + <LaunchIcon /> + </a> + </div> + ); + } +} + +const trimMergeRef = ref => { + return ref.match(/\d/g) || ref; +}; + +const trimTagRef = ref => { + return ref.startsWith("refs/tags/") ? ref.substr(10) : ref; +}; + +// push +// pull request (ref) +// tag (ref) +// deploy diff --git a/web/src/shared/components/build_time.js b/web/src/shared/components/build_time.js new file mode 100644 index 0000000000..22d3cf0b0e --- /dev/null +++ b/web/src/shared/components/build_time.js @@ -0,0 +1,37 @@ +import React, { Component } from "react"; +import { ScheduleIcon, TimelapseIcon } from "shared/components/icons/index"; + +import TimeAgo from "react-timeago"; +import Duration from "./duration"; + +import styles from "./build_time.less"; + +export default class Runtime extends Component { + render() { + const { start, finish } = this.props; + return ( + <div className={styles.host}> + <div className={styles.row}> + <div> + <ScheduleIcon /> + </div> + <div>{start ? <TimeAgo date={start * 1000} /> : <span>--</span>}</div> + </div> + <div className={styles.row}> + <div> + <TimelapseIcon /> + </div> + <div> + {finish ? ( + <Duration start={start} finished={finish} /> + ) : start ? ( + <TimeAgo date={start * 1000} /> + ) : ( + <span>--</span> + )} + </div> + </div> + </div> + ); + } +} diff --git a/web/src/shared/components/drawer/drawer.js b/web/src/shared/components/drawer/drawer.js new file mode 100644 index 0000000000..e3e814a02b --- /dev/null +++ b/web/src/shared/components/drawer/drawer.js @@ -0,0 +1,62 @@ +import React, { Component } from "react"; +import CloseIcon from "shared/components/icons/close"; +import styles from "./drawer.less"; +import { CSSTransitionGroup } from "react-transition-group"; + +export const DOCK_LEFT = styles.left; +export const DOCK_RIGHT = styles.right; + +export class Drawer extends Component { + render() { + const { open, position } = this.props; + + let classes = [styles.drawer]; + if (open) { + classes.push(styles.open); + } + if (position) { + classes.push(position); + } + + var child = open ? ( + <div key={0} onClick={this.props.onClick} className={styles.backdrop} /> + ) : null; + + return ( + <div className={classes.join(" ")}> + <CSSTransitionGroup + transitionName="fade" + transitionEnterTimeout={150} + transitionLeaveTimeout={150} + transitionAppearTimeout={150} + transitionAppear={true} + transitionEnter={true} + transitionLeave={true} + > + {child} + </CSSTransitionGroup> + <div className={styles.inner}>{this.props.children}</div> + </div> + ); + } +} + +export class CloseButton extends Component { + render() { + return ( + <button className={styles.close} onClick={this.props.onClick}> + <CloseIcon /> + </button> + ); + } +} + +export class MenuButton extends Component { + render() { + return ( + <button className={styles.close} onClick={this.props.onClick}> + Show Menu + </button> + ); + } +} diff --git a/web/src/shared/components/duration.js b/web/src/shared/components/duration.js new file mode 100644 index 0000000000..6ea6236d66 --- /dev/null +++ b/web/src/shared/components/duration.js @@ -0,0 +1,10 @@ +import humanizeDuration from "humanize-duration"; +import React from "react"; + +export default class Duration extends React.Component { + render() { + const { start, finished } = this.props; + + return <time>{humanizeDuration((finished - start) * 1000)}</time>; + } +} diff --git a/web/src/shared/components/icons/back.js b/web/src/shared/components/icons/back.js new file mode 100644 index 0000000000..09f1bedcb8 --- /dev/null +++ b/web/src/shared/components/icons/back.js @@ -0,0 +1,17 @@ +import React, { Component } from "react"; + +export default class BackIcon extends Component { + render() { + return ( + <svg + className={this.props.className} + width={this.props.size || 24} + height={this.props.size || 24} + viewBox="0 0 24 24" + > + <path d="M0 0h24v24H0z" fill="none" /> + <path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z" /> + </svg> + ); + } +} diff --git a/web/src/shared/components/icons/branch.js b/web/src/shared/components/icons/branch.js new file mode 100644 index 0000000000..922ef6a21e --- /dev/null +++ b/web/src/shared/components/icons/branch.js @@ -0,0 +1,11 @@ +import React, { Component } from "react"; + +export default class BranchIcon extends Component { + render() { + return ( + <svg viewBox="0 0 24 24"> + <path d="M6,2A3,3 0 0,1 9,5C9,6.28 8.19,7.38 7.06,7.81C7.15,8.27 7.39,8.83 8,9.63C9,10.92 11,12.83 12,14.17C13,12.83 15,10.92 16,9.63C16.61,8.83 16.85,8.27 16.94,7.81C15.81,7.38 15,6.28 15,5A3,3 0 0,1 18,2A3,3 0 0,1 21,5C21,6.32 20.14,7.45 18.95,7.85C18.87,8.37 18.64,9 18,9.83C17,11.17 15,13.08 14,14.38C13.39,15.17 13.15,15.73 13.06,16.19C14.19,16.62 15,17.72 15,19A3,3 0 0,1 12,22A3,3 0 0,1 9,19C9,17.72 9.81,16.62 10.94,16.19C10.85,15.73 10.61,15.17 10,14.38C9,13.08 7,11.17 6,9.83C5.36,9 5.13,8.37 5.05,7.85C3.86,7.45 3,6.32 3,5A3,3 0 0,1 6,2M6,4A1,1 0 0,0 5,5A1,1 0 0,0 6,6A1,1 0 0,0 7,5A1,1 0 0,0 6,4M18,4A1,1 0 0,0 17,5A1,1 0 0,0 18,6A1,1 0 0,0 19,5A1,1 0 0,0 18,4M12,18A1,1 0 0,0 11,19A1,1 0 0,0 12,20A1,1 0 0,0 13,19A1,1 0 0,0 12,18Z" /> + </svg> + ); + } +} diff --git a/web/src/shared/components/icons/check.js b/web/src/shared/components/icons/check.js new file mode 100644 index 0000000000..9c6f40a9d1 --- /dev/null +++ b/web/src/shared/components/icons/check.js @@ -0,0 +1,17 @@ +import React, { Component } from "react"; + +export default class CheckIcon extends Component { + render() { + return ( + <svg + className={this.props.className} + width={this.props.size || 24} + height={this.props.size || 24} + viewBox="0 0 24 24" + > + <path d="M0 0h24v24H0z" fill="none" /> + <path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z" /> + </svg> + ); + } +} diff --git a/web/src/shared/components/icons/clock.js b/web/src/shared/components/icons/clock.js new file mode 100644 index 0000000000..e898d4f9ac --- /dev/null +++ b/web/src/shared/components/icons/clock.js @@ -0,0 +1,17 @@ +import React, { Component } from "react"; + +export default class ClockIcon extends Component { + render() { + return ( + <svg + className={this.props.className} + width={this.props.size || 24} + height={this.props.size || 24} + viewBox="0 0 24 24" + > + <path d="M0 0h24v24H0z" fill="none" /> + <path d="M22 5.72l-4.6-3.86-1.29 1.53 4.6 3.86L22 5.72zM7.88 3.39L6.6 1.86 2 5.71l1.29 1.53 4.59-3.85zM12.5 8H11v6l4.75 2.85.75-1.23-4-2.37V8zM12 4c-4.97 0-9 4.03-9 9s4.02 9 9 9c4.97 0 9-4.03 9-9s-4.03-9-9-9zm0 16c-3.87 0-7-3.13-7-7s3.13-7 7-7 7 3.13 7 7-3.13 7-7 7z" /> + </svg> + ); + } +} diff --git a/web/src/shared/components/icons/close.js b/web/src/shared/components/icons/close.js new file mode 100644 index 0000000000..4ca13e78dc --- /dev/null +++ b/web/src/shared/components/icons/close.js @@ -0,0 +1,17 @@ +import React, { Component } from "react"; + +export default class CloseIcon extends Component { + render() { + return ( + <svg + className={this.props.className} + width={this.props.size || 24} + height={this.props.size || 24} + viewBox="0 0 24 24" + > + <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" /> + <path d="M0 0h24v24H0z" fill="none" /> + </svg> + ); + } +} diff --git a/web/src/shared/components/icons/commit.js b/web/src/shared/components/icons/commit.js new file mode 100644 index 0000000000..36a07b68af --- /dev/null +++ b/web/src/shared/components/icons/commit.js @@ -0,0 +1,16 @@ +import React, { Component } from "react"; + +export default class CommitIcon extends Component { + render() { + return ( + <svg + className={this.props.className} + width={this.props.size || 24} + height={this.props.size || 24} + viewBox="0 0 24 24" + > + <path d="M17,12C17,14.42 15.28,16.44 13,16.9V21H11V16.9C8.72,16.44 7,14.42 7,12C7,9.58 8.72,7.56 11,7.1V3H13V7.1C15.28,7.56 17,9.58 17,12M12,9A3,3 0 0,0 9,12A3,3 0 0,0 12,15A3,3 0 0,0 15,12A3,3 0 0,0 12,9Z" /> + </svg> + ); + } +} diff --git a/web/src/shared/components/icons/deploy.js b/web/src/shared/components/icons/deploy.js new file mode 100644 index 0000000000..dbabeb0fc2 --- /dev/null +++ b/web/src/shared/components/icons/deploy.js @@ -0,0 +1,11 @@ +import React, { Component } from "react"; + +export default class DeployIcon extends Component { + render() { + return ( + <svg className={this.props.className} viewBox="0 0 24 24"> + <path d="M19,18H6A4,4 0 0,1 2,14A4,4 0 0,1 6,10H6.71C7.37,7.69 9.5,6 12,6A5.5,5.5 0 0,1 17.5,11.5V12H19A3,3 0 0,1 22,15A3,3 0 0,1 19,18M19.35,10.03C18.67,6.59 15.64,4 12,4C9.11,4 6.6,5.64 5.35,8.03C2.34,8.36 0,10.9 0,14A6,6 0 0,0 6,20H19A5,5 0 0,0 24,15C24,12.36 21.95,10.22 19.35,10.03Z" /> + </svg> + ); + } +} diff --git a/web/src/shared/components/icons/expand.js b/web/src/shared/components/icons/expand.js new file mode 100644 index 0000000000..d74ba58192 --- /dev/null +++ b/web/src/shared/components/icons/expand.js @@ -0,0 +1,17 @@ +import React, { Component } from "react"; + +export default class ExpandIcon extends Component { + render() { + return ( + <svg + className={this.props.className} + width={this.props.size || 24} + height={this.props.size || 24} + viewBox="0 0 24 24" + > + <path d="M7.41 7.84L12 12.42l4.59-4.58L18 9.25l-6 6-6-6z" /> + <path d="M0-.75h24v24H0z" fill="none" /> + </svg> + ); + } +} diff --git a/web/src/shared/components/icons/index.js b/web/src/shared/components/icons/index.js new file mode 100644 index 0000000000..52574d93ad --- /dev/null +++ b/web/src/shared/components/icons/index.js @@ -0,0 +1,45 @@ +import BackIcon from "./back"; +import BranchIcon from "./branch"; +import CheckIcon from "./check"; +import ClockIcon from "./clock"; +import CloseIcon from "./close"; +import CommitIcon from "./commit"; +import DeployIcon from "./deploy"; +import ExpandIcon from "./expand"; +import LaunchIcon from "./launch"; +import LinkIcon from "./link"; +import MenuIcon from "./menu"; +import MergeIcon from "./merge"; +import PauseIcon from "./pause"; +import PlayIcon from "./play"; +import RefreshIcon from "./refresh"; +import RemoveIcon from "./remove"; +import ScheduleIcon from "./schedule"; +import StarIcon from "./star"; +import SyncIcon from "./sync"; +import TagIcon from "./tag"; +import TimelapseIcon from "./timelapse"; + +export { + BackIcon, + BranchIcon, + CheckIcon, + CloseIcon, + ClockIcon, + CommitIcon, + DeployIcon, + ExpandIcon, + LaunchIcon, + LinkIcon, + MenuIcon, + MergeIcon, + PauseIcon, + PlayIcon, + RefreshIcon, + RemoveIcon, + ScheduleIcon, + StarIcon, + SyncIcon, + TagIcon, + TimelapseIcon, +}; diff --git a/web/src/shared/components/icons/launch.js b/web/src/shared/components/icons/launch.js new file mode 100644 index 0000000000..f53e44ebeb --- /dev/null +++ b/web/src/shared/components/icons/launch.js @@ -0,0 +1,12 @@ +import React, { Component } from "react"; + +export default class LaunchIcon extends Component { + render() { + return ( + <svg className={this.props.className} viewBox="0 0 24 24"> + <path d="M0 0h24v24H0z" fill="none" /> + <path d="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z" /> + </svg> + ); + } +} diff --git a/web/src/shared/components/icons/link.js b/web/src/shared/components/icons/link.js new file mode 100644 index 0000000000..e38f93f603 --- /dev/null +++ b/web/src/shared/components/icons/link.js @@ -0,0 +1,17 @@ +import React, { Component } from "react"; + +export default class LinkIcon extends Component { + render() { + return ( + <svg + className={this.props.className} + width={this.props.size || 24} + height={this.props.size || 24} + viewBox="0 0 24 24" + > + <path d="M0 0h24v24H0z" fill="none" /> + <path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z" /> + </svg> + ); + } +} diff --git a/web/src/shared/components/icons/menu.js b/web/src/shared/components/icons/menu.js new file mode 100644 index 0000000000..60f1d1c8a1 --- /dev/null +++ b/web/src/shared/components/icons/menu.js @@ -0,0 +1,17 @@ +import React, { Component } from "react"; + +export default class MenuIcon extends Component { + render() { + return ( + <svg + className={this.props.className} + width={this.props.size || 24} + height={this.props.size || 24} + viewBox="0 0 24 24" + > + <path d="M0 0h24v24H0z" fill="none" /> + <path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z" /> + </svg> + ); + } +} diff --git a/web/src/shared/components/icons/merge.js b/web/src/shared/components/icons/merge.js new file mode 100644 index 0000000000..92dfb8cbe2 --- /dev/null +++ b/web/src/shared/components/icons/merge.js @@ -0,0 +1,16 @@ +import React, { Component } from "react"; + +export default class MergeIcon extends Component { + render() { + return ( + <svg className={this.props.className} viewBox="0 0 24 24"> + <path d="M5.41,21L6.12,17H2.12L2.47,15H6.47L7.53,9H3.53L3.88,7H7.88L8.59,3H10.59L9.88,7H15.88L16.59,3H18.59L17.88,7H21.88L21.53,9H17.53L16.47,15H20.47L20.12,17H16.12L15.41,21H13.41L14.12,17H8.12L7.41,21H5.41M9.53,9L8.47,15H14.47L15.53,9H9.53Z" /> + </svg> + ); + } +} + +// <svg class={this.props.className} viewBox="0 0 54.5 68"> +// <path d="M20,13C20,8.6,16.4,5,12.1,5C7.7,5,4.2,8.6,4.2,13c0,3.2,1.9,6,4.7,7.2v27.1c-2.7,1.2-4.7,4-4.7,7.2c0,4.4,3.6,7.9,7.9,7.9 c4.4,0,7.9-3.6,7.9-7.9c0-3.2-1.9-6-4.7-7.2V20.2C18.1,18.9,20,16.2,20,13z M16,54.5c0,2.2-1.8,3.9-3.9,3.9c-2.2,0-3.9-1.8-3.9-3.9 c0-2.2,1.8-3.9,3.9-3.9C14.2,50.5,16,52.3,16,54.5z M12.1,16.9c-2.2,0-3.9-1.8-3.9-3.9c0-2.2,1.8-3.9,3.9-3.9C14.2,9,16,10.8,16,13 C16,15.1,14.2,16.9,12.1,16.9z"/> +// <path d="M45.3,47.3V20.8c0-6.1-5-11.1-11.1-11.1h-2.7V3.6L20.7,13l10.8,9.3v-6.1h2.7c2.6,0,4.6,2.1,4.6,4.6v26.4 c-2.7,1.2-4.7,4-4.7,7.2c0,4.4,3.6,7.9,7.9,7.9c4.4,0,7.9-3.6,7.9-7.9C50,51.3,48.1,48.5,45.3,47.3z M42.1,58.4 c-2.2,0-3.9-1.8-3.9-3.9c0-2.2,1.8-3.9,3.9-3.9c2.2,0,3.9,1.8,3.9,3.9C46,56.6,44.2,58.4,42.1,58.4z"/> +// </svg> diff --git a/web/src/shared/components/icons/pause.js b/web/src/shared/components/icons/pause.js new file mode 100644 index 0000000000..3be94b89eb --- /dev/null +++ b/web/src/shared/components/icons/pause.js @@ -0,0 +1,17 @@ +import React, { Component } from "react"; + +export default class PauseIcon extends Component { + render() { + return ( + <svg + className={this.props.className} + width={this.props.size || 24} + height={this.props.size || 24} + viewBox="0 0 24 24" + > + <path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" /> + <path d="M0 0h24v24H0z" fill="none" /> + </svg> + ); + } +} diff --git a/web/src/shared/components/icons/play.js b/web/src/shared/components/icons/play.js new file mode 100644 index 0000000000..d13bab4d26 --- /dev/null +++ b/web/src/shared/components/icons/play.js @@ -0,0 +1,17 @@ +import React, { Component } from "react"; + +export default class PlayIcon extends Component { + render() { + return ( + <svg + className={this.props.className} + width={this.props.size || 24} + height={this.props.size || 24} + viewBox="0 0 24 24" + > + <path d="M8 5v14l11-7z" /> + <path d="M0 0h24v24H0z" fill="none" /> + </svg> + ); + } +} diff --git a/web/src/shared/components/icons/refresh.js b/web/src/shared/components/icons/refresh.js new file mode 100644 index 0000000000..6faed71a4f --- /dev/null +++ b/web/src/shared/components/icons/refresh.js @@ -0,0 +1,17 @@ +import React, { Component } from "react"; + +export default class RefreshIcon extends Component { + render() { + return ( + <svg + className={this.props.className} + width={this.props.size || 24} + height={this.props.size || 24} + viewBox="0 0 24 24" + > + <path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z" /> + <path d="M0 0h24v24H0z" fill="none" /> + </svg> + ); + } +} diff --git a/web/src/shared/components/icons/remove.js b/web/src/shared/components/icons/remove.js new file mode 100644 index 0000000000..4136d9287e --- /dev/null +++ b/web/src/shared/components/icons/remove.js @@ -0,0 +1,17 @@ +import React, { Component } from "react"; + +export default class CheckIcon extends Component { + render() { + return ( + <svg + className={this.props.className} + width={this.props.size || 24} + height={this.props.size || 24} + viewBox="0 0 24 24" + > + <path d="M19 13H5v-2h14v2z" /> + <path d="M0 0h24v24H0z" fill="none" /> + </svg> + ); + } +} diff --git a/web/src/shared/components/icons/report.js b/web/src/shared/components/icons/report.js new file mode 100644 index 0000000000..fc16ded1a0 --- /dev/null +++ b/web/src/shared/components/icons/report.js @@ -0,0 +1,12 @@ +import React, { Component } from "react"; + +export default class ReportIcon extends Component { + render() { + return ( + <svg className={this.props.className} viewBox="0 0 24 24"> + <path d="M15.73 3H8.27L3 8.27v7.46L8.27 21h7.46L21 15.73V8.27L15.73 3zM12 17.3c-.72 0-1.3-.58-1.3-1.3 0-.72.58-1.3 1.3-1.3.72 0 1.3.58 1.3 1.3 0 .72-.58 1.3-1.3 1.3zm1-4.3h-2V7h2v6z" /> + <path d="M0 0h24v24H0z" fill="none" /> + </svg> + ); + } +} diff --git a/web/src/shared/components/icons/schedule.js b/web/src/shared/components/icons/schedule.js new file mode 100644 index 0000000000..1310a0fbea --- /dev/null +++ b/web/src/shared/components/icons/schedule.js @@ -0,0 +1,18 @@ +import React, { Component } from "react"; + +export default class ScheduleIcon extends Component { + render() { + return ( + <svg + className={this.props.className} + width={this.props.size || 24} + height={this.props.size || 24} + viewBox="0 0 24 24" + > + <path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z" /> + <path d="M0 0h24v24H0z" fill="none" /> + <path d="M12.5 7H11v6l5.25 3.15.75-1.23-4.5-2.67z" /> + </svg> + ); + } +} diff --git a/web/src/shared/components/icons/star.js b/web/src/shared/components/icons/star.js new file mode 100644 index 0000000000..3019348ad1 --- /dev/null +++ b/web/src/shared/components/icons/star.js @@ -0,0 +1,20 @@ +import React, { Component } from "react"; + +export default class StarIcon extends Component { + render() { + return ( + <svg + className={this.props.className} + width={this.props.size || 24} + height={this.props.size || 24} + viewBox="0 0 512 512" + > + {this.props.filled === true ? ( + <path d="M256 372.686L380.83 448l-33.021-142.066L458 210.409l-145.267-12.475L256 64l-56.743 133.934L54 210.409l110.192 95.525L131.161 448z" /> + ) : ( + <path d="M458 210.409l-145.267-12.476L256 64l-56.743 133.934L54 210.409l110.192 95.524L131.161 448 256 372.686 380.83 448l-33.021-142.066L458 210.409zM272.531 345.286L256 335.312l-16.53 9.973-59.988 36.191 15.879-68.296 4.369-18.79-14.577-12.637-52.994-45.939 69.836-5.998 19.206-1.65 7.521-17.75 27.276-64.381 27.27 64.379 7.52 17.751 19.208 1.65 69.846 5.998-52.993 45.939-14.576 12.636 4.367 18.788 15.875 68.299-59.984-36.189z" /> + )} + </svg> + ); + } +} diff --git a/web/src/shared/components/icons/sync.js b/web/src/shared/components/icons/sync.js new file mode 100644 index 0000000000..ba8768a532 --- /dev/null +++ b/web/src/shared/components/icons/sync.js @@ -0,0 +1,12 @@ +import React, { Component } from "react"; + +export default class SyncIcon extends Component { + render() { + return ( + <svg className={this.props.className} viewBox="0 0 24 24"> + <path d="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46C19.54 15.03 20 13.57 20 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74C4.46 8.97 4 10.43 4 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z" /> + <path d="M0 0h24v24H0z" fill="none" /> + </svg> + ); + } +} diff --git a/web/src/shared/components/icons/tag.js b/web/src/shared/components/icons/tag.js new file mode 100644 index 0000000000..695d9b558c --- /dev/null +++ b/web/src/shared/components/icons/tag.js @@ -0,0 +1,11 @@ +import React, { Component } from "react"; + +export default class TagIcon extends Component { + render() { + return ( + <svg className={this.props.className} viewBox="0 0 24 24"> + <path d="M5.5,7A1.5,1.5 0 0,0 7,5.5A1.5,1.5 0 0,0 5.5,4A1.5,1.5 0 0,0 4,5.5A1.5,1.5 0 0,0 5.5,7M21.41,11.58C21.77,11.94 22,12.44 22,13C22,13.55 21.78,14.05 21.41,14.41L14.41,21.41C14.05,21.77 13.55,22 13,22C12.45,22 11.95,21.77 11.58,21.41L2.59,12.41C2.22,12.05 2,11.55 2,11V4C2,2.89 2.89,2 4,2H11C11.55,2 12.05,2.22 12.41,2.58L21.41,11.58M13,20L20,13L11.5,4.5L4.5,11.5L13,20Z" /> + </svg> + ); + } +} diff --git a/web/src/shared/components/icons/timelapse.js b/web/src/shared/components/icons/timelapse.js new file mode 100644 index 0000000000..c8afb955d1 --- /dev/null +++ b/web/src/shared/components/icons/timelapse.js @@ -0,0 +1,17 @@ +import React, { Component } from "react"; + +export default class TimelapseIcon extends Component { + render() { + return ( + <svg + className={this.props.className} + width={this.props.size || 24} + height={this.props.size || 24} + viewBox="0 0 24 24" + > + <path d="M0 0h24v24H0z" fill="none" /> + <path d="M16.24 7.76C15.07 6.59 13.54 6 12 6v6l-4.24 4.24c2.34 2.34 6.14 2.34 8.49 0 2.34-2.34 2.34-6.14-.01-8.48zM12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z" /> + </svg> + ); + } +} diff --git a/web/src/shared/components/logo.js b/web/src/shared/components/logo.js new file mode 100644 index 0000000000..920ec5a8b8 --- /dev/null +++ b/web/src/shared/components/logo.js @@ -0,0 +1,17 @@ +import React, { Component } from "react"; + +export default class Logo extends Component { + render() { + return ( + <svg viewBox="0 0 50 62.5" preserveAspectRatio="xMidYMid"> + <g> + <path + fillRule="evenodd" + clipRule="evenodd" + d="M15.872,0.468c1.148,1.088,1.582,2.188,2.855,2.337l0.036,0.007c-0.588,0.606-1.089,1.402-1.443,2.423c-0.379,1.096-0.488,2.285-0.614,3.659c-0.189,2.046-0.401,4.364-1.556,7.269 c-2.486,6.258-1.119,11.631,0.332,17.317c0.664,2.604,1.348,5.297,1.642,8.107c0.035,0.355,0.287,0.652,0.633,0.744 c0.346,0.095,0.709-0.035,0.922-0.323c0.227-0.313,0.524-0.797,0.86-1.424c0.84,3.323,1.355,6.131,1.783,8.697 c0.126,0.73,1.048,0.973,1.517,0.41c2.881-3.463,3.763-8.636,2.184-12.674c0.459-2.433,1.402-4.45,2.398-6.583 c0.536-1.15,1.08-2.318,1.55-3.566c0.228-0.084,0.569-0.314,0.791-0.441l1.706-0.981l-0.256,1.052 c-0.112,0.461,0.171,0.929,0.635,1.04c0.457,0.118,0.93-0.173,1.043-0.632l0.68-2.858l1.285-2.95 c0.19-0.436-0.009-0.943-0.446-1.135c-0.44-0.189-0.947,0.01-1.135,0.448l-1.152,2.669l-2.383,1.372 c0.235-0.932,0.414-1.919,0.508-2.981c0.432-4.859-0.718-9.074-3.066-11.266c-0.163-0.157-0.208-0.281-0.247-0.26 c0.095-0.119,0.249-0.26,0.358-0.374c2.283-1.693,6.047-0.147,8.319,0.751c0.589,0.231,0.876-0.338,0.316-0.67 c-1.949-1.154-5.948-4.197-8.188-6.194c-0.313-0.275-0.527-0.607-0.89-0.913c-2.415-4.266-8.168-1.764-10.885-2.252 C15.862,0.275,15.798,0.396,15.872,0.468 M26.852,6.367c-0.059,1.242-0.603,1.8-0.999,2.208c-0.218,0.224-0.427,0.436-0.525,0.738 c-0.236,0.714,0.008,1.51,0.66,2.143c1.974,1.84,2.925,5.527,2.538,9.861c-0.291,3.287-1.448,5.762-2.671,8.384 c-1.031,2.207-2.096,4.489-2.577,7.259c-0.027,0.161-0.01,0.33,0.056,0.481c1.021,2.433,1.135,6.196-0.672,9.46 c-0.461-2.553-1.053-5.385-1.97-8.712c1.964-4.488,4.203-11.75,2.919-17.668c-0.325-1.497-1.304-3.276-2.387-4.207 c-0.208-0.179-0.402-0.237-0.495-0.167c-0.084,0.061-0.151,0.238-0.062,0.444c0.55,1.266,0.879,2.599,1.226,4.276 c1.125,5.443-0.956,12.49-2.835,16.782l-0.116,0.259l-0.457,0.982c-0.356-2.014-0.849-3.95-1.33-5.839 c-1.379-5.407-2.679-10.516-0.401-16.255c1.247-3.137,1.483-5.692,1.672-7.746c0.116-1.263,0.216-2.355,0.526-3.252 c0.905-2.605,3.062-3.178,4.744-2.852C25.328,3.262,26.936,4.539,26.852,6.367z M23.984,6.988c0.617,0.204,1.283-0.131,1.487-0.75 c0.202-0.617-0.134-1.283-0.751-1.487c-0.618-0.204-1.285,0.134-1.487,0.751C23.029,6.12,23.366,6.786,23.984,6.988z" + /> + </g> + </svg> + ); + } +} diff --git a/web/src/shared/components/menu.js b/web/src/shared/components/menu.js new file mode 100644 index 0000000000..d9d5e133fd --- /dev/null +++ b/web/src/shared/components/menu.js @@ -0,0 +1,32 @@ +import React, { Component } from "react"; +import { NavLink as Link } from "react-router-dom"; +import PropTypes from "prop-types"; + +import styles from "./menu.less"; + +export default class Menu extends Component { + propTypes = { items: PropTypes.array, right: PropTypes.any }; + render() { + const items = this.props.items; + const right = this.props.right ? ( + <div className={styles.right}>{this.props.right}</div> + ) : null; + return ( + <section className={styles.root}> + <div className={styles.left}> + {items.map(i => ( + <Link + key={i.to + i.label} + to={i.to} + exact={true} + activeClassName={styles["link-active"]} + > + {i.label} + </Link> + ))} + </div> + {right} + </section> + ); + } +} diff --git a/web/src/shared/components/snackbar.js b/web/src/shared/components/snackbar.js new file mode 100644 index 0000000000..0484ef6d90 --- /dev/null +++ b/web/src/shared/components/snackbar.js @@ -0,0 +1,47 @@ +import React from "react"; +import styles from "./snackbar.less"; +import CloseIcon from "shared/components/icons/close"; +import { CSSTransitionGroup } from "react-transition-group"; + +export class Snackbar extends React.Component { + render() { + const { message } = this.props; + + let classes = [styles.snackbar]; + if (message) { + classes.push(styles.open); + } + + const content = message ? ( + <div className={classes.join(" ")} key={message}> + <div>{message}</div> + <button onClick={this.props.onClose}> + <CloseIcon /> + </button> + </div> + ) : null; + + return ( + <CSSTransitionGroup + transitionName="slideup" + transitionEnterTimeout={200} + transitionLeaveTimeout={200} + transitionAppearTimeout={200} + transitionAppear={true} + transitionEnter={true} + transitionLeave={true} + className={classes.root} + > + {content} + </CSSTransitionGroup> + ); + } +} + +// const SnackbarContent = ({ children, ...props }) => { +// <div {...props}>{children}</div> +// } +// +// const SnackbarClose = ({ children, ...props }) => { +// <div {...props}>{children}</div> +// } diff --git a/web/src/shared/components/status.js b/web/src/shared/components/status.js new file mode 100644 index 0000000000..bf0e310b60 --- /dev/null +++ b/web/src/shared/components/status.js @@ -0,0 +1,100 @@ +import React, { Component } from "react"; +import classnames from "classnames"; +import { + STATUS_BLOCKED, + STATUS_DECLINED, + STATUS_ERROR, + STATUS_FAILURE, + STATUS_KILLED, + STATUS_PENDING, + STATUS_RUNNING, + STATUS_SKIPPED, + STATUS_STARTED, + STATUS_SUCCESS, +} from "shared/constants/status"; +import style from "./status.less"; + +import { + CheckIcon, + CloseIcon, + ClockIcon, + RefreshIcon, + RemoveIcon, +} from "./icons/index"; + +const defaultIconSize = 15; + +const statusLabel = status => { + switch (status) { + case STATUS_BLOCKED: + return "Pending Approval"; + case STATUS_DECLINED: + return "Declined"; + case STATUS_ERROR: + return "Error"; + case STATUS_FAILURE: + return "Failure"; + case STATUS_KILLED: + return "Cancelled"; + case STATUS_PENDING: + return "Pending"; + case STATUS_RUNNING: + return "Running"; + case STATUS_SKIPPED: + return "Skipped"; + case STATUS_STARTED: + return "Running"; + case STATUS_SUCCESS: + return "Successful"; + default: + return ""; + } +}; + +const renderIcon = (status, size) => { + switch (status) { + case STATUS_SKIPPED: + return <RemoveIcon size={size} />; + case STATUS_PENDING: + return <ClockIcon size={size} />; + case STATUS_RUNNING: + case STATUS_STARTED: + return <RefreshIcon size={size} />; + case STATUS_SUCCESS: + return <CheckIcon size={size} />; + default: + return <CloseIcon size={size} />; + } +}; + +export default class Status extends Component { + shouldComponentUpdate(nextProps, nextState) { + return this.props.status !== nextProps.status; + } + + render() { + const { status } = this.props; + const icon = renderIcon(status, defaultIconSize); + const classes = classnames(style.root, style[status]); + return <div className={classes}>{icon}</div>; + } +} + +export const StatusLabel = ({ status }) => { + return ( + <div className={classnames(style.label, style[status])}> + <div>{statusLabel(status)}</div> + </div> + ); +}; + +export const StatusText = ({ status, text }) => { + return ( + <div + className={classnames(style.label, style[status])} + style="padding: 5px 10px;" + > + <div>{text}</div> + </div> + ); +}; diff --git a/web/src/shared/components/status_number.js b/web/src/shared/components/status_number.js new file mode 100644 index 0000000000..203254fdf6 --- /dev/null +++ b/web/src/shared/components/status_number.js @@ -0,0 +1,12 @@ +import React, { Component } from "react"; +import classnames from "classnames"; + +import styles from "./status_number.less"; + +export default class StatusNumber extends Component { + render() { + const { status, number } = this.props; + const className = classnames(styles.root, styles[status]); + return <div className={className}>{number}</div>; + } +} diff --git a/web/src/shared/components/sync.js b/web/src/shared/components/sync.js new file mode 100644 index 0000000000..5a9b7819af --- /dev/null +++ b/web/src/shared/components/sync.js @@ -0,0 +1,16 @@ +import React from "react"; +import Icon from "./icons/refresh"; +import styles from "./sync.less"; + +export const Message = () => { + return ( + <div className={styles.root}> + <div className={styles.alert}> + <div> + <Icon /> + </div> + <div>Account synchronization in progress</div> + </div> + </div> + ); +}; From b80645ec3eb4f586408db419015663003f001f06 Mon Sep 17 00:00:00 2001 From: Anton Bracke <anton@ju60.de> Date: Sat, 17 Jul 2021 18:52:50 +0200 Subject: [PATCH 004/143] undo some more changes --- web/src/config/client/inject.jsx | 36 --- web/src/index.jsx | 10 - web/src/screens/drone.jsx | 52 ---- web/src/screens/feed/components/index.jsx | 3 - web/src/screens/feed/components/list.jsx | 55 ---- web/src/screens/feed/index.jsx | 192 ------------- web/src/screens/layout.jsx | 224 --------------- web/src/screens/login/screens/error/index.jsx | 34 --- web/src/screens/login/screens/form/index.jsx | 21 -- web/src/screens/login/screens/index.jsx | 4 - web/src/screens/redirect.jsx | 41 --- .../screens/build/components/approval.jsx | 10 - .../repo/screens/build/components/details.jsx | 47 ---- .../repo/screens/build/components/elapsed.jsx | 63 ----- .../repo/screens/build/components/index.jsx | 5 - .../repo/screens/build/components/procs.jsx | 100 ------- web/src/screens/repo/screens/build/index.jsx | 257 ------------------ .../screens/build/logs/components/anchor.jsx | 15 - .../screens/build/logs/components/term.jsx | 93 ------- .../screens/repo/screens/build/logs/index.jsx | 143 ---------- web/src/screens/repo/screens/build/menu.jsx | 78 ------ .../repo/screens/builds/components/index.jsx | 3 - .../repo/screens/builds/components/list.jsx | 55 ---- .../screens/repo/screens/builds/header.jsx | 20 -- web/src/screens/repo/screens/builds/index.jsx | 153 ----------- web/src/screens/repo/screens/builds/menu.jsx | 15 - .../repo/screens/registry/components/form.jsx | 80 ------ .../screens/registry/components/index.jsx | 4 - .../repo/screens/registry/components/list.jsx | 15 - .../screens/repo/screens/registry/index.jsx | 103 ------- .../repo/screens/secrets/components/form.jsx | 140 ---------- .../repo/screens/secrets/components/index.jsx | 4 - .../repo/screens/secrets/components/list.jsx | 20 -- .../screens/repo/screens/secrets/index.jsx | 99 ------- .../screens/repo/screens/settings/index.jsx | 244 ----------------- web/src/screens/titles.jsx | 29 -- .../user/screens/repos/components/index.jsx | 3 - .../user/screens/repos/components/list.jsx | 40 --- .../user/screens/repos/components/switch.jsx | 13 - web/src/screens/user/screens/tokens/index.jsx | 59 ---- web/src/shared/components/avatar.jsx | 12 - web/src/shared/components/breadcrumb.jsx | 21 -- web/src/shared/components/build_event.jsx | 73 ----- web/src/shared/components/build_time.jsx | 37 --- web/src/shared/components/drawer/drawer.jsx | 62 ----- web/src/shared/components/duration.jsx | 10 - web/src/shared/components/icons/back.jsx | 17 -- web/src/shared/components/icons/branch.jsx | 11 - web/src/shared/components/icons/check.jsx | 17 -- web/src/shared/components/icons/clock.jsx | 17 -- web/src/shared/components/icons/close.jsx | 17 -- web/src/shared/components/icons/commit.jsx | 16 -- web/src/shared/components/icons/deploy.jsx | 11 - web/src/shared/components/icons/expand.jsx | 17 -- web/src/shared/components/icons/index.jsx | 45 --- web/src/shared/components/icons/launch.jsx | 12 - web/src/shared/components/icons/link.jsx | 17 -- web/src/shared/components/icons/menu.jsx | 17 -- web/src/shared/components/icons/merge.jsx | 16 -- web/src/shared/components/icons/pause.jsx | 17 -- web/src/shared/components/icons/play.jsx | 17 -- web/src/shared/components/icons/refresh.jsx | 17 -- web/src/shared/components/icons/remove.jsx | 17 -- web/src/shared/components/icons/report.jsx | 12 - web/src/shared/components/icons/schedule.jsx | 18 -- web/src/shared/components/icons/star.jsx | 20 -- web/src/shared/components/icons/sync.jsx | 12 - web/src/shared/components/icons/tag.jsx | 11 - web/src/shared/components/icons/timelapse.jsx | 17 -- web/src/shared/components/logo.jsx | 17 -- web/src/shared/components/menu.jsx | 32 --- web/src/shared/components/snackbar.jsx | 47 ---- web/src/shared/components/status.jsx | 100 ------- web/src/shared/components/status_number.jsx | 12 - web/src/shared/components/sync.jsx | 16 -- 75 files changed, 3409 deletions(-) delete mode 100644 web/src/config/client/inject.jsx delete mode 100644 web/src/index.jsx delete mode 100644 web/src/screens/drone.jsx delete mode 100644 web/src/screens/feed/components/index.jsx delete mode 100644 web/src/screens/feed/components/list.jsx delete mode 100644 web/src/screens/feed/index.jsx delete mode 100644 web/src/screens/layout.jsx delete mode 100644 web/src/screens/login/screens/error/index.jsx delete mode 100644 web/src/screens/login/screens/form/index.jsx delete mode 100644 web/src/screens/login/screens/index.jsx delete mode 100644 web/src/screens/redirect.jsx delete mode 100644 web/src/screens/repo/screens/build/components/approval.jsx delete mode 100644 web/src/screens/repo/screens/build/components/details.jsx delete mode 100644 web/src/screens/repo/screens/build/components/elapsed.jsx delete mode 100644 web/src/screens/repo/screens/build/components/index.jsx delete mode 100644 web/src/screens/repo/screens/build/components/procs.jsx delete mode 100644 web/src/screens/repo/screens/build/index.jsx delete mode 100644 web/src/screens/repo/screens/build/logs/components/anchor.jsx delete mode 100644 web/src/screens/repo/screens/build/logs/components/term.jsx delete mode 100644 web/src/screens/repo/screens/build/logs/index.jsx delete mode 100644 web/src/screens/repo/screens/build/menu.jsx delete mode 100644 web/src/screens/repo/screens/builds/components/index.jsx delete mode 100644 web/src/screens/repo/screens/builds/components/list.jsx delete mode 100644 web/src/screens/repo/screens/builds/header.jsx delete mode 100644 web/src/screens/repo/screens/builds/index.jsx delete mode 100644 web/src/screens/repo/screens/builds/menu.jsx delete mode 100644 web/src/screens/repo/screens/registry/components/form.jsx delete mode 100644 web/src/screens/repo/screens/registry/components/index.jsx delete mode 100644 web/src/screens/repo/screens/registry/components/list.jsx delete mode 100644 web/src/screens/repo/screens/registry/index.jsx delete mode 100644 web/src/screens/repo/screens/secrets/components/form.jsx delete mode 100644 web/src/screens/repo/screens/secrets/components/index.jsx delete mode 100644 web/src/screens/repo/screens/secrets/components/list.jsx delete mode 100644 web/src/screens/repo/screens/secrets/index.jsx delete mode 100644 web/src/screens/repo/screens/settings/index.jsx delete mode 100644 web/src/screens/titles.jsx delete mode 100644 web/src/screens/user/screens/repos/components/index.jsx delete mode 100644 web/src/screens/user/screens/repos/components/list.jsx delete mode 100644 web/src/screens/user/screens/repos/components/switch.jsx delete mode 100644 web/src/screens/user/screens/tokens/index.jsx delete mode 100644 web/src/shared/components/avatar.jsx delete mode 100644 web/src/shared/components/breadcrumb.jsx delete mode 100644 web/src/shared/components/build_event.jsx delete mode 100644 web/src/shared/components/build_time.jsx delete mode 100644 web/src/shared/components/drawer/drawer.jsx delete mode 100644 web/src/shared/components/duration.jsx delete mode 100644 web/src/shared/components/icons/back.jsx delete mode 100644 web/src/shared/components/icons/branch.jsx delete mode 100644 web/src/shared/components/icons/check.jsx delete mode 100644 web/src/shared/components/icons/clock.jsx delete mode 100644 web/src/shared/components/icons/close.jsx delete mode 100644 web/src/shared/components/icons/commit.jsx delete mode 100644 web/src/shared/components/icons/deploy.jsx delete mode 100644 web/src/shared/components/icons/expand.jsx delete mode 100644 web/src/shared/components/icons/index.jsx delete mode 100644 web/src/shared/components/icons/launch.jsx delete mode 100644 web/src/shared/components/icons/link.jsx delete mode 100644 web/src/shared/components/icons/menu.jsx delete mode 100644 web/src/shared/components/icons/merge.jsx delete mode 100644 web/src/shared/components/icons/pause.jsx delete mode 100644 web/src/shared/components/icons/play.jsx delete mode 100644 web/src/shared/components/icons/refresh.jsx delete mode 100644 web/src/shared/components/icons/remove.jsx delete mode 100644 web/src/shared/components/icons/report.jsx delete mode 100644 web/src/shared/components/icons/schedule.jsx delete mode 100644 web/src/shared/components/icons/star.jsx delete mode 100644 web/src/shared/components/icons/sync.jsx delete mode 100644 web/src/shared/components/icons/tag.jsx delete mode 100644 web/src/shared/components/icons/timelapse.jsx delete mode 100644 web/src/shared/components/logo.jsx delete mode 100644 web/src/shared/components/menu.jsx delete mode 100644 web/src/shared/components/snackbar.jsx delete mode 100644 web/src/shared/components/status.jsx delete mode 100644 web/src/shared/components/status_number.jsx delete mode 100644 web/src/shared/components/sync.jsx diff --git a/web/src/config/client/inject.jsx b/web/src/config/client/inject.jsx deleted file mode 100644 index e5f3f95132..0000000000 --- a/web/src/config/client/inject.jsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from "react"; - -export const drone = (client, Component) => { - // @see https://github.com/yannickcr/eslint-plugin-react/issues/512 - // eslint-disable-next-line react/display-name - const component = class extends React.Component { - getChildContext() { - return { - drone: client, - }; - } - - render() { - return <Component {...this.state} {...this.props} />; - } - }; - - component.childContextTypes = { - drone: (props, propName) => {}, - }; - - return component; -}; - -export const inject = Component => { - // @see https://github.com/yannickcr/eslint-plugin-react/issues/512 - // eslint-disable-next-line react/display-name - const component = class extends React.Component { - render() { - this.props.drone = this.context.drone; - return <Component {...this.state} {...this.props} />; - } - }; - - return component; -}; diff --git a/web/src/index.jsx b/web/src/index.jsx deleted file mode 100644 index 0626c17db2..0000000000 --- a/web/src/index.jsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from "react"; -import ReactDOM from "react-dom"; -import App from "./screens/drone"; - -ReactDOM.render( - <React.StrictMode> - <App /> - </React.StrictMode>, - document.getElementById("root"), -); diff --git a/web/src/screens/drone.jsx b/web/src/screens/drone.jsx deleted file mode 100644 index 9f463af7c4..0000000000 --- a/web/src/screens/drone.jsx +++ /dev/null @@ -1,52 +0,0 @@ -import React, { Component } from "react"; - -import { root } from "baobab-react/higher-order"; -import tree from "~/config/state"; -import client from "~/config/client"; -import { drone } from "~/config/client/inject"; -import { LoginForm, LoginError } from "~/screens/login/screens"; -import Title from "./titles"; -import Layout from "./layout"; -import RedirectRoot from "./redirect"; -import { fetchFeedOnce, subscribeToFeedOnce } from "~/shared/utils/feed"; - -import { BrowserRouter, Route, Switch } from "react-router-dom"; - -// eslint-disable-next-line no-unused-vars -import styles from "./drone.less"; - -if (module.hot) { - require("preact/devtools"); -} - -class App extends Component { - render() { - return ( - <BrowserRouter> - <div> - <Title /> - <Switch> - <Route path="/" exact={true} component={RedirectRoot} /> - <Route path="/login/form" exact={true} component={LoginForm} /> - <Route path="/login/error" exact={true} component={LoginError} /> - <Route path="/" exact={false} component={Layout} /> - </Switch> - </div> - </BrowserRouter> - ); - } -} - -if (tree.exists(["user", "data"])) { - fetchFeedOnce(tree, client); - subscribeToFeedOnce(tree, client); -} - -client.onerror = error => { - console.error(error); - if (error.status === 401) { - tree.unset(["user", "data"]); - } -}; - -export default root(tree, drone(client, App)); diff --git a/web/src/screens/feed/components/index.jsx b/web/src/screens/feed/components/index.jsx deleted file mode 100644 index 4ad6b4fdab..0000000000 --- a/web/src/screens/feed/components/index.jsx +++ /dev/null @@ -1,3 +0,0 @@ -import { List, Item } from "./list"; - -export { List, Item }; diff --git a/web/src/screens/feed/components/list.jsx b/web/src/screens/feed/components/list.jsx deleted file mode 100644 index 4f7ab5e03f..0000000000 --- a/web/src/screens/feed/components/list.jsx +++ /dev/null @@ -1,55 +0,0 @@ -import React, { Component } from "react"; - -import Status from "shared/components/status"; -import BuildTime from "shared/components/build_time"; - -import styles from "./list.less"; - -import { StarIcon } from "shared/components/icons/index"; - -export const List = ({ children }) => ( - <div className={styles.list}>{children}</div> -); - -export class Item extends Component { - constructor(props) { - super(props); - - this.handleFave = this.handleFave.bind(this); - } - - handleFave(e) { - e.preventDefault(); - this.props.onFave(this.props.item.full_name); - } - - render() { - const { item, faved } = this.props; - return ( - <div className={styles.item}> - <div onClick={this.handleFave}> - <StarIcon filled={faved} size={16} className={styles.star} /> - </div> - <div className={styles.header}> - <div className={styles.title}>{item.full_name}</div> - <div className={styles.icon}> - {item.status ? <Status status={item.status} /> : <noscript />} - </div> - </div> - - <div className={styles.body}> - <BuildTime - start={item.started_at || item.created_at} - finish={item.finished_at} - /> - </div> - </div> - ); - } - - shouldComponentUpdate(nextProps, nextState) { - return ( - this.props.item !== nextProps.item || this.props.faved !== nextProps.faved - ); - } -} diff --git a/web/src/screens/feed/index.jsx b/web/src/screens/feed/index.jsx deleted file mode 100644 index 70891049c5..0000000000 --- a/web/src/screens/feed/index.jsx +++ /dev/null @@ -1,192 +0,0 @@ -import React, { Component } from "react"; -import { Link } from "react-router-dom"; - -import { compareFeedItem } from "shared/utils/feed"; - -import { branch } from "baobab-react/higher-order"; -import { inject } from "config/client/inject"; - -import DroneIcon from "shared/components/logo"; -import { List, Item } from "./components"; - -import style from "./index.less"; - -import Collapsible from "react-collapsible"; - -const binding = (props, context) => { - return { feed: ["feed"] }; -}; - -@inject -@branch(binding) -export default class Sidebar extends Component { - constructor(props, context) { - super(props, context); - - this.setState({ - starred: JSON.parse(localStorage.getItem("starred") || "[]"), - starredOpen: (localStorage.getItem("starredOpen") || "true") === "true", - reposOpen: (localStorage.getItem("reposOpen") || "true") === "true", - }); - - this.handleFilter = this.handleFilter.bind(this); - this.toggleStarred = this.toggleItem.bind(this, "starredOpen"); - this.toggleAll = this.toggleItem.bind(this, "reposOpen"); - } - - shouldComponentUpdate(nextProps, nextState) { - return ( - this.props.feed !== nextProps.feed || - this.state.filter !== nextState.filter || - this.state.starred.length !== nextState.starred.length - ); - } - - handleFilter(e) { - this.setState({ - filter: e.target.value, - }); - } - - toggleItem = item => { - this.setState(state => { - return { [item]: !state[item] }; - }); - - localStorage.setItem(item, this.state[item]); - }; - - renderFeed = (list, renderStarred) => { - return ( - <div> - <List>{list.map(item => this.renderItem(item, renderStarred))}</List> - </div> - ); - }; - - renderItem = (item, renderStarred) => { - const starred = this.state.starred; - if (renderStarred && !starred.includes(item.full_name)) { - return null; - } - return ( - <Link to={`/${item.full_name}`} key={item.full_name}> - <Item - item={item} - onFave={this.onFave} - faved={starred.includes(item.full_name)} - /> - </Link> - ); - }; - - onFave = fullName => { - if (!this.state.starred.includes(fullName)) { - this.setState(state => { - const list = state.starred.concat(fullName); - return { starred: list }; - }); - } else { - this.setState(state => { - const list = state.starred.filter(v => v !== fullName); - return { starred: list }; - }); - } - - localStorage.setItem("starred", JSON.stringify(this.state.starred)); - }; - - render() { - const { feed } = this.props; - const { filter } = this.state; - - const list = feed.data ? Object.values(feed.data) : []; - - const filterFunc = item => { - return !filter || item.full_name.indexOf(filter) !== -1; - }; - - const filtered = list.filter(filterFunc).sort(compareFeedItem); - const starredOpen = this.state.starredOpen; - const reposOpen = this.state.reposOpen; - return ( - <div className={style.feed}> - {LOGO} - <Collapsible - trigger="Starred" - triggerTagName="div" - transitionTime={200} - open={starredOpen} - onOpen={this.toggleStarred} - onClose={this.toggleStarred} - triggerOpenedClassName={style.Collapsible__trigger} - triggerClassName={style.Collapsible__trigger} - > - {feed.loaded === false ? ( - LOADING - ) : feed.error ? ( - ERROR - ) : list.length === 0 ? ( - EMPTY - ) : ( - this.renderFeed(list, true) - )} - </Collapsible> - <Collapsible - trigger="Repos" - triggerTagName="div" - transitionTime={200} - open={reposOpen} - onOpen={this.toggleAll} - onClose={this.toggleAll} - triggerOpenedClassName={style.Collapsible__trigger} - triggerClassName={style.Collapsible__trigger} - > - <input - type="text" - placeholder="Search …" - onChange={this.handleFilter} - /> - {feed.loaded === false ? ( - LOADING - ) : feed.error ? ( - ERROR - ) : list.length === 0 ? ( - EMPTY - ) : filtered.length > 0 ? ( - this.renderFeed(filtered.sort(compareFeedItem), false) - ) : ( - NO_MATCHES - )} - </Collapsible> - </div> - ); - } -} - -const LOGO = ( - <div className={style.brand}> - <DroneIcon /> - <p> - Woodpecker<span style="margin-left: 4px;">{window.DRONE_VERSION}</span> - <br /> - <span> - <a href={window.DRONE_DOCS} target="_blank" rel="noopener noreferrer"> - Docs - </a> - </span> - </p> - </div> -); - -const LOADING = <div className={style.message}>Loading</div>; - -const EMPTY = <div className={style.message}>Your build feed is empty</div>; - -const NO_MATCHES = <div className={style.message}>No results found</div>; - -const ERROR = ( - <div className={style.message}> - Oops. It looks like there was a problem loading your feed - </div> -); diff --git a/web/src/screens/layout.jsx b/web/src/screens/layout.jsx deleted file mode 100644 index 785792ef00..0000000000 --- a/web/src/screens/layout.jsx +++ /dev/null @@ -1,224 +0,0 @@ -import React, { Component } from "react"; -import classnames from "classnames"; -import { Route, Switch, Link } from "react-router-dom"; -import { connectScreenSize } from "react-screen-size"; - -import { branch } from "baobab-react/higher-order"; -import { inject } from "config/client/inject"; - -import MenuIcon from "shared/components/icons/menu"; - -import Feed from "screens/feed"; -import RepoRegistry from "screens/repo/screens/registry"; -import RepoSecrets from "screens/repo/screens/secrets"; -import RepoSettings from "screens/repo/screens/settings"; -import RepoBuilds from "screens/repo/screens/builds"; -import UserRepos, { UserRepoTitle } from "screens/user/screens/repos"; -import UserTokens from "screens/user/screens/tokens"; -import RedirectRoot from "./redirect"; - -import RepoHeader from "screens/repo/screens/builds/header"; - -import UserReposMenu from "screens/user/screens/repos/menu"; -import BuildLogs, { BuildLogsTitle } from "screens/repo/screens/build"; -import BuildMenu from "screens/repo/screens/build/menu"; -import RepoMenu from "screens/repo/screens/builds/menu"; - -import { Snackbar } from "shared/components/snackbar"; -import { Drawer, DOCK_RIGHT } from "shared/components/drawer/drawer"; - -import styles from "./layout.less"; - -const binding = (props, context) => { - return { - user: ["user"], - message: ["message"], - sidebar: ["sidebar"], - menu: ["menu"], - }; -}; - -const mapScreenSizeToProps = screenSize => { - return { - isTablet: screenSize["small"], - isMobile: screenSize["mobile"], - isDesktop: screenSize["> small"], - }; -}; - -@inject -@branch(binding) -@connectScreenSize(mapScreenSizeToProps) -export default class Default extends Component { - constructor(props, context) { - super(props, context); - this.state = { - menu: false, - feed: false, - }; - - this.openMenu = this.openMenu.bind(this); - this.closeMenu = this.closeMenu.bind(this); - this.closeSnackbar = this.closeSnackbar.bind(this); - } - - componentWillReceiveProps(nextProps) { - if (nextProps.location !== this.props.location) { - this.closeMenu(true); - } - } - - openMenu() { - this.props.dispatch(tree => { - tree.set(["menu"], true); - }); - } - - closeMenu() { - this.props.dispatch(tree => { - tree.set(["menu"], false); - }); - } - - render() { - const { user, message, menu } = this.props; - - const classes = classnames(!user || !user.data ? styles.guest : null); - return ( - <div className={classes}> - <div className={styles.left}> - <Switch> - <Route path={"/"} component={Feed} /> - </Switch> - </div> - <div className={styles.center}> - {!user || !user.data ? ( - <a - href={"/login?url=" + window.location.href} - target="_self" - className={styles.login} - > - Click to Login - </a> - ) : ( - <noscript /> - )} - <div className={styles.title}> - <Switch> - <Route path="/account/repos" component={UserRepoTitle} /> - <Route - path="/:owner/:repo/:build(\d*)/:proc(\d*)" - exact={true} - component={BuildLogsTitle} - /> - <Route - path="/:owner/:repo/:build(\d*)" - component={BuildLogsTitle} - /> - <Route path="/:owner/:repo" component={RepoHeader} /> - </Switch> - {user && user.data ? ( - <div className={styles.avatar}> - <img src={user.data.avatar_url} /> - </div> - ) : ( - undefined - )} - {user && user.data ? ( - <button onClick={this.openMenu}> - <MenuIcon /> - </button> - ) : ( - <noscript /> - )} - </div> - - <div className={styles.menu}> - <Switch> - <Route - path="/account/repos" - exact={true} - component={UserReposMenu} - /> - <Route path="/account/" exact={false} component={undefined} /> - BuildMenu - <Route - path="/:owner/:repo/:build(\d*)/:proc(\d*)" - exact={true} - component={BuildMenu} - /> - <Route - path="/:owner/:repo/:build(\d*)" - exact={true} - component={BuildMenu} - /> - <Route path="/:owner/:repo" exact={false} component={RepoMenu} /> - </Switch> - </div> - - <Switch> - <Route path="/account/token" exact={true} component={UserTokens} /> - <Route path="/account/repos" exact={true} component={UserRepos} /> - <Route - path="/:owner/:repo/settings/secrets" - exact={true} - component={RepoSecrets} - /> - <Route - path="/:owner/:repo/settings/registry" - exact={true} - component={RepoRegistry} - /> - <Route - path="/:owner/:repo/settings" - exact={true} - component={RepoSettings} - /> - <Route - path="/:owner/:repo/:build(\d*)" - exact={true} - component={BuildLogs} - /> - <Route - path="/:owner/:repo/:build(\d*)/:proc(\d*)" - exact={true} - component={BuildLogs} - /> - <Route path="/:owner/:repo" exact={true} component={RepoBuilds} /> - <Route path="/" exact={true} component={RedirectRoot} /> - </Switch> - </div> - - <Snackbar message={message.text} onClose={this.closeSnackbar} /> - - <Drawer onClick={this.closeMenu} position={DOCK_RIGHT} open={menu}> - <section> - <ul> - <li> - <Link to="/account/repos">Repositories</Link> - </li> - <li> - <Link to="/account/token">Token</Link> - </li> - </ul> - </section> - <section> - <ul> - <li> - <a href="/logout" target="_self"> - Logout - </a> - </li> - </ul> - </section> - </Drawer> - </div> - ); - } - - closeSnackbar() { - this.props.dispatch(tree => { - tree.unset(["message", "text"]); - }); - } -} diff --git a/web/src/screens/login/screens/error/index.jsx b/web/src/screens/login/screens/error/index.jsx deleted file mode 100644 index d7a65bb97f..0000000000 --- a/web/src/screens/login/screens/error/index.jsx +++ /dev/null @@ -1,34 +0,0 @@ -import React, { Component } from "react"; -import queryString from "query-string"; -import Icon from "shared/components/icons/report"; - -import styles from "./index.less"; - -const DEFAULT_ERROR = "The system failed to process your Login request."; - -class Error extends Component { - render() { - const parsed = queryString.parse(window.location.search); - let error = DEFAULT_ERROR; - - switch (parsed.code || parsed.error) { - case "oauth_error": - break; - case "access_denied": - break; - } - - return ( - <div className={styles.root}> - <div className={styles.alert}> - <div> - <Icon /> - </div> - <div>{error}</div> - </div> - </div> - ); - } -} - -export default Error; diff --git a/web/src/screens/login/screens/form/index.jsx b/web/src/screens/login/screens/form/index.jsx deleted file mode 100644 index 7434ccdb65..0000000000 --- a/web/src/screens/login/screens/form/index.jsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from "react"; - -import styles from "./index.less"; - -const LoginForm = props => ( - <div className={styles.login}> - <form method="post" action="/authorize"> - <p>Login with your version control system username and password.</p> - <input - placeholder="Username" - name="username" - type="text" - spellCheck="false" - /> - <input placeholder="Password" name="password" type="password" /> - <input value="Login" type="submit" /> - </form> - </div> -); - -export default LoginForm; diff --git a/web/src/screens/login/screens/index.jsx b/web/src/screens/login/screens/index.jsx deleted file mode 100644 index 07b139db48..0000000000 --- a/web/src/screens/login/screens/index.jsx +++ /dev/null @@ -1,4 +0,0 @@ -import LoginForm from "./form"; -import LoginError from "./error"; - -export { LoginForm, LoginError }; diff --git a/web/src/screens/redirect.jsx b/web/src/screens/redirect.jsx deleted file mode 100644 index 1b36f7d38e..0000000000 --- a/web/src/screens/redirect.jsx +++ /dev/null @@ -1,41 +0,0 @@ -import React, { Component } from "react"; -import { Redirect } from "react-router-dom"; -import { branch } from "baobab-react/higher-order"; -import { Message } from "shared/components/sync"; - -const binding = (props, context) => { - return { - feed: ["feed"], - user: ["user", "data"], - syncing: ["user", "syncing"], - }; -}; - -@branch(binding) -export default class RedirectRoot extends Component { - componentWillReceiveProps(nextProps) { - const { user } = nextProps; - if (!user && window) { - window.location.href = "/login?url=" + window.location.href; - } - } - - render() { - const { user, syncing } = this.props; - const { latest, loaded } = this.props.feed; - - return !loaded && syncing ? ( - <Message /> - ) : !loaded ? ( - undefined - ) : !user ? ( - undefined - ) : !latest ? ( - <Redirect to="/account/repos" /> - ) : !latest.number ? ( - <Redirect to={`/${latest.full_name}`} /> - ) : ( - <Redirect to={`/${latest.full_name}/${latest.number}`} /> - ); - } -} diff --git a/web/src/screens/repo/screens/build/components/approval.jsx b/web/src/screens/repo/screens/build/components/approval.jsx deleted file mode 100644 index 9936cfc4b5..0000000000 --- a/web/src/screens/repo/screens/build/components/approval.jsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from "react"; -import style from "./approval.less"; - -export const Approval = ({ onapprove, ondecline }) => ( - <div className={style.root}> - <p>Pipeline execution is blocked pending administrator approval</p> - <button onClick={onapprove}>Approve</button> - <button onClick={ondecline}>Decline</button> - </div> -); diff --git a/web/src/screens/repo/screens/build/components/details.jsx b/web/src/screens/repo/screens/build/components/details.jsx deleted file mode 100644 index ff137d1674..0000000000 --- a/web/src/screens/repo/screens/build/components/details.jsx +++ /dev/null @@ -1,47 +0,0 @@ -import React, { Component } from "react"; - -import BuildMeta from "shared/components/build_event"; -import BuildTime from "shared/components/build_time"; -import { StatusLabel } from "shared/components/status"; - -import styles from "./details.less"; - -export class Details extends Component { - render() { - const { build } = this.props; - - return ( - <div className={styles.info}> - <StatusLabel status={build.status} /> - - <section className={styles.message} style={{ whiteSpace: "pre-line" }}> - {build.message} - </section> - - <section> - <div className={styles.author}> - <img src={build.author_avatar} /> - <span>{build.author}</span> - </div> - - <BuildTime - start={build.started_at || build.created_at} - finish={build.finished_at} - /> - </section> - - <section> - <BuildMeta - link={build.link_url} - event={build.event} - commit={build.commit} - branch={build.branch} - target={build.deploy_to} - refspec={build.refspec} - refs={build.ref} - /> - </section> - </div> - ); - } -} diff --git a/web/src/screens/repo/screens/build/components/elapsed.jsx b/web/src/screens/repo/screens/build/components/elapsed.jsx deleted file mode 100644 index e3b7ec18ca..0000000000 --- a/web/src/screens/repo/screens/build/components/elapsed.jsx +++ /dev/null @@ -1,63 +0,0 @@ -import React, { Component } from "react"; - -export class Elapsed extends Component { - constructor(props, context) { - super(props); - - this.state = { - elapsed: 0, - }; - - this.tick = this.tick.bind(this); - } - - componentDidMount() { - this.timer = setInterval(this.tick, 1000); - } - - componentWillUnmount() { - clearInterval(this.timer); - } - - tick() { - const { start } = this.props; - const stop = ~~(Date.now() / 1000); - this.setState({ - elapsed: stop - start, - }); - } - - render() { - const { elapsed } = this.state; - const date = new Date(null); - date.setSeconds(elapsed); - return ( - <time> - {!elapsed ? ( - undefined - ) : elapsed > 3600 ? ( - date.toISOString().substr(11, 8) - ) : ( - date.toISOString().substr(14, 5) - )} - </time> - ); - } -} - -/* - * Returns the duration in hh:mm:ss format. - * - * @param {number} from - The start time in secnds - * @param {number} to - The end time in seconds - * @return {string} - */ -export const formatTime = (end, start) => { - const diff = end - start; - const date = new Date(null); - date.setSeconds(diff); - - return diff > 3600 - ? date.toISOString().substr(11, 8) - : date.toISOString().substr(14, 5); -}; diff --git a/web/src/screens/repo/screens/build/components/index.jsx b/web/src/screens/repo/screens/build/components/index.jsx deleted file mode 100644 index a7fc1de149..0000000000 --- a/web/src/screens/repo/screens/build/components/index.jsx +++ /dev/null @@ -1,5 +0,0 @@ -import { Approval } from "./approval"; -import { Details } from "./details"; -import { ProcList, ProcListItem } from "./procs"; - -export { Approval, Details, ProcList, ProcListItem }; diff --git a/web/src/screens/repo/screens/build/components/procs.jsx b/web/src/screens/repo/screens/build/components/procs.jsx deleted file mode 100644 index 4da793acd0..0000000000 --- a/web/src/screens/repo/screens/build/components/procs.jsx +++ /dev/null @@ -1,100 +0,0 @@ -import React, { Component } from "react"; -import { Link } from "react-router-dom"; -import classnames from "classnames"; - -import { Elapsed, formatTime } from "./elapsed"; -import { default as Status, StatusText } from "shared/components/status"; - -import styles from "./procs.less"; - -const renderEnviron = data => { - return ( - <div> - {data[0]}={data[1]} - </div> - ); -}; - -class ProcListHolder extends Component { - constructor(props, context) { - super(props, context); - this.state = { open: !this.props.renderName }; - } - - toggleOpen = () => { - this.setState({ open: !this.state.open }); - }; - - render() { - const { vars, renderName, children } = this.props; - const groupExpandStatus = this.state.open - ? styles.collapsed - : styles.expanded; - - return ( - <div className={styles.list}> - {renderName && vars.name !== "drone" ? ( - <div - onClick={this.toggleOpen} - className={`${styles.group} ${groupExpandStatus}`} - > - <StatusText status={vars.state} text={vars.name} /> - </div> - ) : null} - {vars.environ ? ( - <div - onClick={this.toggleOpen} - className={`${styles.group} ${groupExpandStatus}`} - > - <StatusText - status={vars.state} - text={Object.entries(vars.environ).map(renderEnviron)} - /> - </div> - ) : null} - <div className={!this.state.open ? styles.hide : ""}>{children}</div> - </div> - ); - } -} - -export class ProcList extends Component { - render() { - const { repo, build, rootProc, selectedProc, renderName } = this.props; - return ( - <ProcListHolder vars={rootProc} renderName={renderName}> - {this.props.rootProc.children.map(function(child) { - return ( - <Link - to={`/${repo.full_name}/${build.number}/${child.pid}`} - key={`${repo.full_name}-${build.number}-${child.pid}`} - > - <ProcListItem - key={child.pid} - name={child.name} - start={child.start_time} - finish={child.end_time} - state={child.state} - selected={child.pid === selectedProc.pid} - /> - </Link> - ); - })} - </ProcListHolder> - ); - } -} - -export const ProcListItem = ({ name, start, finish, state, selected }) => ( - <div className={classnames(styles.item, selected ? styles.selected : null)}> - <h3>{name}</h3> - {finish ? ( - <time>{formatTime(finish, start)}</time> - ) : ( - <Elapsed start={start} /> - )} - <div> - <Status status={state} /> - </div> - </div> -); diff --git a/web/src/screens/repo/screens/build/index.jsx b/web/src/screens/repo/screens/build/index.jsx deleted file mode 100644 index f5ab148e7a..0000000000 --- a/web/src/screens/repo/screens/build/index.jsx +++ /dev/null @@ -1,257 +0,0 @@ -import React, { Component } from "react"; -import { Link } from "react-router-dom"; - -import { fetchBuild, approveBuild, declineBuild } from "shared/utils/build"; -import { - STATUS_BLOCKED, - STATUS_DECLINED, - STATUS_ERROR, -} from "shared/constants/status"; - -import { findChildProcess } from "shared/utils/proc"; -import { fetchRepository } from "shared/utils/repository"; - -import Breadcrumb, { SEPARATOR } from "shared/components/breadcrumb"; - -import { Approval, Details, ProcList } from "./components"; - -import { branch } from "baobab-react/higher-order"; -import { inject } from "config/client/inject"; - -import Output from "./logs"; - -import styles from "./index.less"; - -const binding = (props, context) => { - const { owner, repo, build } = props.match.params; - const slug = `${owner}/${repo}`; - const number = parseInt(build); - - return { - repo: ["repos", "data", slug], - build: ["builds", "data", slug, number], - }; -}; - -@inject -@branch(binding) -export default class BuildLogs extends Component { - constructor(props, context) { - super(props, context); - - this.handleApprove = this.handleApprove.bind(this); - this.handleDecline = this.handleDecline.bind(this); - } - - componentWillMount() { - this.synchronize(this.props); - } - - handleApprove() { - const { repo, build, drone } = this.props; - this.props.dispatch( - approveBuild, - drone, - repo.owner, - repo.name, - build.number, - ); - } - - handleDecline() { - const { repo, build, drone } = this.props; - this.props.dispatch( - declineBuild, - drone, - repo.owner, - repo.name, - build.number, - ); - } - - componentWillUpdate(nextProps) { - if (this.props.match.url !== nextProps.match.url) { - this.synchronize(nextProps); - } - } - - synchronize(props) { - if (!props.repo) { - this.props.dispatch( - fetchRepository, - props.drone, - props.match.params.owner, - props.match.params.repo, - ); - } - if (!props.build || !props.build.procs) { - this.props.dispatch( - fetchBuild, - props.drone, - props.match.params.owner, - props.match.params.repo, - props.match.params.build, - ); - } - } - - shouldComponentUpdate(nextProps, nextState) { - return this.props !== nextProps; - } - - render() { - const { repo, build } = this.props; - - if (!build || !repo) { - return this.renderLoading(); - } - - if (build.status === STATUS_DECLINED || build.status === STATUS_ERROR) { - return this.renderError(); - } - - if (build.status === STATUS_BLOCKED) { - return this.renderBlocked(); - } - - if (!build.procs) { - return this.renderLoading(); - } - - return this.renderSimple(); - } - - renderLoading() { - return ( - <div className={styles.host}> - <div className={styles.columns}> - <div className={styles.right}>Loading ...</div> - </div> - </div> - ); - } - - renderBlocked() { - const { build } = this.props; - return ( - <div className={styles.host}> - <div className={styles.columns}> - <div className={styles.right}> - <Details build={build} /> - </div> - <div className={styles.left}> - <Approval - onapprove={this.handleApprove} - ondecline={this.handleDecline} - /> - </div> - </div> - </div> - ); - } - - renderError() { - const { build } = this.props; - return ( - <div className={styles.host}> - <div className={styles.columns}> - <div className={styles.right}> - <Details build={build} /> - </div> - <div className={styles.left}> - <div className={styles.logerror}> - {build.status === STATUS_ERROR ? ( - build.error - ) : ( - "Pipeline execution was declined" - )} - </div> - </div> - </div> - </div> - ); - } - - highlightedLine() { - if (location.hash.startsWith("#L")) { - return parseInt(location.hash.substr(2)) - 1; - } - - return undefined; - } - - renderSimple() { - // if (nextProps.build.procs[0].children !== undefined){ - // return null; - // } - - const { repo, build, match } = this.props; - const selectedProc = match.params.proc - ? findChildProcess(build.procs, match.params.proc) - : build.procs[0].children[0]; - const selectedProcParent = findChildProcess(build.procs, selectedProc.ppid); - const highlighted = this.highlightedLine(); - - return ( - <div className={styles.host}> - <div className={styles.columns}> - <div className={styles.right}> - <Details build={build} /> - <section className={styles.sticky}> - {build.procs.map(function(rootProc) { - return ( - <div style="padding-bottom: 20px;" key={rootProc.pid}> - <ProcList - key={rootProc.pid} - repo={repo} - build={build} - rootProc={rootProc} - selectedProc={selectedProc} - renderName={build.procs.length > 1} - /> - </div> - ); - })} - </section> - </div> - <div className={styles.left}> - {selectedProc && selectedProc.error ? ( - <div className={styles.logerror}>{selectedProc.error}</div> - ) : null} - {selectedProcParent && selectedProcParent.error ? ( - <div className={styles.logerror}>{selectedProcParent.error}</div> - ) : null} - <Output - match={this.props.match} - build={this.props.build} - proc={selectedProc} - highlighted={highlighted} - /> - </div> - </div> - </div> - ); - } -} - -export class BuildLogsTitle extends Component { - render() { - const { owner, repo, build } = this.props.match.params; - return ( - <Breadcrumb - elements={[ - <Link to={`/${owner}/${repo}`} key={`${owner}-${repo}`}> - {owner} / {repo} - </Link>, - SEPARATOR, - <Link - to={`/${owner}/${repo}/${build}`} - key={`${owner}-${repo}-${build}`} - > - {build} - </Link>, - ]} - /> - ); - } -} diff --git a/web/src/screens/repo/screens/build/logs/components/anchor.jsx b/web/src/screens/repo/screens/build/logs/components/anchor.jsx deleted file mode 100644 index 17370fcd8d..0000000000 --- a/web/src/screens/repo/screens/build/logs/components/anchor.jsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from "react"; - -import styles from "./anchor.less"; - -export const Top = () => <div className={styles.top} />; - -export const Bottom = () => <div className={styles.bottom} />; - -export const scrollToTop = () => { - document.querySelector(`.${styles.top}`).scrollIntoView(); -}; - -export const scrollToBottom = () => { - document.querySelector(`.${styles.bottom}`).scrollIntoView(); -}; diff --git a/web/src/screens/repo/screens/build/logs/components/term.jsx b/web/src/screens/repo/screens/build/logs/components/term.jsx deleted file mode 100644 index 6fc74d5be5..0000000000 --- a/web/src/screens/repo/screens/build/logs/components/term.jsx +++ /dev/null @@ -1,93 +0,0 @@ -import React, { Component } from "react"; -import AnsiUp from "ansi_up"; -import style from "./term.less"; -import { Link } from "react-router-dom"; - -let formatter = new AnsiUp(); -formatter.use_classes = true; - -class Term extends Component { - render() { - const { lines, exitcode, highlighted } = this.props; - return ( - <div className={style.term}> - {lines.map(line => renderTermLine(line, highlighted))} - {exitcode !== undefined ? renderExitCode(exitcode) : undefined} - </div> - ); - } - - shouldComponentUpdate(nextProps, nextState) { - return ( - this.props.lines !== nextProps.lines || - this.props.exitcode !== nextProps.exitcode || - this.props.highlighted !== nextProps.highlighted - ); - } -} - -class TermLine extends Component { - render() { - const { line, highlighted } = this.props; - return ( - <div - className={highlighted === line.pos ? style.highlight : style.line} - key={line.pos} - ref={highlighted === line.pos ? ref => (this.ref = ref) : null} - > - <div> - <Link to={`#L${line.pos + 1}`} key={line.pos + 1}> - {line.pos + 1} - </Link> - </div> - <div dangerouslySetInnerHTML={{ __html: this.colored }} /> - <div>{line.time || 0}s</div> - </div> - ); - } - - componentDidMount() { - if (this.ref !== undefined) { - scrollToRef(this.ref); - } - } - - get colored() { - return formatter.ansi_to_html(this.props.line.out || ""); - } - - shouldComponentUpdate(nextProps, nextState) { - return ( - this.props.line.out !== nextProps.line.out || - this.props.highlighted !== nextProps.highlighted - ); - } -} - -const renderTermLine = (line, highlighted) => { - return <TermLine line={line} highlighted={highlighted} />; -}; - -const renderExitCode = code => { - return <div className={style.exitcode}>exit code {code}</div>; -}; - -const TermError = () => { - return ( - <div className={style.error}> - Oops. There was a problem loading the logs. - </div> - ); -}; - -const TermLoading = () => { - return <div className={style.loading}>Loading ...</div>; -}; - -const scrollToRef = ref => window.scrollTo(0, ref.offsetTop - 100); - -Term.Line = TermLine; -Term.Error = TermError; -Term.Loading = TermLoading; - -export default Term; diff --git a/web/src/screens/repo/screens/build/logs/index.jsx b/web/src/screens/repo/screens/build/logs/index.jsx deleted file mode 100644 index 384ab3af20..0000000000 --- a/web/src/screens/repo/screens/build/logs/index.jsx +++ /dev/null @@ -1,143 +0,0 @@ -import React, { Component } from "react"; -import { inject } from "config/client/inject"; -import { branch } from "baobab-react/higher-order"; -import { repositorySlug } from "shared/utils/repository"; -import { assertProcFinished, assertProcRunning } from "shared/utils/proc"; -import { fetchLogs, subscribeToLogs, toggleLogs } from "shared/utils/logs"; - -import Term from "./components/term"; - -import { Top, Bottom, scrollToTop, scrollToBottom } from "./components/anchor"; - -import { ExpandIcon, PauseIcon, PlayIcon } from "shared/components/icons/index"; - -import styles from "./index.less"; - -const binding = (props, context) => { - const { owner, repo, build } = props.match.params; - const slug = repositorySlug(owner, repo); - const number = parseInt(build); - const pid = parseInt(props.proc.pid); - - return { - logs: ["logs", "data", slug, number, pid, "data"], - eof: ["logs", "data", slug, number, pid, "eof"], - loading: ["logs", "data", slug, number, pid, "loading"], - error: ["logs", "data", slug, number, pid, "error"], - follow: ["logs", "follow"], - }; -}; - -@inject -@branch(binding) -export default class Output extends Component { - constructor(props, context) { - super(props, context); - this.handleFollow = this.handleFollow.bind(this); - } - - componentWillMount() { - if (this.props.proc) { - this.componentWillUpdate(this.props); - } - } - - componentWillUpdate(nextProps) { - const { loading, logs, eof, error } = nextProps; - const routeChange = this.props.match.url !== nextProps.match.url; - - if (loading || error || (logs && eof)) { - return; - } - - if (assertProcFinished(nextProps.proc)) { - return this.props.dispatch( - fetchLogs, - nextProps.drone, - nextProps.match.params.owner, - nextProps.match.params.repo, - nextProps.build.number, - nextProps.proc.pid, - ); - } - - if (assertProcRunning(nextProps.proc) && (!logs || routeChange)) { - this.props.dispatch( - subscribeToLogs, - nextProps.drone, - nextProps.match.params.owner, - nextProps.match.params.repo, - nextProps.build.number, - nextProps.proc, - ); - } - } - - componentDidUpdate() { - if (this.props.follow) { - scrollToBottom(); - } - } - - handleFollow() { - this.props.dispatch(toggleLogs, !this.props.follow); - } - - render() { - const { logs, error, proc, loading, follow, highlighted } = this.props; - - if (loading || !proc) { - return <Term.Loading />; - } - - if (error) { - return <Term.Error />; - } - - return ( - <div> - <Top /> - <Term - lines={logs || []} - highlighted={highlighted} - exitcode={assertProcFinished(proc) ? proc.exit_code : undefined} - /> - <Bottom /> - <Actions - running={assertProcRunning(proc)} - following={follow} - onfollow={this.handleFollow} - onunfollow={this.handleFollow} - /> - </div> - ); - } -} - -/** - * Component renders floating log actions. These can be used - * to follow, unfollow, scroll to top and scroll to bottom. - */ -const Actions = ({ following, running, onfollow, onunfollow }) => ( - <div className={styles.actions}> - {running && !following ? ( - <button onClick={onfollow} className={styles.follow}> - <PlayIcon /> - </button> - ) : null} - - {running && following ? ( - <button onClick={onunfollow} className={styles.unfollow}> - <PauseIcon /> - </button> - ) : null} - - <button onClick={scrollToTop} className={styles.bottom}> - <ExpandIcon /> - </button> - - <button onClick={scrollToBottom} className={styles.top}> - <ExpandIcon /> - </button> - </div> -); diff --git a/web/src/screens/repo/screens/build/menu.jsx b/web/src/screens/repo/screens/build/menu.jsx deleted file mode 100644 index 51780d0553..0000000000 --- a/web/src/screens/repo/screens/build/menu.jsx +++ /dev/null @@ -1,78 +0,0 @@ -import React, { Component } from "react"; -import RepoMenu from "../builds/menu"; -import { RefreshIcon, CloseIcon } from "shared/components/icons"; - -import { cancelBuild, restartBuild } from "shared/utils/build"; -import { findChildProcess } from "shared/utils/proc"; -import { repositorySlug } from "shared/utils/repository"; - -import { branch } from "baobab-react/higher-order"; -import { inject } from "config/client/inject"; - -const binding = (props, context) => { - const { owner, repo, build } = props.match.params; - const slug = repositorySlug(owner, repo); - const number = parseInt(build); - return { - repo: ["repos", "data", slug], - build: ["builds", "data", slug, number], - }; -}; - -@inject -@branch(binding) -export default class BuildMenu extends Component { - constructor(props, context) { - super(props, context); - - this.handleCancel = this.handleCancel.bind(this); - this.handleRestart = this.handleRestart.bind(this); - } - - handleRestart() { - const { dispatch, drone, repo, build } = this.props; - dispatch(restartBuild, drone, repo.owner, repo.name, build.number); - } - - handleCancel() { - const { dispatch, drone, repo, build, match } = this.props; - const proc = findChildProcess(build.procs, match.params.proc || 2); - - dispatch( - cancelBuild, - drone, - repo.owner, - repo.name, - build.number, - proc.ppid, - ); - } - - render() { - const { build } = this.props; - - const rightSide = !build ? ( - undefined - ) : ( - <section> - {build.status === "pending" || build.status === "running" ? ( - <button onClick={this.handleCancel}> - <CloseIcon /> - <span>Cancel</span> - </button> - ) : ( - <button onClick={this.handleRestart}> - <RefreshIcon /> - <span>Restart Build</span> - </button> - )} - </section> - ); - - return ( - <div> - <RepoMenu {...this.props} right={rightSide} /> - </div> - ); - } -} diff --git a/web/src/screens/repo/screens/builds/components/index.jsx b/web/src/screens/repo/screens/builds/components/index.jsx deleted file mode 100644 index 4ad6b4fdab..0000000000 --- a/web/src/screens/repo/screens/builds/components/index.jsx +++ /dev/null @@ -1,3 +0,0 @@ -import { List, Item } from "./list"; - -export { List, Item }; diff --git a/web/src/screens/repo/screens/builds/components/list.jsx b/web/src/screens/repo/screens/builds/components/list.jsx deleted file mode 100644 index 8ffec50d06..0000000000 --- a/web/src/screens/repo/screens/builds/components/list.jsx +++ /dev/null @@ -1,55 +0,0 @@ -import React, { Component } from "react"; - -import Status from "shared/components/status"; -import StatusNumber from "shared/components/status_number"; -import BuildTime from "shared/components/build_time"; -import BuildMeta from "shared/components/build_event"; - -import styles from "./list.less"; - -export const List = ({ children }) => ( - <div className={styles.list}>{children}</div> -); - -export class Item extends Component { - render() { - const { build } = this.props; - return ( - <div className={styles.item}> - <div className={styles.icon}> - <img src={build.author_avatar} title={build.author} /> - </div> - - <div className={styles.body}> - <h3>{build.message.split("\n")[0]}</h3> - </div> - - <div className={styles.meta}> - <BuildMeta - link={build.link_url} - event={build.event} - commit={build.commit} - branch={build.branch} - target={build.deploy_to} - refspec={build.refspec} - refs={build.ref} - /> - </div> - - <div className={styles.break} /> - - <div className={styles.time}> - <BuildTime - start={build.started_at || build.created_at} - finish={build.finished_at} - /> - </div> - - <div className={styles.status}> - <StatusNumber status={build.status} number={build.number} /> - <Status status={build.status} /> - </div> - </div> - ); - } -} diff --git a/web/src/screens/repo/screens/builds/header.jsx b/web/src/screens/repo/screens/builds/header.jsx deleted file mode 100644 index e82650c6f4..0000000000 --- a/web/src/screens/repo/screens/builds/header.jsx +++ /dev/null @@ -1,20 +0,0 @@ -import React, { Component } from "react"; -import { Link } from "react-router-dom"; -import Breadcrumb from "shared/components/breadcrumb"; - -export default class Header extends Component { - render() { - const { owner, repo } = this.props.match.params; - return ( - <div> - <Breadcrumb - elements={[ - <Link to={`/${owner}/${repo}`} key={`${owner}-${repo}`}> - {owner} / {repo} - </Link>, - ]} - /> - </div> - ); - } -} diff --git a/web/src/screens/repo/screens/builds/index.jsx b/web/src/screens/repo/screens/builds/index.jsx deleted file mode 100644 index cf3f7b0348..0000000000 --- a/web/src/screens/repo/screens/builds/index.jsx +++ /dev/null @@ -1,153 +0,0 @@ -import React, { Component } from "react"; -import { Link } from "react-router-dom"; -import { List, Item } from "./components"; - -import { fetchBuildList, compareBuild } from "shared/utils/build"; -import { fetchRepository, repositorySlug } from "shared/utils/repository"; - -import { branch } from "baobab-react/higher-order"; -import { inject } from "config/client/inject"; - -import styles from "./index.less"; - -const binding = (props, context) => { - const { owner, repo } = props.match.params; - const slug = repositorySlug(owner, repo); - return { - repo: ["repos", "data", slug], - builds: ["builds", "data", slug], - loaded: ["builds", "loaded"], - error: ["builds", "error"], - }; -}; - -@inject -@branch(binding) -export default class Main extends Component { - constructor(props, context) { - super(props, context); - - this.fetchNextBuildPage = this.fetchNextBuildPage.bind(this); - this.selectBranch = this.selectBranch.bind(this); - } - - componentWillMount() { - this.synchronize(this.props); - } - - shouldComponentUpdate(nextProps, nextState) { - return ( - this.props.repo !== nextProps.repo || - (nextProps.builds !== undefined && - this.props.builds !== nextProps.builds) || - this.props.error !== nextProps.error || - this.props.loaded !== nextProps.loaded || - this.state.branch !== nextState.branch - ); - } - - componentWillUpdate(nextProps) { - if (this.props.match.url !== nextProps.match.url) { - this.synchronize(nextProps); - } - } - - componentDidUpdate(prevProps) { - if (this.props.location !== prevProps.location) { - window.scrollTo(0, 0); - } - } - - synchronize(props) { - const { drone, dispatch, match, repo } = props; - - if (!repo) { - dispatch(fetchRepository, drone, match.params.owner, match.params.repo); - } - - dispatch(fetchBuildList, drone, match.params.owner, match.params.repo); - } - - fetchNextBuildPage(buildList) { - const { drone, dispatch, match } = this.props; - const page = Math.floor(buildList.length / 50) + 1; - - dispatch( - fetchBuildList, - drone, - match.params.owner, - match.params.repo, - page, - ); - } - - selectBranch(branch) { - this.setState({ - branch: branch, - }); - } - - render() { - const { repo, builds, loaded, error } = this.props; - const { branch } = this.state; - const list = Object.values(builds || {}); - - function renderBuild(build) { - return ( - <Link to={`/${repo.full_name}/${build.number}`} key={build.number}> - <Item build={build} /> - </Link> - ); - } - - const filterBranch = build => { - return !branch || build.branch === branch; - }; - - if (error) { - return <div>Not Found</div>; - } - - if (!loaded && list.length === 0) { - return <div>Loading</div>; - } - - if (!repo) { - return <div>Loading</div>; - } - - if (list.length === 0) { - return <div>Build list is empty</div>; - } - - return ( - <div className={styles.root}> - <div className={styles.right}> - {!branch ? ( - <button onClick={() => this.selectBranch(repo.default_branch)}> - Show {repo.default_branch} branch only - </button> - ) : ( - <button onClick={() => this.selectBranch(undefined)}> - Show all branches - </button> - )} - </div> - <List> - {list - .sort(compareBuild) - .filter(filterBranch) - .map(renderBuild)} - </List> - {list.length < repo.last_build && ( - <button - onClick={() => this.fetchNextBuildPage(list)} - className={styles.more} - > - Show more builds - </button> - )} - </div> - ); - } -} diff --git a/web/src/screens/repo/screens/builds/menu.jsx b/web/src/screens/repo/screens/builds/menu.jsx deleted file mode 100644 index 71788860df..0000000000 --- a/web/src/screens/repo/screens/builds/menu.jsx +++ /dev/null @@ -1,15 +0,0 @@ -import React, { Component } from "react"; -import Menu from "shared/components/menu"; - -export default class RepoMenu extends Component { - render() { - const { owner, repo } = this.props.match.params; - const menu = [ - { to: `/${owner}/${repo}`, label: "Builds" }, - { to: `/${owner}/${repo}/settings/secrets`, label: "Secrets" }, - { to: `/${owner}/${repo}/settings/registry`, label: "Registry" }, - { to: `/${owner}/${repo}/settings`, label: "Settings" }, - ]; - return <Menu items={menu} {...this.props} />; - } -} diff --git a/web/src/screens/repo/screens/registry/components/form.jsx b/web/src/screens/repo/screens/registry/components/form.jsx deleted file mode 100644 index b9e71592fe..0000000000 --- a/web/src/screens/repo/screens/registry/components/form.jsx +++ /dev/null @@ -1,80 +0,0 @@ -import React, { Component } from "react"; -import styles from "./form.less"; - -export class Form extends Component { - constructor(props, context) { - super(props, context); - - this.state = { - address: "", - username: "", - password: "", - }; - - this._handleAddressChange = this._handleAddressChange.bind(this); - this._handleUsernameChange = this._handleUsernameChange.bind(this); - this._handlePasswordChange = this._handlePasswordChange.bind(this); - this._handleSubmit = this._handleSubmit.bind(this); - - this.clear = this.clear.bind(this); - } - - _handleAddressChange(event) { - this.setState({ address: event.target.value }); - } - - _handleUsernameChange(event) { - this.setState({ username: event.target.value }); - } - - _handlePasswordChange(event) { - this.setState({ password: event.target.value }); - } - - _handleSubmit() { - const { onsubmit } = this.props; - - const detail = { - address: this.state.address, - username: this.state.username, - password: this.state.password, - }; - - onsubmit({ detail }); - this.clear(); - } - - clear() { - this.setState({ address: "" }); - this.setState({ username: "" }); - this.setState({ password: "" }); - } - - render() { - return ( - <div className={styles.form}> - <input - type="text" - value={this.state.address} - onChange={this._handleAddressChange} - placeholder="Registry Address (e.g. docker.io)" - /> - <input - type="text" - value={this.state.username} - onChange={this._handleUsernameChange} - placeholder="Registry Username" - /> - <textarea - rows="1" - value={this.state.password} - onChange={this._handlePasswordChange} - placeholder="Registry Password" - /> - <div className={styles.actions}> - <button onClick={this._handleSubmit}>Save</button> - </div> - </div> - ); - } -} diff --git a/web/src/screens/repo/screens/registry/components/index.jsx b/web/src/screens/repo/screens/registry/components/index.jsx deleted file mode 100644 index ecd6b3500f..0000000000 --- a/web/src/screens/repo/screens/registry/components/index.jsx +++ /dev/null @@ -1,4 +0,0 @@ -import { Form } from "./form"; -import { List, Item } from "./list"; - -export { Form, List, Item }; diff --git a/web/src/screens/repo/screens/registry/components/list.jsx b/web/src/screens/repo/screens/registry/components/list.jsx deleted file mode 100644 index db084782bb..0000000000 --- a/web/src/screens/repo/screens/registry/components/list.jsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from "react"; -import styles from "./list.less"; - -export const List = ({ children }) => ( - <div className={styles.list}>{children}</div> -); - -export const Item = props => ( - <div className={styles.item} key={props.name}> - <div>{props.name}</div> - <div> - <button onClick={props.ondelete}>delete</button> - </div> - </div> -); diff --git a/web/src/screens/repo/screens/registry/index.jsx b/web/src/screens/repo/screens/registry/index.jsx deleted file mode 100644 index fe6fbd0e0f..0000000000 --- a/web/src/screens/repo/screens/registry/index.jsx +++ /dev/null @@ -1,103 +0,0 @@ -import React, { Component } from "react"; - -import { repositorySlug } from "shared/utils/repository"; -import { - fetchRegistryList, - createRegistry, - deleteRegistry, -} from "shared/utils/registry"; - -import { branch } from "baobab-react/higher-order"; -import { inject } from "config/client/inject"; - -import { List, Item, Form } from "./components"; - -import styles from "./index.less"; - -const binding = (props, context) => { - const { owner, repo } = props.match.params; - const slug = repositorySlug(owner, repo); - return { - loaded: ["registry", "loaded"], - registries: ["registry", "data", slug], - }; -}; - -@inject -@branch(binding) -export default class RepoRegistry extends Component { - constructor(props, context) { - super(props, context); - - this.handleDelete = this.handleDelete.bind(this); - this.handleSave = this.handleSave.bind(this); - } - - shouldComponentUpdate(nextProps, nextState) { - return this.props.registries !== nextProps.registries; - } - - componentWillMount() { - const { dispatch, drone, match } = this.props; - const { owner, repo } = match.params; - dispatch(fetchRegistryList, drone, owner, repo); - } - - handleSave(e) { - const { dispatch, drone, match } = this.props; - const { owner, repo } = match.params; - const registry = { - address: e.detail.address, - username: e.detail.username, - password: e.detail.password, - }; - - dispatch(createRegistry, drone, owner, repo, registry); - } - - handleDelete(registry) { - const { dispatch, drone, match } = this.props; - const { owner, repo } = match.params; - dispatch(deleteRegistry, drone, owner, repo, registry.address); - } - - render() { - const { registries, loaded } = this.props; - - if (!loaded) { - return LOADING; - } - - return ( - <div className={styles.root}> - <div className={styles.left}> - {Object.keys(registries || {}).length === 0 ? EMPTY : undefined} - <List> - {Object.values(registries || {}).map(renderRegistry.bind(this))} - </List> - </div> - - <div className={styles.right}> - <Form onsubmit={this.handleSave} /> - </div> - </div> - ); - } -} - -function renderRegistry(registry) { - return ( - <Item - name={registry.address} - ondelete={this.handleDelete.bind(this, registry)} - /> - ); -} - -const LOADING = <div className={styles.loading}>Loading</div>; - -const EMPTY = ( - <div className={styles.empty}> - There are no registry credentials for this repository. - </div> -); diff --git a/web/src/screens/repo/screens/secrets/components/form.jsx b/web/src/screens/repo/screens/secrets/components/form.jsx deleted file mode 100644 index 183e7520da..0000000000 --- a/web/src/screens/repo/screens/secrets/components/form.jsx +++ /dev/null @@ -1,140 +0,0 @@ -import React, { Component } from "react"; - -import { - EVENT_PUSH, - EVENT_TAG, - EVENT_PULL_REQUEST, - EVENT_DEPLOY, -} from "shared/constants/events"; - -import styles from "./form.less"; - -export class Form extends Component { - constructor(props, context) { - super(props, context); - - this.state = { - name: "", - value: "", - event: [EVENT_PUSH, EVENT_TAG, EVENT_DEPLOY], - }; - - this._handleNameChange = this._handleNameChange.bind(this); - this._handleValueChange = this._handleValueChange.bind(this); - this._handleEventChange = this._handleEventChange.bind(this); - this._handleSubmit = this._handleSubmit.bind(this); - - this.clear = this.clear.bind(this); - } - - _handleNameChange(event) { - this.setState({ name: event.target.value }); - } - - _handleValueChange(event) { - this.setState({ value: event.target.value }); - } - - _handleEventChange(event) { - const selected = this.state.event; - let index; - - if (event.target.checked) { - selected.push(event.target.value); - } else { - index = selected.indexOf(event.target.value); - selected.splice(index, 1); - } - - this.setState({ event: selected }); - } - - _handleSubmit() { - const { onsubmit } = this.props; - - const detail = { - name: this.state.name, - value: this.state.value, - event: this.state.event, - }; - - onsubmit({ detail }); - this.clear(); - } - - clear() { - this.setState({ name: "" }); - this.setState({ value: "" }); - this.setState({ event: [EVENT_PUSH, EVENT_TAG, EVENT_DEPLOY] }); - } - - render() { - let checked = this.state.event.reduce((map, event) => { - map[event] = true; - return map; - }, {}); - - return ( - <div className={styles.form}> - <input - type="text" - name="name" - value={this.state.name} - placeholder="Secret Name" - onChange={this._handleNameChange} - /> - <textarea - rows="1" - name="value" - value={this.state.value} - placeholder="Secret Value" - onChange={this._handleValueChange} - /> - <section> - <h2>Events</h2> - <div> - <label> - <input - type="checkbox" - checked={checked[EVENT_PUSH]} - value={EVENT_PUSH} - onChange={this._handleEventChange} - /> - <span>push</span> - </label> - <label> - <input - type="checkbox" - checked={checked[EVENT_TAG]} - value={EVENT_TAG} - onChange={this._handleEventChange} - /> - <span>tag</span> - </label> - <label> - <input - type="checkbox" - checked={checked[EVENT_PULL_REQUEST]} - value={EVENT_PULL_REQUEST} - onChange={this._handleEventChange} - /> - <span>pull request</span> - </label> - <label> - <input - type="checkbox" - checked={checked[EVENT_DEPLOY]} - value={EVENT_DEPLOY} - onChange={this._handleEventChange} - /> - <span>deploy</span> - </label> - </div> - </section> - <div className={styles.actions}> - <button onClick={this._handleSubmit}>Save</button> - </div> - </div> - ); - } -} diff --git a/web/src/screens/repo/screens/secrets/components/index.jsx b/web/src/screens/repo/screens/secrets/components/index.jsx deleted file mode 100644 index ecd6b3500f..0000000000 --- a/web/src/screens/repo/screens/secrets/components/index.jsx +++ /dev/null @@ -1,4 +0,0 @@ -import { Form } from "./form"; -import { List, Item } from "./list"; - -export { Form, List, Item }; diff --git a/web/src/screens/repo/screens/secrets/components/list.jsx b/web/src/screens/repo/screens/secrets/components/list.jsx deleted file mode 100644 index 7fb80ba78e..0000000000 --- a/web/src/screens/repo/screens/secrets/components/list.jsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from "react"; -import styles from "./list.less"; - -export const List = ({ children }) => <div>{children}</div>; - -export const Item = props => ( - <div className={styles.item} key={props.name}> - <div> - {props.name} - <ul>{props.event ? props.event.map(renderEvent) : null}</ul> - </div> - <div> - <button onClick={props.ondelete}>delete</button> - </div> - </div> -); - -const renderEvent = event => { - return <li>{event}</li>; -}; diff --git a/web/src/screens/repo/screens/secrets/index.jsx b/web/src/screens/repo/screens/secrets/index.jsx deleted file mode 100644 index c07dddac12..0000000000 --- a/web/src/screens/repo/screens/secrets/index.jsx +++ /dev/null @@ -1,99 +0,0 @@ -import React, { Component } from "react"; - -import { repositorySlug } from "shared/utils/repository"; -import { - fetchSecretList, - createSecret, - deleteSecret, -} from "shared/utils/secrets"; - -import { branch } from "baobab-react/higher-order"; -import { inject } from "config/client/inject"; - -import { List, Item, Form } from "./components"; - -import styles from "./index.less"; - -const binding = (props, context) => { - const { owner, repo } = props.match.params; - const slug = repositorySlug(owner, repo); - return { - loaded: ["secrets", "loaded"], - secrets: ["secrets", "data", slug], - }; -}; - -@inject -@branch(binding) -export default class RepoSecrets extends Component { - constructor(props, context) { - super(props, context); - - this.handleSave = this.handleSave.bind(this); - } - - shouldComponentUpdate(nextProps, nextState) { - return this.props.secrets !== nextProps.secrets; - } - - componentWillMount() { - const { owner, repo } = this.props.match.params; - this.props.dispatch(fetchSecretList, this.props.drone, owner, repo); - } - - handleSave(e) { - const { dispatch, drone, match } = this.props; - const { owner, repo } = match.params; - const secret = { - name: e.detail.name, - value: e.detail.value, - event: e.detail.event, - }; - - dispatch(createSecret, drone, owner, repo, secret); - } - - handleDelete(secret) { - const { dispatch, drone, match } = this.props; - const { owner, repo } = match.params; - dispatch(deleteSecret, drone, owner, repo, secret.name); - } - - render() { - const { secrets, loaded } = this.props; - - if (!loaded) { - return LOADING; - } - - return ( - <div className={styles.root}> - <div className={styles.left}> - {Object.keys(secrets || {}).length === 0 ? EMPTY : undefined} - <List> - {Object.values(secrets || {}).map(renderSecret.bind(this))} - </List> - </div> - <div className={styles.right}> - <Form onsubmit={this.handleSave} /> - </div> - </div> - ); - } -} - -function renderSecret(secret) { - return ( - <Item - name={secret.name} - event={secret.event} - ondelete={this.handleDelete.bind(this, secret)} - /> - ); -} - -const LOADING = <div className={styles.loading}>Loading</div>; - -const EMPTY = ( - <div className={styles.empty}>There are no secrets for this repository.</div> -); diff --git a/web/src/screens/repo/screens/settings/index.jsx b/web/src/screens/repo/screens/settings/index.jsx deleted file mode 100644 index a7acc8a16e..0000000000 --- a/web/src/screens/repo/screens/settings/index.jsx +++ /dev/null @@ -1,244 +0,0 @@ -import React, { Component } from "react"; - -import { branch } from "baobab-react/higher-order"; -import { inject } from "config/client/inject"; - -import { - fetchRepository, - updateRepository, - repositorySlug, -} from "shared/utils/repository"; - -import { - VISIBILITY_PUBLIC, - VISIBILITY_PRIVATE, - VISIBILITY_INTERNAL, -} from "shared/constants/visibility"; - -import styles from "./index.less"; - -const binding = (props, context) => { - const { owner, repo } = props.match.params; - const slug = repositorySlug(owner, repo); - return { - user: ["user", "data"], - repo: ["repos", "data", slug], - }; -}; - -@inject -@branch(binding) -export default class Settings extends Component { - constructor(props, context) { - super(props, context); - - this.handlePushChange = this.handlePushChange.bind(this); - this.handlePullChange = this.handlePullChange.bind(this); - this.handleTagChange = this.handleTagChange.bind(this); - this.handleDeployChange = this.handleDeployChange.bind(this); - this.handleTrustedChange = this.handleTrustedChange.bind(this); - this.handleProtectedChange = this.handleProtectedChange.bind(this); - this.handleVisibilityChange = this.handleVisibilityChange.bind(this); - this.handleTimeoutChange = this.handleTimeoutChange.bind(this); - this.handlePathChange = this.handlePathChange.bind(this); - this.handleFallbackChange = this.handleFallbackChange.bind(this); - this.handleChange = this.handleChange.bind(this); - } - - shouldComponentUpdate(nextProps, nextState) { - return this.props.repo !== nextProps.repo; - } - - componentWillMount() { - const { drone, dispatch, match, repo } = this.props; - - if (!repo) { - dispatch(fetchRepository, drone, match.params.owner, match.params.repo); - } - } - - render() { - const { repo } = this.props; - - if (!repo) { - return undefined; - } - - return ( - <div className={styles.root}> - <section> - <h2>Pipeline Path</h2> - <div> - <input - type="text" - value={repo.config_file} - onBlur={this.handlePathChange} - /> - <label> - <input - type="checkbox" - checked={repo.fallback} - onChange={this.handleFallbackChange} - /> - <span>Fallback to .drone.yml if path not exists</span> - </label> - </div> - </section> - <section> - <h2>Repository Hooks</h2> - <div> - <label> - <input - type="checkbox" - checked={repo.allow_push} - onChange={this.handlePushChange} - /> - <span>push</span> - </label> - <label> - <input - type="checkbox" - checked={repo.allow_pr} - onChange={this.handlePullChange} - /> - <span>pull request</span> - </label> - <label> - <input - type="checkbox" - checked={repo.allow_tags} - onChange={this.handleTagChange} - /> - <span>tag</span> - </label> - <label> - <input - type="checkbox" - checked={repo.allow_deploys} - onChange={this.handleDeployChange} - /> - <span>deployment</span> - </label> - </div> - </section> - - <section> - <h2>Project Settings</h2> - <div> - <label> - <input - type="checkbox" - checked={repo.gated} - onChange={this.handleProtectedChange} - /> - <span>Protected</span> - </label> - <label> - <input - type="checkbox" - checked={repo.trusted} - onChange={this.handleTrustedChange} - /> - <span>Trusted</span> - </label> - </div> - </section> - - <section> - <h2>Project Visibility</h2> - <div> - <label> - <input - type="radio" - name="visibility" - value="public" - checked={repo.visibility === VISIBILITY_PUBLIC} - onChange={this.handleVisibilityChange} - /> - <span>Public</span> - </label> - <label> - <input - type="radio" - name="visibility" - value="private" - checked={repo.visibility === VISIBILITY_PRIVATE} - onChange={this.handleVisibilityChange} - /> - <span>Private</span> - </label> - <label> - <input - type="radio" - name="visibility" - value="internal" - checked={repo.visibility === VISIBILITY_INTERNAL} - onChange={this.handleVisibilityChange} - /> - <span>Internal</span> - </label> - </div> - </section> - - <section> - <h2>Timeout</h2> - <div> - <input - type="number" - value={repo.timeout} - onBlur={this.handleTimeoutChange} - /> - <span className={styles.minutes}>minutes</span> - </div> - </section> - </div> - ); - } - - handlePushChange(e) { - this.handleChange("allow_push", e.target.checked); - } - - handlePullChange(e) { - this.handleChange("allow_pr", e.target.checked); - } - - handleTagChange(e) { - this.handleChange("allow_tag", e.target.checked); - } - - handleDeployChange(e) { - this.handleChange("allow_deploy", e.target.checked); - } - - handleTrustedChange(e) { - this.handleChange("trusted", e.target.checked); - } - - handleProtectedChange(e) { - this.handleChange("gated", e.target.checked); - } - - handleVisibilityChange(e) { - this.handleChange("visibility", e.target.value); - } - - handleTimeoutChange(e) { - this.handleChange("timeout", parseInt(e.target.value)); - } - - handlePathChange(e) { - this.handleChange("config_file", e.target.value); - } - - handleFallbackChange(e) { - this.handleChange("fallback", e.target.checked); - } - - handleChange(prop, value) { - const { dispatch, drone, repo } = this.props; - let data = {}; - data[prop] = value; - dispatch(updateRepository, drone, repo.owner, repo.name, data); - } -} diff --git a/web/src/screens/titles.jsx b/web/src/screens/titles.jsx deleted file mode 100644 index 3f0db7c606..0000000000 --- a/web/src/screens/titles.jsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from "react"; -import { Route, Switch } from "react-router-dom"; -import Title from "react-title-component"; - -// @see https://github.com/yannickcr/eslint-plugin-react/issues/512 -// eslint-disable-next-line react/display-name -export default function() { - return ( - <Switch> - <Route path="/account/tokens" exact={true} component={accountTitle} /> - <Route path="/account/repos" exact={true} component={accountRepos} /> - <Route path="/login" exact={false} component={loginTitle} /> - <Route path="/:owner/:repo" exact={false} component={repoTitle} /> - <Route path="/" exact={false} component={defautTitle} /> - </Switch> - ); -} - -const accountTitle = () => <Title render="Tokens | woodpecker" />; - -const accountRepos = () => <Title render="Repositories | woodpecker" />; - -const loginTitle = () => <Title render="Login | woodpecker" />; - -const repoTitle = ({ match }) => ( - <Title render={`${match.params.owner}/${match.params.repo} | woodpecker`} /> -); - -const defautTitle = () => <Title render="Welcome | woodpecker" />; diff --git a/web/src/screens/user/screens/repos/components/index.jsx b/web/src/screens/user/screens/repos/components/index.jsx deleted file mode 100644 index 4ad6b4fdab..0000000000 --- a/web/src/screens/user/screens/repos/components/index.jsx +++ /dev/null @@ -1,3 +0,0 @@ -import { List, Item } from "./list"; - -export { List, Item }; diff --git a/web/src/screens/user/screens/repos/components/list.jsx b/web/src/screens/user/screens/repos/components/list.jsx deleted file mode 100644 index 36f2a2485b..0000000000 --- a/web/src/screens/user/screens/repos/components/list.jsx +++ /dev/null @@ -1,40 +0,0 @@ -import React, { Component } from "react"; -import { Link } from "react-router-dom"; - -import { LaunchIcon } from "shared/components/icons"; -import { Switch } from "./switch"; - -import styles from "./list.less"; - -export const List = ({ children }) => ( - <div className={styles.list}>{children}</div> -); - -export class Item extends Component { - render() { - const { owner, name, active, link, onchange } = this.props; - return ( - <div className={styles.item}> - <div> - {owner}/{name} - </div> - <div className={active ? styles.active : styles.inactive}> - <Link to={link}> - <LaunchIcon /> - </Link> - </div> - <div> - <Switch onchange={onchange} checked={active} /> - </div> - </div> - ); - } - - shouldComponentUpdate(nextProps) { - return ( - this.props.owner !== nextProps.owner || - this.props.name !== nextProps.name || - this.props.active !== nextProps.active - ); - } -} diff --git a/web/src/screens/user/screens/repos/components/switch.jsx b/web/src/screens/user/screens/repos/components/switch.jsx deleted file mode 100644 index b656295485..0000000000 --- a/web/src/screens/user/screens/repos/components/switch.jsx +++ /dev/null @@ -1,13 +0,0 @@ -import React, { Component } from "react"; -import styles from "./switch.less"; - -export class Switch extends Component { - render() { - const { checked, onchange } = this.props; - return ( - <label className={styles.switch}> - <input type="checkbox" checked={checked} onChange={onchange} /> - </label> - ); - } -} diff --git a/web/src/screens/user/screens/tokens/index.jsx b/web/src/screens/user/screens/tokens/index.jsx deleted file mode 100644 index c6b3e12b35..0000000000 --- a/web/src/screens/user/screens/tokens/index.jsx +++ /dev/null @@ -1,59 +0,0 @@ -import React, { Component } from "react"; - -import { generateToken } from "shared/utils/users"; -import { branch } from "baobab-react/higher-order"; -import { inject } from "config/client/inject"; -import styles from "./index.less"; - -const binding = (props, context) => { - return { - location: ["location"], - token: ["token"], - }; -}; - -@inject -@branch(binding) -export default class Tokens extends Component { - shouldComponentUpdate(nextProps, nextState) { - return ( - this.props.location !== nextProps.location || - this.props.token !== nextProps.token - ); - } - - componentWillMount() { - const { drone, dispatch } = this.props; - - dispatch(generateToken, drone); - } - - render() { - const { location, token } = this.props; - - if (!location || !token) { - return <div>Loading</div>; - } - return ( - <div className={styles.root}> - <h2>Your Personal Token:</h2> - <pre>{token}</pre> - <h2>Example API Usage:</h2> - <pre>{usageWithCURL(location, token)}</pre> - <h2>Example CLI Usage:</h2> - <pre>{usageWithCLI(location, token)}</pre> - </div> - ); - } -} - -const usageWithCURL = (location, token) => { - return `curl -i ${location.protocol}//${location.host}/api/user -H "Authorization: Bearer ${token}"`; -}; - -const usageWithCLI = (location, token) => { - return `export DRONE_SERVER=${location.protocol}//${location.host} - export DRONE_TOKEN=${token} - - drone info`; -}; diff --git a/web/src/shared/components/avatar.jsx b/web/src/shared/components/avatar.jsx deleted file mode 100644 index 4d38e24618..0000000000 --- a/web/src/shared/components/avatar.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import React, { Component } from "react"; -import styles from "./avatar.less"; - -export default class Avatar extends Component { - render() { - const image = this.props.image; - const style = { - backgroundImage: `url(${image})`, - }; - return <div className={styles.avatar} style={style} />; - } -} diff --git a/web/src/shared/components/breadcrumb.jsx b/web/src/shared/components/breadcrumb.jsx deleted file mode 100644 index 736b18d302..0000000000 --- a/web/src/shared/components/breadcrumb.jsx +++ /dev/null @@ -1,21 +0,0 @@ -import React, { Component } from "react"; -import { ExpandIcon, BackIcon } from "shared/components/icons/index"; -import style from "./breadcrumb.less"; - -// breadcrumb separater icon. -export const SEPARATOR = <ExpandIcon size={18} className={style.separator} />; - -// breadcrumb back button. -export const BACK_BUTTON = <BackIcon size={18} className={style.back} />; - -// helper function to render a list item. -const renderItem = (element, index) => { - return <li key={index}>{element}</li>; -}; - -export default class Breadcrumb extends Component { - render() { - const { elements } = this.props; - return <ol className={style.breadcrumb}>{elements.map(renderItem)}</ol>; - } -} diff --git a/web/src/shared/components/build_event.jsx b/web/src/shared/components/build_event.jsx deleted file mode 100644 index 0d62e09388..0000000000 --- a/web/src/shared/components/build_event.jsx +++ /dev/null @@ -1,73 +0,0 @@ -import React, { Component } from "react"; -import { - BranchIcon, - CommitIcon, - DeployIcon, - LaunchIcon, - MergeIcon, - TagIcon, -} from "shared/components/icons/index"; -import { - EVENT_TAG, - EVENT_PULL_REQUEST, - EVENT_DEPLOY, -} from "shared/constants/events"; - -import styles from "./build_event.less"; - -export default class BuildEvent extends Component { - render() { - const { event, branch, commit, refs, refspec, link, target } = this.props; - - return ( - <div className={styles.host}> - <div className={styles.row}> - <div> - <CommitIcon /> - </div> - <div>{commit && commit.substr(0, 10)}</div> - </div> - <div className={styles.row}> - <div> - {event === EVENT_TAG ? ( - <TagIcon /> - ) : event === EVENT_PULL_REQUEST ? ( - <MergeIcon /> - ) : event === EVENT_DEPLOY ? ( - <DeployIcon /> - ) : ( - <BranchIcon /> - )} - </div> - <div> - {event === EVENT_TAG && refs ? ( - trimTagRef(refs) - ) : event === EVENT_PULL_REQUEST && refspec ? ( - trimMergeRef(refs) - ) : event === EVENT_DEPLOY && target ? ( - target - ) : ( - branch - )} - </div> - </div> - <a href={link} target="_blank"> - <LaunchIcon /> - </a> - </div> - ); - } -} - -const trimMergeRef = ref => { - return ref.match(/\d/g) || ref; -}; - -const trimTagRef = ref => { - return ref.startsWith("refs/tags/") ? ref.substr(10) : ref; -}; - -// push -// pull request (ref) -// tag (ref) -// deploy diff --git a/web/src/shared/components/build_time.jsx b/web/src/shared/components/build_time.jsx deleted file mode 100644 index 22d3cf0b0e..0000000000 --- a/web/src/shared/components/build_time.jsx +++ /dev/null @@ -1,37 +0,0 @@ -import React, { Component } from "react"; -import { ScheduleIcon, TimelapseIcon } from "shared/components/icons/index"; - -import TimeAgo from "react-timeago"; -import Duration from "./duration"; - -import styles from "./build_time.less"; - -export default class Runtime extends Component { - render() { - const { start, finish } = this.props; - return ( - <div className={styles.host}> - <div className={styles.row}> - <div> - <ScheduleIcon /> - </div> - <div>{start ? <TimeAgo date={start * 1000} /> : <span>--</span>}</div> - </div> - <div className={styles.row}> - <div> - <TimelapseIcon /> - </div> - <div> - {finish ? ( - <Duration start={start} finished={finish} /> - ) : start ? ( - <TimeAgo date={start * 1000} /> - ) : ( - <span>--</span> - )} - </div> - </div> - </div> - ); - } -} diff --git a/web/src/shared/components/drawer/drawer.jsx b/web/src/shared/components/drawer/drawer.jsx deleted file mode 100644 index e3e814a02b..0000000000 --- a/web/src/shared/components/drawer/drawer.jsx +++ /dev/null @@ -1,62 +0,0 @@ -import React, { Component } from "react"; -import CloseIcon from "shared/components/icons/close"; -import styles from "./drawer.less"; -import { CSSTransitionGroup } from "react-transition-group"; - -export const DOCK_LEFT = styles.left; -export const DOCK_RIGHT = styles.right; - -export class Drawer extends Component { - render() { - const { open, position } = this.props; - - let classes = [styles.drawer]; - if (open) { - classes.push(styles.open); - } - if (position) { - classes.push(position); - } - - var child = open ? ( - <div key={0} onClick={this.props.onClick} className={styles.backdrop} /> - ) : null; - - return ( - <div className={classes.join(" ")}> - <CSSTransitionGroup - transitionName="fade" - transitionEnterTimeout={150} - transitionLeaveTimeout={150} - transitionAppearTimeout={150} - transitionAppear={true} - transitionEnter={true} - transitionLeave={true} - > - {child} - </CSSTransitionGroup> - <div className={styles.inner}>{this.props.children}</div> - </div> - ); - } -} - -export class CloseButton extends Component { - render() { - return ( - <button className={styles.close} onClick={this.props.onClick}> - <CloseIcon /> - </button> - ); - } -} - -export class MenuButton extends Component { - render() { - return ( - <button className={styles.close} onClick={this.props.onClick}> - Show Menu - </button> - ); - } -} diff --git a/web/src/shared/components/duration.jsx b/web/src/shared/components/duration.jsx deleted file mode 100644 index 6ea6236d66..0000000000 --- a/web/src/shared/components/duration.jsx +++ /dev/null @@ -1,10 +0,0 @@ -import humanizeDuration from "humanize-duration"; -import React from "react"; - -export default class Duration extends React.Component { - render() { - const { start, finished } = this.props; - - return <time>{humanizeDuration((finished - start) * 1000)}</time>; - } -} diff --git a/web/src/shared/components/icons/back.jsx b/web/src/shared/components/icons/back.jsx deleted file mode 100644 index 09f1bedcb8..0000000000 --- a/web/src/shared/components/icons/back.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import React, { Component } from "react"; - -export default class BackIcon extends Component { - render() { - return ( - <svg - className={this.props.className} - width={this.props.size || 24} - height={this.props.size || 24} - viewBox="0 0 24 24" - > - <path d="M0 0h24v24H0z" fill="none" /> - <path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z" /> - </svg> - ); - } -} diff --git a/web/src/shared/components/icons/branch.jsx b/web/src/shared/components/icons/branch.jsx deleted file mode 100644 index 922ef6a21e..0000000000 --- a/web/src/shared/components/icons/branch.jsx +++ /dev/null @@ -1,11 +0,0 @@ -import React, { Component } from "react"; - -export default class BranchIcon extends Component { - render() { - return ( - <svg viewBox="0 0 24 24"> - <path d="M6,2A3,3 0 0,1 9,5C9,6.28 8.19,7.38 7.06,7.81C7.15,8.27 7.39,8.83 8,9.63C9,10.92 11,12.83 12,14.17C13,12.83 15,10.92 16,9.63C16.61,8.83 16.85,8.27 16.94,7.81C15.81,7.38 15,6.28 15,5A3,3 0 0,1 18,2A3,3 0 0,1 21,5C21,6.32 20.14,7.45 18.95,7.85C18.87,8.37 18.64,9 18,9.83C17,11.17 15,13.08 14,14.38C13.39,15.17 13.15,15.73 13.06,16.19C14.19,16.62 15,17.72 15,19A3,3 0 0,1 12,22A3,3 0 0,1 9,19C9,17.72 9.81,16.62 10.94,16.19C10.85,15.73 10.61,15.17 10,14.38C9,13.08 7,11.17 6,9.83C5.36,9 5.13,8.37 5.05,7.85C3.86,7.45 3,6.32 3,5A3,3 0 0,1 6,2M6,4A1,1 0 0,0 5,5A1,1 0 0,0 6,6A1,1 0 0,0 7,5A1,1 0 0,0 6,4M18,4A1,1 0 0,0 17,5A1,1 0 0,0 18,6A1,1 0 0,0 19,5A1,1 0 0,0 18,4M12,18A1,1 0 0,0 11,19A1,1 0 0,0 12,20A1,1 0 0,0 13,19A1,1 0 0,0 12,18Z" /> - </svg> - ); - } -} diff --git a/web/src/shared/components/icons/check.jsx b/web/src/shared/components/icons/check.jsx deleted file mode 100644 index 9c6f40a9d1..0000000000 --- a/web/src/shared/components/icons/check.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import React, { Component } from "react"; - -export default class CheckIcon extends Component { - render() { - return ( - <svg - className={this.props.className} - width={this.props.size || 24} - height={this.props.size || 24} - viewBox="0 0 24 24" - > - <path d="M0 0h24v24H0z" fill="none" /> - <path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z" /> - </svg> - ); - } -} diff --git a/web/src/shared/components/icons/clock.jsx b/web/src/shared/components/icons/clock.jsx deleted file mode 100644 index e898d4f9ac..0000000000 --- a/web/src/shared/components/icons/clock.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import React, { Component } from "react"; - -export default class ClockIcon extends Component { - render() { - return ( - <svg - className={this.props.className} - width={this.props.size || 24} - height={this.props.size || 24} - viewBox="0 0 24 24" - > - <path d="M0 0h24v24H0z" fill="none" /> - <path d="M22 5.72l-4.6-3.86-1.29 1.53 4.6 3.86L22 5.72zM7.88 3.39L6.6 1.86 2 5.71l1.29 1.53 4.59-3.85zM12.5 8H11v6l4.75 2.85.75-1.23-4-2.37V8zM12 4c-4.97 0-9 4.03-9 9s4.02 9 9 9c4.97 0 9-4.03 9-9s-4.03-9-9-9zm0 16c-3.87 0-7-3.13-7-7s3.13-7 7-7 7 3.13 7 7-3.13 7-7 7z" /> - </svg> - ); - } -} diff --git a/web/src/shared/components/icons/close.jsx b/web/src/shared/components/icons/close.jsx deleted file mode 100644 index 4ca13e78dc..0000000000 --- a/web/src/shared/components/icons/close.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import React, { Component } from "react"; - -export default class CloseIcon extends Component { - render() { - return ( - <svg - className={this.props.className} - width={this.props.size || 24} - height={this.props.size || 24} - viewBox="0 0 24 24" - > - <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" /> - <path d="M0 0h24v24H0z" fill="none" /> - </svg> - ); - } -} diff --git a/web/src/shared/components/icons/commit.jsx b/web/src/shared/components/icons/commit.jsx deleted file mode 100644 index 36a07b68af..0000000000 --- a/web/src/shared/components/icons/commit.jsx +++ /dev/null @@ -1,16 +0,0 @@ -import React, { Component } from "react"; - -export default class CommitIcon extends Component { - render() { - return ( - <svg - className={this.props.className} - width={this.props.size || 24} - height={this.props.size || 24} - viewBox="0 0 24 24" - > - <path d="M17,12C17,14.42 15.28,16.44 13,16.9V21H11V16.9C8.72,16.44 7,14.42 7,12C7,9.58 8.72,7.56 11,7.1V3H13V7.1C15.28,7.56 17,9.58 17,12M12,9A3,3 0 0,0 9,12A3,3 0 0,0 12,15A3,3 0 0,0 15,12A3,3 0 0,0 12,9Z" /> - </svg> - ); - } -} diff --git a/web/src/shared/components/icons/deploy.jsx b/web/src/shared/components/icons/deploy.jsx deleted file mode 100644 index dbabeb0fc2..0000000000 --- a/web/src/shared/components/icons/deploy.jsx +++ /dev/null @@ -1,11 +0,0 @@ -import React, { Component } from "react"; - -export default class DeployIcon extends Component { - render() { - return ( - <svg className={this.props.className} viewBox="0 0 24 24"> - <path d="M19,18H6A4,4 0 0,1 2,14A4,4 0 0,1 6,10H6.71C7.37,7.69 9.5,6 12,6A5.5,5.5 0 0,1 17.5,11.5V12H19A3,3 0 0,1 22,15A3,3 0 0,1 19,18M19.35,10.03C18.67,6.59 15.64,4 12,4C9.11,4 6.6,5.64 5.35,8.03C2.34,8.36 0,10.9 0,14A6,6 0 0,0 6,20H19A5,5 0 0,0 24,15C24,12.36 21.95,10.22 19.35,10.03Z" /> - </svg> - ); - } -} diff --git a/web/src/shared/components/icons/expand.jsx b/web/src/shared/components/icons/expand.jsx deleted file mode 100644 index d74ba58192..0000000000 --- a/web/src/shared/components/icons/expand.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import React, { Component } from "react"; - -export default class ExpandIcon extends Component { - render() { - return ( - <svg - className={this.props.className} - width={this.props.size || 24} - height={this.props.size || 24} - viewBox="0 0 24 24" - > - <path d="M7.41 7.84L12 12.42l4.59-4.58L18 9.25l-6 6-6-6z" /> - <path d="M0-.75h24v24H0z" fill="none" /> - </svg> - ); - } -} diff --git a/web/src/shared/components/icons/index.jsx b/web/src/shared/components/icons/index.jsx deleted file mode 100644 index 52574d93ad..0000000000 --- a/web/src/shared/components/icons/index.jsx +++ /dev/null @@ -1,45 +0,0 @@ -import BackIcon from "./back"; -import BranchIcon from "./branch"; -import CheckIcon from "./check"; -import ClockIcon from "./clock"; -import CloseIcon from "./close"; -import CommitIcon from "./commit"; -import DeployIcon from "./deploy"; -import ExpandIcon from "./expand"; -import LaunchIcon from "./launch"; -import LinkIcon from "./link"; -import MenuIcon from "./menu"; -import MergeIcon from "./merge"; -import PauseIcon from "./pause"; -import PlayIcon from "./play"; -import RefreshIcon from "./refresh"; -import RemoveIcon from "./remove"; -import ScheduleIcon from "./schedule"; -import StarIcon from "./star"; -import SyncIcon from "./sync"; -import TagIcon from "./tag"; -import TimelapseIcon from "./timelapse"; - -export { - BackIcon, - BranchIcon, - CheckIcon, - CloseIcon, - ClockIcon, - CommitIcon, - DeployIcon, - ExpandIcon, - LaunchIcon, - LinkIcon, - MenuIcon, - MergeIcon, - PauseIcon, - PlayIcon, - RefreshIcon, - RemoveIcon, - ScheduleIcon, - StarIcon, - SyncIcon, - TagIcon, - TimelapseIcon, -}; diff --git a/web/src/shared/components/icons/launch.jsx b/web/src/shared/components/icons/launch.jsx deleted file mode 100644 index f53e44ebeb..0000000000 --- a/web/src/shared/components/icons/launch.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import React, { Component } from "react"; - -export default class LaunchIcon extends Component { - render() { - return ( - <svg className={this.props.className} viewBox="0 0 24 24"> - <path d="M0 0h24v24H0z" fill="none" /> - <path d="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z" /> - </svg> - ); - } -} diff --git a/web/src/shared/components/icons/link.jsx b/web/src/shared/components/icons/link.jsx deleted file mode 100644 index e38f93f603..0000000000 --- a/web/src/shared/components/icons/link.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import React, { Component } from "react"; - -export default class LinkIcon extends Component { - render() { - return ( - <svg - className={this.props.className} - width={this.props.size || 24} - height={this.props.size || 24} - viewBox="0 0 24 24" - > - <path d="M0 0h24v24H0z" fill="none" /> - <path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z" /> - </svg> - ); - } -} diff --git a/web/src/shared/components/icons/menu.jsx b/web/src/shared/components/icons/menu.jsx deleted file mode 100644 index 60f1d1c8a1..0000000000 --- a/web/src/shared/components/icons/menu.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import React, { Component } from "react"; - -export default class MenuIcon extends Component { - render() { - return ( - <svg - className={this.props.className} - width={this.props.size || 24} - height={this.props.size || 24} - viewBox="0 0 24 24" - > - <path d="M0 0h24v24H0z" fill="none" /> - <path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z" /> - </svg> - ); - } -} diff --git a/web/src/shared/components/icons/merge.jsx b/web/src/shared/components/icons/merge.jsx deleted file mode 100644 index 92dfb8cbe2..0000000000 --- a/web/src/shared/components/icons/merge.jsx +++ /dev/null @@ -1,16 +0,0 @@ -import React, { Component } from "react"; - -export default class MergeIcon extends Component { - render() { - return ( - <svg className={this.props.className} viewBox="0 0 24 24"> - <path d="M5.41,21L6.12,17H2.12L2.47,15H6.47L7.53,9H3.53L3.88,7H7.88L8.59,3H10.59L9.88,7H15.88L16.59,3H18.59L17.88,7H21.88L21.53,9H17.53L16.47,15H20.47L20.12,17H16.12L15.41,21H13.41L14.12,17H8.12L7.41,21H5.41M9.53,9L8.47,15H14.47L15.53,9H9.53Z" /> - </svg> - ); - } -} - -// <svg class={this.props.className} viewBox="0 0 54.5 68"> -// <path d="M20,13C20,8.6,16.4,5,12.1,5C7.7,5,4.2,8.6,4.2,13c0,3.2,1.9,6,4.7,7.2v27.1c-2.7,1.2-4.7,4-4.7,7.2c0,4.4,3.6,7.9,7.9,7.9 c4.4,0,7.9-3.6,7.9-7.9c0-3.2-1.9-6-4.7-7.2V20.2C18.1,18.9,20,16.2,20,13z M16,54.5c0,2.2-1.8,3.9-3.9,3.9c-2.2,0-3.9-1.8-3.9-3.9 c0-2.2,1.8-3.9,3.9-3.9C14.2,50.5,16,52.3,16,54.5z M12.1,16.9c-2.2,0-3.9-1.8-3.9-3.9c0-2.2,1.8-3.9,3.9-3.9C14.2,9,16,10.8,16,13 C16,15.1,14.2,16.9,12.1,16.9z"/> -// <path d="M45.3,47.3V20.8c0-6.1-5-11.1-11.1-11.1h-2.7V3.6L20.7,13l10.8,9.3v-6.1h2.7c2.6,0,4.6,2.1,4.6,4.6v26.4 c-2.7,1.2-4.7,4-4.7,7.2c0,4.4,3.6,7.9,7.9,7.9c4.4,0,7.9-3.6,7.9-7.9C50,51.3,48.1,48.5,45.3,47.3z M42.1,58.4 c-2.2,0-3.9-1.8-3.9-3.9c0-2.2,1.8-3.9,3.9-3.9c2.2,0,3.9,1.8,3.9,3.9C46,56.6,44.2,58.4,42.1,58.4z"/> -// </svg> diff --git a/web/src/shared/components/icons/pause.jsx b/web/src/shared/components/icons/pause.jsx deleted file mode 100644 index 3be94b89eb..0000000000 --- a/web/src/shared/components/icons/pause.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import React, { Component } from "react"; - -export default class PauseIcon extends Component { - render() { - return ( - <svg - className={this.props.className} - width={this.props.size || 24} - height={this.props.size || 24} - viewBox="0 0 24 24" - > - <path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" /> - <path d="M0 0h24v24H0z" fill="none" /> - </svg> - ); - } -} diff --git a/web/src/shared/components/icons/play.jsx b/web/src/shared/components/icons/play.jsx deleted file mode 100644 index d13bab4d26..0000000000 --- a/web/src/shared/components/icons/play.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import React, { Component } from "react"; - -export default class PlayIcon extends Component { - render() { - return ( - <svg - className={this.props.className} - width={this.props.size || 24} - height={this.props.size || 24} - viewBox="0 0 24 24" - > - <path d="M8 5v14l11-7z" /> - <path d="M0 0h24v24H0z" fill="none" /> - </svg> - ); - } -} diff --git a/web/src/shared/components/icons/refresh.jsx b/web/src/shared/components/icons/refresh.jsx deleted file mode 100644 index 6faed71a4f..0000000000 --- a/web/src/shared/components/icons/refresh.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import React, { Component } from "react"; - -export default class RefreshIcon extends Component { - render() { - return ( - <svg - className={this.props.className} - width={this.props.size || 24} - height={this.props.size || 24} - viewBox="0 0 24 24" - > - <path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z" /> - <path d="M0 0h24v24H0z" fill="none" /> - </svg> - ); - } -} diff --git a/web/src/shared/components/icons/remove.jsx b/web/src/shared/components/icons/remove.jsx deleted file mode 100644 index 4136d9287e..0000000000 --- a/web/src/shared/components/icons/remove.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import React, { Component } from "react"; - -export default class CheckIcon extends Component { - render() { - return ( - <svg - className={this.props.className} - width={this.props.size || 24} - height={this.props.size || 24} - viewBox="0 0 24 24" - > - <path d="M19 13H5v-2h14v2z" /> - <path d="M0 0h24v24H0z" fill="none" /> - </svg> - ); - } -} diff --git a/web/src/shared/components/icons/report.jsx b/web/src/shared/components/icons/report.jsx deleted file mode 100644 index fc16ded1a0..0000000000 --- a/web/src/shared/components/icons/report.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import React, { Component } from "react"; - -export default class ReportIcon extends Component { - render() { - return ( - <svg className={this.props.className} viewBox="0 0 24 24"> - <path d="M15.73 3H8.27L3 8.27v7.46L8.27 21h7.46L21 15.73V8.27L15.73 3zM12 17.3c-.72 0-1.3-.58-1.3-1.3 0-.72.58-1.3 1.3-1.3.72 0 1.3.58 1.3 1.3 0 .72-.58 1.3-1.3 1.3zm1-4.3h-2V7h2v6z" /> - <path d="M0 0h24v24H0z" fill="none" /> - </svg> - ); - } -} diff --git a/web/src/shared/components/icons/schedule.jsx b/web/src/shared/components/icons/schedule.jsx deleted file mode 100644 index 1310a0fbea..0000000000 --- a/web/src/shared/components/icons/schedule.jsx +++ /dev/null @@ -1,18 +0,0 @@ -import React, { Component } from "react"; - -export default class ScheduleIcon extends Component { - render() { - return ( - <svg - className={this.props.className} - width={this.props.size || 24} - height={this.props.size || 24} - viewBox="0 0 24 24" - > - <path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z" /> - <path d="M0 0h24v24H0z" fill="none" /> - <path d="M12.5 7H11v6l5.25 3.15.75-1.23-4.5-2.67z" /> - </svg> - ); - } -} diff --git a/web/src/shared/components/icons/star.jsx b/web/src/shared/components/icons/star.jsx deleted file mode 100644 index 3019348ad1..0000000000 --- a/web/src/shared/components/icons/star.jsx +++ /dev/null @@ -1,20 +0,0 @@ -import React, { Component } from "react"; - -export default class StarIcon extends Component { - render() { - return ( - <svg - className={this.props.className} - width={this.props.size || 24} - height={this.props.size || 24} - viewBox="0 0 512 512" - > - {this.props.filled === true ? ( - <path d="M256 372.686L380.83 448l-33.021-142.066L458 210.409l-145.267-12.475L256 64l-56.743 133.934L54 210.409l110.192 95.525L131.161 448z" /> - ) : ( - <path d="M458 210.409l-145.267-12.476L256 64l-56.743 133.934L54 210.409l110.192 95.524L131.161 448 256 372.686 380.83 448l-33.021-142.066L458 210.409zM272.531 345.286L256 335.312l-16.53 9.973-59.988 36.191 15.879-68.296 4.369-18.79-14.577-12.637-52.994-45.939 69.836-5.998 19.206-1.65 7.521-17.75 27.276-64.381 27.27 64.379 7.52 17.751 19.208 1.65 69.846 5.998-52.993 45.939-14.576 12.636 4.367 18.788 15.875 68.299-59.984-36.189z" /> - )} - </svg> - ); - } -} diff --git a/web/src/shared/components/icons/sync.jsx b/web/src/shared/components/icons/sync.jsx deleted file mode 100644 index ba8768a532..0000000000 --- a/web/src/shared/components/icons/sync.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import React, { Component } from "react"; - -export default class SyncIcon extends Component { - render() { - return ( - <svg className={this.props.className} viewBox="0 0 24 24"> - <path d="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46C19.54 15.03 20 13.57 20 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74C4.46 8.97 4 10.43 4 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z" /> - <path d="M0 0h24v24H0z" fill="none" /> - </svg> - ); - } -} diff --git a/web/src/shared/components/icons/tag.jsx b/web/src/shared/components/icons/tag.jsx deleted file mode 100644 index 695d9b558c..0000000000 --- a/web/src/shared/components/icons/tag.jsx +++ /dev/null @@ -1,11 +0,0 @@ -import React, { Component } from "react"; - -export default class TagIcon extends Component { - render() { - return ( - <svg className={this.props.className} viewBox="0 0 24 24"> - <path d="M5.5,7A1.5,1.5 0 0,0 7,5.5A1.5,1.5 0 0,0 5.5,4A1.5,1.5 0 0,0 4,5.5A1.5,1.5 0 0,0 5.5,7M21.41,11.58C21.77,11.94 22,12.44 22,13C22,13.55 21.78,14.05 21.41,14.41L14.41,21.41C14.05,21.77 13.55,22 13,22C12.45,22 11.95,21.77 11.58,21.41L2.59,12.41C2.22,12.05 2,11.55 2,11V4C2,2.89 2.89,2 4,2H11C11.55,2 12.05,2.22 12.41,2.58L21.41,11.58M13,20L20,13L11.5,4.5L4.5,11.5L13,20Z" /> - </svg> - ); - } -} diff --git a/web/src/shared/components/icons/timelapse.jsx b/web/src/shared/components/icons/timelapse.jsx deleted file mode 100644 index c8afb955d1..0000000000 --- a/web/src/shared/components/icons/timelapse.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import React, { Component } from "react"; - -export default class TimelapseIcon extends Component { - render() { - return ( - <svg - className={this.props.className} - width={this.props.size || 24} - height={this.props.size || 24} - viewBox="0 0 24 24" - > - <path d="M0 0h24v24H0z" fill="none" /> - <path d="M16.24 7.76C15.07 6.59 13.54 6 12 6v6l-4.24 4.24c2.34 2.34 6.14 2.34 8.49 0 2.34-2.34 2.34-6.14-.01-8.48zM12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z" /> - </svg> - ); - } -} diff --git a/web/src/shared/components/logo.jsx b/web/src/shared/components/logo.jsx deleted file mode 100644 index 920ec5a8b8..0000000000 --- a/web/src/shared/components/logo.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import React, { Component } from "react"; - -export default class Logo extends Component { - render() { - return ( - <svg viewBox="0 0 50 62.5" preserveAspectRatio="xMidYMid"> - <g> - <path - fillRule="evenodd" - clipRule="evenodd" - d="M15.872,0.468c1.148,1.088,1.582,2.188,2.855,2.337l0.036,0.007c-0.588,0.606-1.089,1.402-1.443,2.423c-0.379,1.096-0.488,2.285-0.614,3.659c-0.189,2.046-0.401,4.364-1.556,7.269 c-2.486,6.258-1.119,11.631,0.332,17.317c0.664,2.604,1.348,5.297,1.642,8.107c0.035,0.355,0.287,0.652,0.633,0.744 c0.346,0.095,0.709-0.035,0.922-0.323c0.227-0.313,0.524-0.797,0.86-1.424c0.84,3.323,1.355,6.131,1.783,8.697 c0.126,0.73,1.048,0.973,1.517,0.41c2.881-3.463,3.763-8.636,2.184-12.674c0.459-2.433,1.402-4.45,2.398-6.583 c0.536-1.15,1.08-2.318,1.55-3.566c0.228-0.084,0.569-0.314,0.791-0.441l1.706-0.981l-0.256,1.052 c-0.112,0.461,0.171,0.929,0.635,1.04c0.457,0.118,0.93-0.173,1.043-0.632l0.68-2.858l1.285-2.95 c0.19-0.436-0.009-0.943-0.446-1.135c-0.44-0.189-0.947,0.01-1.135,0.448l-1.152,2.669l-2.383,1.372 c0.235-0.932,0.414-1.919,0.508-2.981c0.432-4.859-0.718-9.074-3.066-11.266c-0.163-0.157-0.208-0.281-0.247-0.26 c0.095-0.119,0.249-0.26,0.358-0.374c2.283-1.693,6.047-0.147,8.319,0.751c0.589,0.231,0.876-0.338,0.316-0.67 c-1.949-1.154-5.948-4.197-8.188-6.194c-0.313-0.275-0.527-0.607-0.89-0.913c-2.415-4.266-8.168-1.764-10.885-2.252 C15.862,0.275,15.798,0.396,15.872,0.468 M26.852,6.367c-0.059,1.242-0.603,1.8-0.999,2.208c-0.218,0.224-0.427,0.436-0.525,0.738 c-0.236,0.714,0.008,1.51,0.66,2.143c1.974,1.84,2.925,5.527,2.538,9.861c-0.291,3.287-1.448,5.762-2.671,8.384 c-1.031,2.207-2.096,4.489-2.577,7.259c-0.027,0.161-0.01,0.33,0.056,0.481c1.021,2.433,1.135,6.196-0.672,9.46 c-0.461-2.553-1.053-5.385-1.97-8.712c1.964-4.488,4.203-11.75,2.919-17.668c-0.325-1.497-1.304-3.276-2.387-4.207 c-0.208-0.179-0.402-0.237-0.495-0.167c-0.084,0.061-0.151,0.238-0.062,0.444c0.55,1.266,0.879,2.599,1.226,4.276 c1.125,5.443-0.956,12.49-2.835,16.782l-0.116,0.259l-0.457,0.982c-0.356-2.014-0.849-3.95-1.33-5.839 c-1.379-5.407-2.679-10.516-0.401-16.255c1.247-3.137,1.483-5.692,1.672-7.746c0.116-1.263,0.216-2.355,0.526-3.252 c0.905-2.605,3.062-3.178,4.744-2.852C25.328,3.262,26.936,4.539,26.852,6.367z M23.984,6.988c0.617,0.204,1.283-0.131,1.487-0.75 c0.202-0.617-0.134-1.283-0.751-1.487c-0.618-0.204-1.285,0.134-1.487,0.751C23.029,6.12,23.366,6.786,23.984,6.988z" - /> - </g> - </svg> - ); - } -} diff --git a/web/src/shared/components/menu.jsx b/web/src/shared/components/menu.jsx deleted file mode 100644 index d9d5e133fd..0000000000 --- a/web/src/shared/components/menu.jsx +++ /dev/null @@ -1,32 +0,0 @@ -import React, { Component } from "react"; -import { NavLink as Link } from "react-router-dom"; -import PropTypes from "prop-types"; - -import styles from "./menu.less"; - -export default class Menu extends Component { - propTypes = { items: PropTypes.array, right: PropTypes.any }; - render() { - const items = this.props.items; - const right = this.props.right ? ( - <div className={styles.right}>{this.props.right}</div> - ) : null; - return ( - <section className={styles.root}> - <div className={styles.left}> - {items.map(i => ( - <Link - key={i.to + i.label} - to={i.to} - exact={true} - activeClassName={styles["link-active"]} - > - {i.label} - </Link> - ))} - </div> - {right} - </section> - ); - } -} diff --git a/web/src/shared/components/snackbar.jsx b/web/src/shared/components/snackbar.jsx deleted file mode 100644 index 0484ef6d90..0000000000 --- a/web/src/shared/components/snackbar.jsx +++ /dev/null @@ -1,47 +0,0 @@ -import React from "react"; -import styles from "./snackbar.less"; -import CloseIcon from "shared/components/icons/close"; -import { CSSTransitionGroup } from "react-transition-group"; - -export class Snackbar extends React.Component { - render() { - const { message } = this.props; - - let classes = [styles.snackbar]; - if (message) { - classes.push(styles.open); - } - - const content = message ? ( - <div className={classes.join(" ")} key={message}> - <div>{message}</div> - <button onClick={this.props.onClose}> - <CloseIcon /> - </button> - </div> - ) : null; - - return ( - <CSSTransitionGroup - transitionName="slideup" - transitionEnterTimeout={200} - transitionLeaveTimeout={200} - transitionAppearTimeout={200} - transitionAppear={true} - transitionEnter={true} - transitionLeave={true} - className={classes.root} - > - {content} - </CSSTransitionGroup> - ); - } -} - -// const SnackbarContent = ({ children, ...props }) => { -// <div {...props}>{children}</div> -// } -// -// const SnackbarClose = ({ children, ...props }) => { -// <div {...props}>{children}</div> -// } diff --git a/web/src/shared/components/status.jsx b/web/src/shared/components/status.jsx deleted file mode 100644 index bf0e310b60..0000000000 --- a/web/src/shared/components/status.jsx +++ /dev/null @@ -1,100 +0,0 @@ -import React, { Component } from "react"; -import classnames from "classnames"; -import { - STATUS_BLOCKED, - STATUS_DECLINED, - STATUS_ERROR, - STATUS_FAILURE, - STATUS_KILLED, - STATUS_PENDING, - STATUS_RUNNING, - STATUS_SKIPPED, - STATUS_STARTED, - STATUS_SUCCESS, -} from "shared/constants/status"; -import style from "./status.less"; - -import { - CheckIcon, - CloseIcon, - ClockIcon, - RefreshIcon, - RemoveIcon, -} from "./icons/index"; - -const defaultIconSize = 15; - -const statusLabel = status => { - switch (status) { - case STATUS_BLOCKED: - return "Pending Approval"; - case STATUS_DECLINED: - return "Declined"; - case STATUS_ERROR: - return "Error"; - case STATUS_FAILURE: - return "Failure"; - case STATUS_KILLED: - return "Cancelled"; - case STATUS_PENDING: - return "Pending"; - case STATUS_RUNNING: - return "Running"; - case STATUS_SKIPPED: - return "Skipped"; - case STATUS_STARTED: - return "Running"; - case STATUS_SUCCESS: - return "Successful"; - default: - return ""; - } -}; - -const renderIcon = (status, size) => { - switch (status) { - case STATUS_SKIPPED: - return <RemoveIcon size={size} />; - case STATUS_PENDING: - return <ClockIcon size={size} />; - case STATUS_RUNNING: - case STATUS_STARTED: - return <RefreshIcon size={size} />; - case STATUS_SUCCESS: - return <CheckIcon size={size} />; - default: - return <CloseIcon size={size} />; - } -}; - -export default class Status extends Component { - shouldComponentUpdate(nextProps, nextState) { - return this.props.status !== nextProps.status; - } - - render() { - const { status } = this.props; - const icon = renderIcon(status, defaultIconSize); - const classes = classnames(style.root, style[status]); - return <div className={classes}>{icon}</div>; - } -} - -export const StatusLabel = ({ status }) => { - return ( - <div className={classnames(style.label, style[status])}> - <div>{statusLabel(status)}</div> - </div> - ); -}; - -export const StatusText = ({ status, text }) => { - return ( - <div - className={classnames(style.label, style[status])} - style="padding: 5px 10px;" - > - <div>{text}</div> - </div> - ); -}; diff --git a/web/src/shared/components/status_number.jsx b/web/src/shared/components/status_number.jsx deleted file mode 100644 index 203254fdf6..0000000000 --- a/web/src/shared/components/status_number.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import React, { Component } from "react"; -import classnames from "classnames"; - -import styles from "./status_number.less"; - -export default class StatusNumber extends Component { - render() { - const { status, number } = this.props; - const className = classnames(styles.root, styles[status]); - return <div className={className}>{number}</div>; - } -} diff --git a/web/src/shared/components/sync.jsx b/web/src/shared/components/sync.jsx deleted file mode 100644 index 5a9b7819af..0000000000 --- a/web/src/shared/components/sync.jsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from "react"; -import Icon from "./icons/refresh"; -import styles from "./sync.less"; - -export const Message = () => { - return ( - <div className={styles.root}> - <div className={styles.alert}> - <div> - <Icon /> - </div> - <div>Account synchronization in progress</div> - </div> - </div> - ); -}; From 667173631aed31a6d02e3609eda829c301e7f90a Mon Sep 17 00:00:00 2001 From: Anton Bracke <anton@ju60.de> Date: Mon, 19 Jul 2021 15:15:01 +0200 Subject: [PATCH 005/143] some web-ui stuff --- web-new/index.html | 1 + web-new/src/App.vue | 9 +++ web-new/src/components/HelloWorld.vue | 70 ------------------- web-new/src/components/Navbar.vue | 31 +++++--- web-new/src/components/atomic/Button.vue | 70 +++++++++++++++++++ web-new/src/compositions/useApiClient.ts | 8 ++- web-new/src/compositions/useAuthentication.ts | 15 ++++ web-new/src/compositions/useConfig.ts | 19 +++++ web-new/src/lib/api/client.ts | 2 +- web-new/src/lib/api/index.ts | 15 ++-- web-new/src/router.ts | 22 ++++-- web-new/src/views/Home.vue | 7 ++ web-new/src/views/Login.vue | 27 +++++-- web-new/src/views/repos/Repo.vue | 46 ++++++++++++ web-new/src/views/repos/RepoAdd.vue | 46 ++++++++++++ web-new/src/views/repos/Repos.vue | 37 ++++++++++ web-new/vite.config.ts | 9 --- web-new/windi.config.ts | 6 +- 18 files changed, 331 insertions(+), 109 deletions(-) delete mode 100644 web-new/src/components/HelloWorld.vue create mode 100644 web-new/src/components/atomic/Button.vue create mode 100644 web-new/src/compositions/useAuthentication.ts create mode 100644 web-new/src/compositions/useConfig.ts create mode 100644 web-new/src/views/repos/Repo.vue create mode 100644 web-new/src/views/repos/RepoAdd.vue create mode 100644 web-new/src/views/repos/Repos.vue diff --git a/web-new/index.html b/web-new/index.html index 033e6646cc..84690446a4 100644 --- a/web-new/index.html +++ b/web-new/index.html @@ -5,6 +5,7 @@ <link rel="icon" href="/favicon.svg" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Woodpecker +
diff --git a/web-new/src/App.vue b/web-new/src/App.vue index 2fbdd9645a..b3c9b7c912 100644 --- a/web-new/src/App.vue +++ b/web-new/src/App.vue @@ -18,6 +18,15 @@ export default defineComponent({ }); + + diff --git a/web-new/src/components/Navbar.vue b/web-new/src/components/Navbar.vue index 1e715a6f26..ce4c3d50e5 100644 --- a/web-new/src/components/Navbar.vue +++ b/web-new/src/components/Navbar.vue @@ -1,29 +1,38 @@ diff --git a/web-new/src/components/atomic/Button.vue b/web-new/src/components/atomic/Button.vue new file mode 100644 index 0000000000..103135ad9c --- /dev/null +++ b/web-new/src/components/atomic/Button.vue @@ -0,0 +1,70 @@ + + + diff --git a/web-new/src/compositions/useApiClient.ts b/web-new/src/compositions/useApiClient.ts index 7a8d5b6dff..d53c1f8593 100644 --- a/web-new/src/compositions/useApiClient.ts +++ b/web-new/src/compositions/useApiClient.ts @@ -1,12 +1,18 @@ import WoodpeckerClient from '~/lib/api'; +import useConfig from './useConfig'; let apiClient: WoodpeckerClient | undefined; export default (): WoodpeckerClient => { if (!apiClient) { + const config = useConfig(); const server = 'http://localhost:8000'; const token = ''; - const csrf = ''; + const csrf = config.csrf; + + if (!csrf) { + throw new Error('CSRF unknown'); + } apiClient = new WoodpeckerClient(server, token, csrf); } diff --git a/web-new/src/compositions/useAuthentication.ts b/web-new/src/compositions/useAuthentication.ts new file mode 100644 index 0000000000..26ab5e1412 --- /dev/null +++ b/web-new/src/compositions/useAuthentication.ts @@ -0,0 +1,15 @@ +import useConfig from '~/compositions/useConfig'; +import { User } from '~/lib/api/types'; + +export function isAuthenticated(): boolean { + return !!useConfig().user; +} + +export function user(): User | null { + return useConfig().user; +} + +export function authenticate(origin?: string): void { + const url = `/login?url=${origin || ''}`; + window.location.href = url; +} diff --git a/web-new/src/compositions/useConfig.ts b/web-new/src/compositions/useConfig.ts new file mode 100644 index 0000000000..bfed43eae3 --- /dev/null +++ b/web-new/src/compositions/useConfig.ts @@ -0,0 +1,19 @@ +import { User } from '~/lib/api/types'; + +declare global { + interface Window { + WOODPECKER_USER: User | undefined; + WOODPECKER_SYNC: boolean | undefined; + WOODPECKER_DOCS: string | undefined; + WOODPECKER_VERSION: string | undefined; + WOODPECKER_CSRF: string | undefined; + } +} + +export default () => ({ + user: window.WOODPECKER_USER || null, + syncing: window.WOODPECKER_SYNC || null, + docs: window.WOODPECKER_DOCS || null, + version: window.WOODPECKER_VERSION, + csrf: window.WOODPECKER_CSRF || null, +}); diff --git a/web-new/src/lib/api/client.ts b/web-new/src/lib/api/client.ts index 7ad5f8e66f..3fa57bff72 100644 --- a/web-new/src/lib/api/client.ts +++ b/web-new/src/lib/api/client.ts @@ -3,7 +3,7 @@ export type ApiError = { message: string; }; -export function encodeQueryString(params: Record): string { +export function encodeQueryString(params: Record = {}): string { return params ? Object.keys(params) .sort() diff --git a/web-new/src/lib/api/index.ts b/web-new/src/lib/api/index.ts index 4c2e5c38c8..c04c0be53b 100644 --- a/web-new/src/lib/api/index.ts +++ b/web-new/src/lib/api/index.ts @@ -1,12 +1,17 @@ import ApiClient, { encodeQueryString } from './client'; import { Repo } from './types'; +type RepoListOptions = { + all?: boolean; + flush?: boolean; +}; + export default class WoodpeckerClient extends ApiClient { constructor(server: string, token: string, csrf: string) { super(server, token, csrf); } - getRepoList(opts: Record): Promise { + getRepoList(opts?: RepoListOptions): Promise { var query = encodeQueryString(opts); return this._get('/api/user/repos?' + query); } @@ -19,7 +24,7 @@ export default class WoodpeckerClient extends ApiClient { return this._post('/api/repos/' + owner + '/' + repo); } - updateRepo(owner: string, repo: string, data) { + updateRepo(owner: string, repo: string, data: Record) { return this._patch('/api/repos/' + owner + '/' + repo, data); } @@ -27,7 +32,7 @@ export default class WoodpeckerClient extends ApiClient { return this._delete('/api/repos/' + owner + '/' + repo); } - getBuildList(owner: string, repo: string, opts) { + getBuildList(owner: string, repo: string, opts: Record) { var query = encodeQueryString(opts); return this._get('/api/repos/' + owner + '/' + repo + '/builds?' + query); } @@ -36,7 +41,7 @@ export default class WoodpeckerClient extends ApiClient { return this._get('/api/repos/' + owner + '/' + repo + '/builds/' + number); } - getBuildFeed(opts) { + getBuildFeed(opts: Record) { var query = encodeQueryString(opts); return this._get('/api/user/feed?' + query); } @@ -53,7 +58,7 @@ export default class WoodpeckerClient extends ApiClient { return this._post('/api/repos/' + owner + '/' + repo + '/builds/' + build + '/decline'); } - restartBuild(owner: string, repo: string, build: string, opts) { + restartBuild(owner: string, repo: string, build: string, opts: Record) { var query = encodeQueryString(opts); return this._post('/api/repos/' + owner + '/' + repo + '/builds/' + build + '?' + query); } diff --git a/web-new/src/router.ts b/web-new/src/router.ts index 8a6bbcccd4..24b9f58ac2 100644 --- a/web-new/src/router.ts +++ b/web-new/src/router.ts @@ -1,5 +1,6 @@ import { Component } from 'vue'; import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'; +import { isAuthenticated } from './compositions/useAuthentication'; const routes: RouteRecordRaw[] = [ { @@ -8,11 +9,24 @@ const routes: RouteRecordRaw[] = [ component: (): Component => import('~/views/Home.vue'), }, { - path: '/projects', - name: 'projects', - component: (): Component => import('~/views/Home.vue'), + path: '/repos', + name: 'repos', + component: (): Component => import('~/views/repos/Repos.vue'), + meta: { authentication: 'required' }, + }, + { + path: '/repo/add', + name: 'repo-add', + component: (): Component => import('~/views/repos/RepoAdd.vue'), meta: { authentication: 'required' }, }, + { + path: '/repo/:repoOwner/:repoId', + name: 'repo', + component: (): Component => import('~/views/repos/Repo.vue'), + meta: { authentication: 'required' }, + props: true, + }, { path: '/do-login/:origin?', name: 'login', @@ -32,7 +46,7 @@ const router = createRouter({ }); router.beforeEach(async (to, _, next) => { - if (to.meta.authentication === 'required') { + if (to.meta.authentication === 'required' && !isAuthenticated()) { next({ name: 'login', params: { origin: to.fullPath } }); return; } diff --git a/web-new/src/views/Home.vue b/web-new/src/views/Home.vue index 0b938e68f2..44d96a5588 100644 --- a/web-new/src/views/Home.vue +++ b/web-new/src/views/Home.vue @@ -4,8 +4,15 @@ diff --git a/web-new/src/views/Login.vue b/web-new/src/views/Login.vue index 8cd801ddd5..c5b72a8fd2 100644 --- a/web-new/src/views/Login.vue +++ b/web-new/src/views/Login.vue @@ -1,27 +1,42 @@ diff --git a/web-new/src/views/repos/RepoAdd.vue b/web-new/src/views/repos/RepoAdd.vue new file mode 100644 index 0000000000..9d9fdbc9c6 --- /dev/null +++ b/web-new/src/views/repos/RepoAdd.vue @@ -0,0 +1,46 @@ + + + diff --git a/web-new/src/views/repos/Repos.vue b/web-new/src/views/repos/Repos.vue new file mode 100644 index 0000000000..c4162457fe --- /dev/null +++ b/web-new/src/views/repos/Repos.vue @@ -0,0 +1,37 @@ + + + diff --git a/web-new/vite.config.ts b/web-new/vite.config.ts index 05906068dc..c682152529 100644 --- a/web-new/vite.config.ts +++ b/web-new/vite.config.ts @@ -11,13 +11,4 @@ export default defineConfig({ '~/': `${path.resolve(__dirname, 'src')}/`, }, }, - server: { - proxy: { - '/api': { - target: 'http://localhost:4000', - ws: true, - changeOrigin: true, - }, - }, - }, }); diff --git a/web-new/windi.config.ts b/web-new/windi.config.ts index 84377135b6..80829811ac 100644 --- a/web-new/windi.config.ts +++ b/web-new/windi.config.ts @@ -5,8 +5,10 @@ import typography from 'windicss/plugin/typography'; export default defineConfig({ darkMode: 'class', theme: { - colors: { - green: '#4caf50', + extend: { + colors: { + green: '#4caf50', + }, }, }, plugins: [typography], From e7e26f579c9f55d0fdf35a5139d0c950987220e0 Mon Sep 17 00:00:00 2001 From: Anton Bracke Date: Mon, 19 Jul 2021 15:25:38 +0200 Subject: [PATCH 006/143] feat: add www-proxy for development --- cmd/drone-server/flags.go | 6 ++++++ cmd/drone-server/server.go | 27 +++++++++++++++++++++++---- router/router.go | 6 +++--- 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/cmd/drone-server/flags.go b/cmd/drone-server/flags.go index 965872169e..c7ba15ea40 100644 --- a/cmd/drone-server/flags.go +++ b/cmd/drone-server/flags.go @@ -63,6 +63,12 @@ var flags = []cli.Flag{ Usage: "serve the website from disk", Hidden: true, }, + cli.StringFlag{ + EnvVar: "WOODPECKER_WWW_PROXY", + Name: "www-proxy", + Usage: "serve the website with a proxy", + Hidden: true, + }, cli.StringSliceFlag{ EnvVar: "DRONE_ADMIN,WOODPECKER_ADMIN", Name: "admin", diff --git a/cmd/drone-server/server.go b/cmd/drone-server/server.go index 8c909e0594..9b601a8381 100644 --- a/cmd/drone-server/server.go +++ b/cmd/drone-server/server.go @@ -20,6 +20,7 @@ import ( "errors" "net" "net/http" + "net/http/httputil" "net/url" "os" "path/filepath" @@ -83,13 +84,31 @@ func server(c *cli.Context) error { store_ := setupStore(c) setupEvilGlobals(c, store_, remote_) - // we are switching from gin to httpservermux|treemux, - // so if this code looks strange, that is why. - tree := setupTree(c) + proxyWebUI := c.String("www-proxy") + + var webUIServe func(w http.ResponseWriter, r *http.Request) + + if proxyWebUI == "" { + // we are switching from gin to httpservermux|treemux, + // so if this code looks strange, that is why. + webUIServe = setupTree(c).ServeHTTP + } else { + origin, _ := url.Parse(proxyWebUI) + + director := func(req *http.Request) { + req.Header.Add("X-Forwarded-Host", req.Host) + req.Header.Add("X-Origin-Host", origin.Host) + req.URL.Scheme = origin.Scheme + req.URL.Host = origin.Host + } + + proxy := &httputil.ReverseProxy{Director: director} + webUIServe = proxy.ServeHTTP + } // setup the server and start the listener handler := router.Load( - tree, + webUIServe, ginrus.Ginrus(logrus.StandardLogger(), time.RFC3339, true), middleware.Version, middleware.Config(c), diff --git a/router/router.go b/router/router.go index a695787268..27757ef592 100644 --- a/router/router.go +++ b/router/router.go @@ -17,7 +17,6 @@ package router import ( "net/http" - "github.com/dimfeld/httptreemux" "github.com/gin-gonic/gin" "github.com/woodpecker-ci/woodpecker/router/middleware/header" @@ -30,7 +29,7 @@ import ( ) // Load loads the router -func Load(mux *httptreemux.ContextMux, middleware ...gin.HandlerFunc) http.Handler { +func Load(serveHTTP func(w http.ResponseWriter, r *http.Request), middleware ...gin.HandlerFunc) http.Handler { e := gin.New() e.Use(gin.Recovery()) @@ -49,7 +48,8 @@ func Load(mux *httptreemux.ContextMux, middleware ...gin.HandlerFunc) http.Handl session.User(c), ), ) - mux.ServeHTTP(c.Writer, req) + + serveHTTP(c.Writer, req) }) e.GET("/logout", server.GetLogout) From 3235ef0994e87053d00c0799a47337c006b60885 Mon Sep 17 00:00:00 2001 From: Anton Bracke Date: Mon, 19 Jul 2021 15:32:56 +0200 Subject: [PATCH 007/143] feat: create new endpoint serving a js file containing the web config --- router/router.go | 2 ++ server/web/config.go | 80 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 server/web/config.go diff --git a/router/router.go b/router/router.go index a695787268..83bb6a4683 100644 --- a/router/router.go +++ b/router/router.go @@ -188,6 +188,8 @@ func Load(mux *httptreemux.ContextMux, middleware ...gin.HandlerFunc) http.Handl debugger.GET("/pprof/trace", debug.TraceHandler()) } + e.GET("/api/web-config.js", web.WebConfig) + monitor := e.Group("/metrics") { monitor.GET("", metrics.PromHandler()) diff --git a/server/web/config.go b/server/web/config.go new file mode 100644 index 0000000000..d59b487676 --- /dev/null +++ b/server/web/config.go @@ -0,0 +1,80 @@ +// Copyright 2018 Drone.IO Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package web + +import ( + "encoding/json" + "fmt" + "net/http" + "text/template" + "time" + + "github.com/gin-gonic/gin" + "github.com/woodpecker-ci/woodpecker/router/middleware/session" + "github.com/woodpecker-ci/woodpecker/shared/token" + "github.com/woodpecker-ci/woodpecker/version" +) + +func WebConfig(c *gin.Context) { + var err error + + user := session.User(c) + + var csrf string + if user != nil { + csrf, _ = token.New( + token.CsrfToken, + user.Login, + ).Sign(user.Hash) + } + + var syncing bool + if user != nil { + syncing = time.Unix(user.Synced, 0).Add(time.Hour * 72).Before(time.Now()) + } + + configData := map[string]interface{}{ + "user": user, + "csrf": csrf, + "syncing": syncing, + "docs": nil, // TODO + "version": version.String(), + } + + // default func map with json parser. + var funcMap = template.FuncMap{ + "json": func(v interface{}) string { + a, _ := json.Marshal(v) + return string(a) + }, + } + + c.Header("Content-Type", "text/javascript; charset=utf-8") + tmpl := template.Must(template.New("").Funcs(funcMap).Parse(configTemplate)) + + err = tmpl.Execute(c.Writer, configData) + if err != nil { + fmt.Println(err) + c.AbortWithError(http.StatusInternalServerError, nil) + } +} + +const configTemplate = ` +window.WOODPECKER_USER = {{ json .user }}; +window.WOODPECKER_SYNC = {{ .syncing }}; +window.WOODPECKER_CSRF = "{{ .csrf }}"; +window.WOODPECKER_VERSION = "{{ .version }}"; +window.WOODPECKER_DOCS = "{{ .docs }}"; +` From 673927dd39b6ffd652f28545641903d1ee3bf243 Mon Sep 17 00:00:00 2001 From: Anbraten Date: Mon, 19 Jul 2021 15:41:52 +0200 Subject: [PATCH 008/143] Update cmd/drone-server/server.go Co-authored-by: 6543 <6543@obermui.de> --- cmd/drone-server/server.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/drone-server/server.go b/cmd/drone-server/server.go index 9b601a8381..b044e48954 100644 --- a/cmd/drone-server/server.go +++ b/cmd/drone-server/server.go @@ -90,7 +90,6 @@ func server(c *cli.Context) error { if proxyWebUI == "" { // we are switching from gin to httpservermux|treemux, - // so if this code looks strange, that is why. webUIServe = setupTree(c).ServeHTTP } else { origin, _ := url.Parse(proxyWebUI) From b53a4bbf2dab7af6ffe257889b774a5dd1770586 Mon Sep 17 00:00:00 2001 From: Anton Bracke Date: Tue, 20 Jul 2021 13:40:17 +0200 Subject: [PATCH 009/143] Load config via js endpoint --- server/web/template.go | 112 ---------------------------------- server/web/template_test.go | 48 --------------- server/web/web.go | 45 +++----------- web/src/config/state.js | 4 +- web/src/index.html | 5 +- web/src/screens/feed/index.js | 10 ++- web/vendor/drone-js/index.js | 16 +---- web/webpack.config.js | 20 +----- 8 files changed, 25 insertions(+), 235 deletions(-) delete mode 100644 server/web/template.go delete mode 100644 server/web/template_test.go diff --git a/server/web/template.go b/server/web/template.go deleted file mode 100644 index b48675429a..0000000000 --- a/server/web/template.go +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright 2018 Drone.IO Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package web - -import ( - "bytes" - "encoding/json" - "fmt" - "html/template" - "strings" - - "golang.org/x/net/html" -) - -// default func map with json parser. -var funcMap = template.FuncMap{ - "json": func(v interface{}) template.JS { - a, _ := json.Marshal(v) - return template.JS(a) - }, -} - -// helper function creates a new template from the text string. -func mustCreateTemplate(text string) *template.Template { - templ, err := createTemplate(text) - if err != nil { - panic(err) - } - return templ -} - -// helper function creates a new template from the text string. -func createTemplate(text string) (*template.Template, error) { - templ, err := template.New("_").Funcs(funcMap).Parse(partials) - if err != nil { - return nil, err - } - return templ.Parse( - injectPartials(text), - ) -} - -// helper function that parses the html file and injects -// named partial templates. -func injectPartials(s string) string { - w := new(bytes.Buffer) - r := bytes.NewBufferString(s) - t := html.NewTokenizer(r) - for { - tt := t.Next() - if tt == html.ErrorToken { - break - } - if tt == html.CommentToken { - txt := string(t.Text()) - txt = strings.TrimSpace(txt) - seg := strings.Split(txt, ":") - if len(seg) == 2 && seg[0] == "drone" { - fmt.Fprintf(w, "{{ template %q . }}", seg[1]) - continue - } - } - w.Write(t.Raw()) - } - return w.String() -} - -const partials = ` -{{define "user"}} -{{ if .user }} - -{{ end }} -{{end}} - -{{define "csrf"}} -{{ if .csrf -}} - -{{- end }} -{{end}} - -{{define "version"}} - - -{{end}} - -{{define "docs"}} -{{ if .docs -}} - -{{- end }} -{{end}} -` diff --git a/server/web/template_test.go b/server/web/template_test.go deleted file mode 100644 index 6ad8a28509..0000000000 --- a/server/web/template_test.go +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2018 Drone.IO Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package web - -import ( - "testing" -) - -func Test_injectPartials(t *testing.T) { - got, want := injectPartials(before), after - if got != want { - t.Errorf("Want html %q, got %q", want, got) - } -} - -var before = ` - - - - - - - - -` - -var after = ` - - - - {{ template "version" . }} - {{ template "user" . }} - {{ template "csrf" . }} - - -` diff --git a/server/web/web.go b/server/web/web.go index 9faa9e5e5e..7f3427e918 100644 --- a/server/web/web.go +++ b/server/web/web.go @@ -18,15 +18,12 @@ import ( "context" "crypto/md5" "fmt" - "html/template" "io/ioutil" "net/http" "path/filepath" "time" "github.com/woodpecker-ci/woodpecker/model" - "github.com/woodpecker-ci/woodpecker/shared/token" - "github.com/woodpecker-ci/woodpecker/version" "github.com/woodpecker-ci/woodpecker/web/dist" "github.com/dimfeld/httptreemux" @@ -50,11 +47,9 @@ func New(opt ...Option) Endpoint { } return &website{ - fs: dist.New(), - opts: opts, - tmpl: mustCreateTemplate( - string(dist.MustLookup("/index.html")), - ), + fs: dist.New(), + opts: opts, + content: dist.MustLookup("/index.html"), } } @@ -65,16 +60,16 @@ func fromPath(opts *Options) *website { panic(err) } return &website{ - fs: http.Dir(opts.path), - tmpl: mustCreateTemplate(string(b)), - opts: opts, + fs: http.Dir(opts.path), + content: b, + opts: opts, } } type website struct { - opts *Options - fs http.FileSystem - tmpl *template.Template + opts *Options + fs http.FileSystem + content []byte } func (w *website) Register(mux *httptreemux.ContextMux) { @@ -87,28 +82,8 @@ func (w *website) Register(mux *httptreemux.ContextMux) { func (w *website) handleIndex(rw http.ResponseWriter, r *http.Request) { rw.WriteHeader(200) - - var csrf string - var user, _ = ToUser(r.Context()) - if user != nil { - csrf, _ = token.New( - token.CsrfToken, - user.Login, - ).Sign(user.Hash) - } - var syncing bool - if user != nil { - syncing = time.Unix(user.Synced, 0).Add(w.opts.sync).Before(time.Now()) - } - params := map[string]interface{}{ - "user": user, - "csrf": csrf, - "syncing": syncing, - "version": version.String(), - } rw.Header().Set("Content-Type", "text/html; charset=UTF-8") - - w.tmpl.Execute(rw, params) + rw.Write(w.content) } func setupCache(h http.Handler) http.Handler { diff --git a/web/src/config/state.js b/web/src/config/state.js index bf5a8dfedc..ac48114948 100644 --- a/web/src/config/state.js +++ b/web/src/config/state.js @@ -1,7 +1,7 @@ import Baobab from "baobab"; -const user = window.DRONE_USER; -const sync = window.DRONE_SYNC; +const user = window.WOODPECKER_USER; +const sync = window.WOODPECKER_SYNC; const state = { follow: false, diff --git a/web/src/index.html b/web/src/index.html index 3e5a764f71..7116546838 100644 --- a/web/src/index.html +++ b/web/src/index.html @@ -2,10 +2,7 @@ - - - - + diff --git a/web/src/screens/feed/index.js b/web/src/screens/feed/index.js index 70891049c5..a1388860ee 100644 --- a/web/src/screens/feed/index.js +++ b/web/src/screens/feed/index.js @@ -168,10 +168,16 @@ const LOGO = (

- Woodpecker{window.DRONE_VERSION} + Woodpecker + {window.WOODPECKER_VERSION} +
- + Docs diff --git a/web/vendor/drone-js/index.js b/web/vendor/drone-js/index.js index 58f2215912..b596f40e54 100644 --- a/web/vendor/drone-js/index.js +++ b/web/vendor/drone-js/index.js @@ -380,23 +380,13 @@ } ], [ - { - key: "fromEnviron", - value: function fromEnviron() { - return new DroneClient( - process && process.env && process.env.DRONE_SERVER, - process && process.env && process.env.DRONE_TOKEN, - process && process.env && process.env.DRONE_CSRF - ); - } - }, { key: "fromWindow", value: function fromWindow() { return new DroneClient( - window && window.DRONE_SERVER, - window && window.DRONE_TOKEN, - window && window.DRONE_CSRF + window && window.WOODPECKER_SERVER, + window && window.WOODPECKER_TOKEN, + window && window.WOODPECKER_CSRF ); } } diff --git a/web/webpack.config.js b/web/webpack.config.js index 7058a937b5..031bc77d13 100644 --- a/web/webpack.config.js +++ b/web/webpack.config.js @@ -124,25 +124,7 @@ module.exports = { } }) ] - : [ - new webpack.DefinePlugin({ - // drone server uses authorization cookies, but the client can - // optionally source the authorization token from the environment. - // this should be used for the test server only. - "window.DRONE_TOKEN": JSON.stringify(process.env.DRONE_TOKEN), - "window.DRONE_SERVER": JSON.stringify(process.env.DRONE_SERVER), - - // drone server provides the currently authenticated user in the - // index.html file. For testing purposes we simulate this and provides - // a dummy user object. - "window.DRONE_USER": { - login: JSON.stringify("octocat"), - avatar_url: JSON.stringify( - "https://avatars3.githubusercontent.com/u/583231" - ) - } - }) - ] + : [] ), devServer: { From a5b6a320ce7efc8c3db211c45b5b4a11a7798740 Mon Sep 17 00:00:00 2001 From: Anton Bracke Date: Tue, 20 Jul 2021 13:40:59 +0200 Subject: [PATCH 010/143] Remove docs setting --- cmd/drone-server/flags.go | 6 ------ cmd/drone-server/setup.go | 1 - server/web/opts.go | 8 -------- server/web/opts_test.go | 8 -------- 4 files changed, 23 deletions(-) diff --git a/cmd/drone-server/flags.go b/cmd/drone-server/flags.go index c7ba15ea40..18e9c90dca 100644 --- a/cmd/drone-server/flags.go +++ b/cmd/drone-server/flags.go @@ -95,12 +95,6 @@ var flags = []cli.Flag{ Usage: "file path for the drone config", Value: ".drone.yml", }, - cli.StringFlag{ - EnvVar: "DRONE_DOCS,WOODPECKER_DOCS", - Name: "docs", - Usage: "link to user documentation", - Value: "https://woodpecker.laszlo.cloud", - }, cli.DurationFlag{ EnvVar: "DRONE_SESSION_EXPIRES,WOODPECKER_SESSION_EXPIRES", Name: "session-expires", diff --git a/cmd/drone-server/setup.go b/cmd/drone-server/setup.go index 8650309104..719760bd3a 100644 --- a/cmd/drone-server/setup.go +++ b/cmd/drone-server/setup.go @@ -211,7 +211,6 @@ func setupTree(c *cli.Context) *httptreemux.ContextMux { web.New( web.WithDir(c.String("www")), web.WithSync(time.Hour*72), - web.WithDocs(c.String("docs")), ).Register(tree) return tree } diff --git a/server/web/opts.go b/server/web/opts.go index 32ba6d5939..0b419dce5a 100644 --- a/server/web/opts.go +++ b/server/web/opts.go @@ -41,11 +41,3 @@ func WithDir(s string) Option { o.path = s } } - -// WithDocs configures the website handler with the documentation -// website address, which should be included in the user interface. -func WithDocs(s string) Option { - return func(o *Options) { - o.docs = s - } -} diff --git a/server/web/opts_test.go b/server/web/opts_test.go index a217f2f024..c50e8ea853 100644 --- a/server/web/opts_test.go +++ b/server/web/opts_test.go @@ -34,11 +34,3 @@ func TestWithDir(t *testing.T) { t.Errorf("Want www directory %q, got %q", want, got) } } - -func TestWithDocs(t *testing.T) { - opts := new(Options) - WithDocs("http://docs.drone.io")(opts) - if got, want := opts.docs, "http://docs.drone.io"; got != want { - t.Errorf("Want documentation url %q, got %q", want, got) - } -} From 7b6488a3e79ff85bcd9ae784cb6a59b2f922bea5 Mon Sep 17 00:00:00 2001 From: Anton Bracke Date: Tue, 20 Jul 2021 14:10:44 +0200 Subject: [PATCH 011/143] Use server-host as source for public links --- cmd/drone-server/server.go | 8 +++++++- server/build.go | 5 ++--- server/hook.go | 3 +-- server/repo.go | 9 ++++----- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/cmd/drone-server/server.go b/cmd/drone-server/server.go index 8c909e0594..6f669694d6 100644 --- a/cmd/drone-server/server.go +++ b/cmd/drone-server/server.go @@ -69,6 +69,12 @@ func server(c *cli.Context) error { ) } + if strings.Contains(c.String("server-host"), "://localhost") { + logrus.Warningln( + "DRONE_HOST/DRONE_SERVER_HOST/WOODPECKER_HOST/WOODPECKER_SERVER_HOST should probably be publicly accessible (not localhost)", + ) + } + if strings.HasSuffix(c.String("server-host"), "/") { logrus.Fatalln( "DRONE_HOST/DRONE_SERVER_HOST/WOODPECKER_HOST/WOODPECKER_SERVER_HOST must not have trailing slash", @@ -224,7 +230,7 @@ func setupEvilGlobals(c *cli.Context, v store.Store, r remote.Remote) { droneserver.Config.Server.Cert = c.String("server-cert") droneserver.Config.Server.Key = c.String("server-key") droneserver.Config.Server.Pass = c.String("agent-secret") - droneserver.Config.Server.Host = strings.TrimRight(c.String("server-host"), "/") + droneserver.Config.Server.Host = c.String("server-host") droneserver.Config.Server.Port = c.String("server-addr") droneserver.Config.Server.RepoConfig = c.String("repo-config") droneserver.Config.Server.SessionExpires = c.Duration("session-expires") diff --git a/server/build.go b/server/build.go index db983467bd..ab6a7b6784 100644 --- a/server/build.go +++ b/server/build.go @@ -31,7 +31,6 @@ import ( "github.com/sirupsen/logrus" "github.com/woodpecker-ci/woodpecker/cncd/queue" "github.com/woodpecker-ci/woodpecker/remote" - "github.com/woodpecker-ci/woodpecker/shared/httputil" "github.com/woodpecker-ci/woodpecker/store" "github.com/woodpecker-ci/woodpecker/model" @@ -309,7 +308,7 @@ func PostApproval(c *gin.Context) { Netrc: netrc, Secs: secs, Regs: regs, - Link: httputil.GetURL(c.Request), + Link: Config.Server.Host, Yamls: yamls, Envs: envs, } @@ -518,7 +517,7 @@ func PostBuild(c *gin.Context) { Netrc: netrc, Secs: secs, Regs: regs, - Link: httputil.GetURL(c.Request), + Link: Config.Server.Host, Yamls: yamls, Envs: buildParams, } diff --git a/server/hook.go b/server/hook.go index b9abed9a45..4feb7a3b50 100644 --- a/server/hook.go +++ b/server/hook.go @@ -33,7 +33,6 @@ import ( "github.com/sirupsen/logrus" "github.com/woodpecker-ci/woodpecker/model" "github.com/woodpecker-ci/woodpecker/remote" - "github.com/woodpecker-ci/woodpecker/shared/httputil" "github.com/woodpecker-ci/woodpecker/shared/token" "github.com/woodpecker-ci/woodpecker/store" @@ -257,7 +256,7 @@ func PostHook(c *gin.Context) { Secs: secs, Regs: regs, Envs: envs, - Link: httputil.GetURL(c.Request), + Link: Config.Server.Host, Yamls: remoteYamlConfigs, } buildItems, err := b.Build() diff --git a/server/repo.go b/server/repo.go index 0288cb9195..b313bc6d7a 100644 --- a/server/repo.go +++ b/server/repo.go @@ -26,7 +26,6 @@ import ( "github.com/woodpecker-ci/woodpecker/model" "github.com/woodpecker-ci/woodpecker/remote" "github.com/woodpecker-ci/woodpecker/router/middleware/session" - "github.com/woodpecker-ci/woodpecker/shared/httputil" "github.com/woodpecker-ci/woodpecker/shared/token" "github.com/woodpecker-ci/woodpecker/store" ) @@ -75,7 +74,7 @@ func PostRepo(c *gin.Context) { link := fmt.Sprintf( "%s/hook?access_token=%s", - httputil.GetURL(c.Request), + Config.Server.Host, sig, ) @@ -203,7 +202,7 @@ func DeleteRepo(c *gin.Context) { } } - remote.Deactivate(user, repo, httputil.GetURL(c.Request)) + remote.Deactivate(user, repo, Config.Server.Host) c.JSON(200, repo) } @@ -221,7 +220,7 @@ func RepairRepo(c *gin.Context) { } // reconstruct the link - host := httputil.GetURL(c.Request) + host := Config.Server.Host link := fmt.Sprintf( "%s/hook?access_token=%s", host, @@ -307,7 +306,7 @@ func MoveRepo(c *gin.Context) { } // reconstruct the link - host := httputil.GetURL(c.Request) + host := Config.Server.Host link := fmt.Sprintf( "%s/hook?access_token=%s", host, From 0c6a4617e2c1b57ee07ecc99b4d6a0eff06a0c65 Mon Sep 17 00:00:00 2001 From: Anton Bracke Date: Tue, 20 Jul 2021 14:30:51 +0200 Subject: [PATCH 012/143] Load docs link from server --- server/web/config.go | 2 +- web/src/screens/feed/index.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/web/config.go b/server/web/config.go index d59b487676..2fd1e054a6 100644 --- a/server/web/config.go +++ b/server/web/config.go @@ -49,7 +49,7 @@ func WebConfig(c *gin.Context) { "user": user, "csrf": csrf, "syncing": syncing, - "docs": nil, // TODO + "docs": "https://woodpecker.laszlo.cloud", "version": version.String(), } diff --git a/web/src/screens/feed/index.js b/web/src/screens/feed/index.js index a1388860ee..574be576dd 100644 --- a/web/src/screens/feed/index.js +++ b/web/src/screens/feed/index.js @@ -174,7 +174,7 @@ const LOGO = (
From 72718a041e4978d768b8c71482aa71cf3e665863 Mon Sep 17 00:00:00 2001 From: Anton Bracke Date: Tue, 20 Jul 2021 19:27:37 +0200 Subject: [PATCH 013/143] further ui improvements --- web-new/.pnpm-debug.log | 20 ++ web-new/package.json | 6 + web-new/pnpm-lock.yaml | 182 ++++++++++++++++-- web-new/src/App.vue | 20 +- web-new/src/assets/pecking_woodpecker.gif | Bin 0 -> 616632 bytes web-new/src/components/Navbar.vue | 38 ---- .../src/components/build-feed/FeedSidebar.vue | 58 ++++++ web-new/src/components/layout/Breadcrumbs.vue | 32 +++ .../src/components/layout/FluidContainer.vue | 14 ++ web-new/src/components/layout/Navbar.vue | 50 +++++ web-new/src/components/repo/BuildItem.vue | 103 ++++++++++ web-new/src/compositions/useAuthentication.ts | 19 +- web-new/src/compositions/useNotifications.ts | 5 + web-new/src/lib/api/index.ts | 6 +- web-new/src/lib/api/types/build.ts | 67 +++++++ web-new/src/lib/api/types/index.ts | 1 + web-new/src/lib/api/types/repo.ts | 3 + web-new/src/main.ts | 6 + web-new/src/router.ts | 43 ++++- web-new/src/views/Admin.vue | 23 +++ web-new/src/views/repo/Repo.vue | 68 +++++++ web-new/src/views/{repos => repo}/RepoAdd.vue | 17 +- web-new/src/views/repo/RepoSettings.vue | 64 ++++++ .../{repos/Repo.vue => repo/RepoWrapper.vue} | 34 ++-- web-new/src/views/{repos => repo}/Repos.vue | 17 +- web-new/src/views/repo/build/Build.vue | 72 +++++++ 26 files changed, 866 insertions(+), 102 deletions(-) create mode 100644 web-new/.pnpm-debug.log create mode 100644 web-new/src/assets/pecking_woodpecker.gif delete mode 100644 web-new/src/components/Navbar.vue create mode 100644 web-new/src/components/build-feed/FeedSidebar.vue create mode 100644 web-new/src/components/layout/Breadcrumbs.vue create mode 100644 web-new/src/components/layout/FluidContainer.vue create mode 100644 web-new/src/components/layout/Navbar.vue create mode 100644 web-new/src/components/repo/BuildItem.vue create mode 100644 web-new/src/compositions/useNotifications.ts create mode 100644 web-new/src/lib/api/types/build.ts create mode 100644 web-new/src/views/Admin.vue create mode 100644 web-new/src/views/repo/Repo.vue rename web-new/src/views/{repos => repo}/RepoAdd.vue (61%) create mode 100644 web-new/src/views/repo/RepoSettings.vue rename web-new/src/views/{repos/Repo.vue => repo/RepoWrapper.vue} (52%) rename web-new/src/views/{repos => repo}/Repos.vue (67%) create mode 100644 web-new/src/views/repo/build/Build.vue diff --git a/web-new/.pnpm-debug.log b/web-new/.pnpm-debug.log new file mode 100644 index 0000000000..19ba5ee5cc --- /dev/null +++ b/web-new/.pnpm-debug.log @@ -0,0 +1,20 @@ +{ + "0 debug pnpm:scope": { + "selected": 1 + }, + "1 error pnpm": { + "code": "ELIFECYCLE", + "errno": "ENOENT", + "syscall": "spawn", + "file": "sh", + "pkgid": "@0.0.0", + "stage": "start", + "script": "vite", + "err": { + "name": "Error", + "message": "@0.0.0 start: `vite`\nspawn ENOENT", + "code": "ELIFECYCLE", + "stack": "Error: @0.0.0 start: `vite`\nspawn ENOENT\n at ChildProcess. (/home/anton/.nvm/versions/node/v14.15.4/pnpm-global/5/node_modules/.pnpm/pnpm@6.10.3/node_modules/pnpm/dist/pnpm.cjs:92970:22)\n at ChildProcess.emit (events.js:315:20)\n at maybeClose (internal/child_process.js:1048:16)\n at Process.ChildProcess._handle.onexit (internal/child_process.js:288:5)" + } + } +} \ No newline at end of file diff --git a/web-new/package.json b/web-new/package.json index dbcbe9f651..19b30217a7 100644 --- a/web-new/package.json +++ b/web-new/package.json @@ -8,10 +8,16 @@ "format": "prettier --write src/**/*.js" }, "dependencies": { + "@kyvg/vue3-notification": "^2.3.3", + "@meforma/vue-toaster": "^1.2.2", + "@types/javascript-time-ago": "^2.0.3", + "humanize-duration": "^3.27.0", + "javascript-time-ago": "^2.3.8", "vue": "^3.0.5", "vue-router": "4.0.10" }, "devDependencies": { + "@types/humanize-duration": "^3.25.1", "@types/node": "^16.3.3", "@vitejs/plugin-vue": "^1.2.5", "@vue/compiler-sfc": "^3.0.5", diff --git a/web-new/pnpm-lock.yaml b/web-new/pnpm-lock.yaml index 8c81b2d80f..ef139647b7 100644 --- a/web-new/pnpm-lock.yaml +++ b/web-new/pnpm-lock.yaml @@ -1,9 +1,15 @@ lockfileVersion: 5.3 specifiers: + '@kyvg/vue3-notification': ^2.3.3 + '@meforma/vue-toaster': ^1.2.2 + '@types/humanize-duration': ^3.25.1 + '@types/javascript-time-ago': ^2.0.3 '@types/node': ^16.3.3 '@vitejs/plugin-vue': ^1.2.5 '@vue/compiler-sfc': ^3.0.5 + humanize-duration: ^3.27.0 + javascript-time-ago: ^2.3.8 typescript: ^4.3.2 vite: ^2.4.2 vite-plugin-windicss: ^1.2.4 @@ -13,10 +19,16 @@ specifiers: windicss: 3.1.0 dependencies: + '@kyvg/vue3-notification': 2.3.3_vue@3.1.5 + '@meforma/vue-toaster': 1.2.2 + '@types/javascript-time-ago': 2.0.3 + humanize-duration: 3.27.0 + javascript-time-ago: 2.3.8 vue: 3.1.5 vue-router: 4.0.10_vue@3.1.5 devDependencies: + '@types/humanize-duration': 3.25.1 '@types/node': 16.3.3 '@vitejs/plugin-vue': 1.2.5_@vue+compiler-sfc@3.1.5 '@vue/compiler-sfc': 3.1.5_vue@3.1.5 @@ -50,6 +62,21 @@ packages: '@babel/helper-validator-identifier': 7.14.5 to-fast-properties: 2.0.0 + /@kyvg/vue3-notification/2.3.3_vue@3.1.5: + resolution: {integrity: sha512-NxFejSs7FlaUzsPQ4jJyrB+UCFfsBUqICcQH70wHId52Hc9G9i0rBp2Wj3UsNj21H/HitP3r92rfz4ly2ImyMA==} + peerDependencies: + vue: ^3.0.11 + dependencies: + vue: 3.1.5 + dev: false + + /@meforma/vue-toaster/1.2.2: + resolution: {integrity: sha512-J/UKmqi2BLgiYrex4OclWvID5QDZO2IHGn9ZJZF+HjnLG1TgR4VvWnHGTeyr2/EUQfFc64qC78Jd66TAp9zvzg==} + dependencies: + stylus: 0.54.8 + stylus-loader: 3.0.2_stylus@0.54.8 + dev: false + /@nodelib/fs.scandir/2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -75,6 +102,14 @@ packages: resolution: {integrity: sha512-LfZwXoGUDo0C3me81HXgkBg5CTQYb6xzEl+fNmbO4JdRiSKQ8A0GD1OBBvKAIsbCUgoyAty7m99GqqMQe784ew==} dev: true + /@types/humanize-duration/3.25.1: + resolution: {integrity: sha512-WZU/4bb+lvzyDmZzjJtp++9mfKy6B3lH6gGISgkcz6SU8hMILKRM0vi08TxIsb0dQB4Gzo68MWLmctu6xqUi9g==} + dev: true + + /@types/javascript-time-ago/2.0.3: + resolution: {integrity: sha512-G6SdYh6gHxgCTU0s4cMIRHwRO4p3f7jQSZbDPfUOZpUAG1od3rTjT0e8rxGThUiTTWQHwpBRws8eHO8D2QqfkA==} + dev: false + /@types/node/16.3.3: resolution: {integrity: sha512-8h7k1YgQKxKXWckzFCMfsIwn0Y61UK6tlD6y2lOb3hTOIMlK3t9/QwHOhc81TwU+RMf0As5fj7NPjroERCnejQ==} dev: true @@ -198,9 +233,14 @@ packages: color-convert: 2.0.1 dev: true + /atob/2.1.2: + resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==} + engines: {node: '>= 4.5.0'} + hasBin: true + dev: false + /balanced-match/1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - dev: true /big-integer/1.6.48: resolution: {integrity: sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w==} @@ -209,7 +249,6 @@ packages: /big.js/5.2.2: resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} - dev: true /binary/0.3.0: resolution: {integrity: sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=} @@ -231,7 +270,6 @@ packages: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - dev: true /braces/3.0.2: resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} @@ -281,7 +319,6 @@ packages: /concat-map/0.0.1: resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} - dev: true /consolidate/0.16.0: resolution: {integrity: sha512-Nhl1wzCslqXYTJVDyJCu3ODohy9OfBMB5uD2BiBTzd7w+QY0lBzafkR8y8755yMYHAaMD4NuzbAw03/xzfw+eQ==} @@ -294,6 +331,21 @@ packages: resolution: {integrity: sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=} dev: true + /css-parse/2.0.0: + resolution: {integrity: sha1-pGjuZnwW2BzPBcWMONKpfHgNv9Q=} + dependencies: + css: 2.2.4 + dev: false + + /css/2.2.4: + resolution: {integrity: sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==} + dependencies: + inherits: 2.0.4 + source-map: 0.6.1 + source-map-resolve: 0.5.3 + urix: 0.1.0 + dev: false + /cssesc/3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -304,6 +356,12 @@ packages: resolution: {integrity: sha512-u1wmTI1jJGzCJzWndZo8mk4wnPTZd1eOIYTYvuEyOQGfmDl3TrabCCfKnOC86FZwW/9djqTl933UF/cS425i9A==} dev: false + /debug/3.1.0: + resolution: {integrity: sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==} + dependencies: + ms: 2.0.0 + dev: false + /debug/4.3.2: resolution: {integrity: sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==} engines: {node: '>=6.0'} @@ -316,6 +374,11 @@ packages: ms: 2.1.2 dev: true + /decode-uri-component/0.2.0: + resolution: {integrity: sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=} + engines: {node: '>=0.10'} + dev: false + /duplexer2/0.1.4: resolution: {integrity: sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=} dependencies: @@ -325,7 +388,6 @@ packages: /emojis-list/3.0.0: resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==} engines: {node: '>= 4'} - dev: true /esbuild/0.12.15: resolution: {integrity: sha512-72V4JNd2+48eOVCXx49xoSWHgC3/cCy96e7mbXKY+WOWghN00cCmlGnwVLRhRHorvv0dgCyuMYBZlM2xDM5OQw==} @@ -362,7 +424,6 @@ packages: /fs.realpath/1.0.0: resolution: {integrity: sha1-FQStJSMVjKpA20onh8sBQRmU6k8=} - dev: true /fsevents/2.3.2: resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} @@ -407,7 +468,6 @@ packages: minimatch: 3.0.4 once: 1.4.0 path-is-absolute: 1.0.1 - dev: true /graceful-fs/4.2.6: resolution: {integrity: sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==} @@ -429,6 +489,10 @@ packages: resolution: {integrity: sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==} dev: true + /humanize-duration/3.27.0: + resolution: {integrity: sha512-qLo/08cNc3Tb0uD7jK0jAcU5cnqCM0n568918E7R2XhMr/+7F37p4EY062W/stg7tmzvknNn9b/1+UhVRzsYrQ==} + dev: false + /icss-replace-symbols/1.1.0: resolution: {integrity: sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=} dev: true @@ -447,11 +511,9 @@ packages: dependencies: once: 1.4.0 wrappy: 1.0.2 - dev: true /inherits/2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - dev: true /is-core-module/2.5.0: resolution: {integrity: sha512-TXCMSDsEHMEEZ6eCA8rwRDbLu55MRGmrctljsBX/2v1d9/GzqHOxW5c5oPSgrUt2vBFXebu9rGqckXGPWOlYpg==} @@ -480,6 +542,12 @@ packages: resolution: {integrity: sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=} dev: true + /javascript-time-ago/2.3.8: + resolution: {integrity: sha512-ahVSuInQC6iJtwy/XsburOc6JMsI0OI/84b3nAhtMlDhCm9g4Py+zuiPASnt02B4GkaURqWtiyw98ce0ICAZYQ==} + dependencies: + relative-time-format: 1.0.5 + dev: false + /jiti/1.10.1: resolution: {integrity: sha512-qux9juDtAC8HlZxAk/fku73ak4TWNLigRFTNzFShE/kw4bXVFsVu538vLXAxvNyPszXgpX4YxkXfwTYEi+zf5A==} hasBin: true @@ -490,7 +558,6 @@ packages: hasBin: true dependencies: minimist: 1.2.5 - dev: true /listenercount/1.0.1: resolution: {integrity: sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=} @@ -503,12 +570,15 @@ packages: big.js: 5.2.2 emojis-list: 3.0.0 json5: 1.0.1 - dev: true /lodash.camelcase/4.3.0: resolution: {integrity: sha1-soqmKIorn8ZRA1x3EfZathkDMaY=} dev: true + /lodash.clonedeep/4.5.0: + resolution: {integrity: sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=} + dev: false + /lru-cache/5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} dependencies: @@ -544,11 +614,9 @@ packages: resolution: {integrity: sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==} dependencies: brace-expansion: 1.1.11 - dev: true /minimist/1.2.5: resolution: {integrity: sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==} - dev: true /mkdirp/0.5.5: resolution: {integrity: sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==} @@ -557,6 +625,16 @@ packages: minimist: 1.2.5 dev: true + /mkdirp/1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + dev: false + + /ms/2.0.0: + resolution: {integrity: sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=} + dev: false + /ms/2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} dev: true @@ -571,12 +649,10 @@ packages: resolution: {integrity: sha1-WDsap3WWHUsROsF9nFC6753Xa9E=} dependencies: wrappy: 1.0.2 - dev: true /path-is-absolute/1.0.1: resolution: {integrity: sha1-F0uSaHNVNP+8es5r9TpanhtcX18=} engines: {node: '>=0.10.0'} - dev: true /path-parse/1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -685,6 +761,15 @@ packages: util-deprecate: 1.0.2 dev: true + /relative-time-format/1.0.5: + resolution: {integrity: sha512-MAgx/YKcUQYJpIaWcfetPstElnWf26JxVis4PirdwVrrymFdbxyCSm6yENpfB1YuwFbtHSHksN3aBajVNxk10Q==} + dev: false + + /resolve-url/0.2.1: + resolution: {integrity: sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=} + deprecated: https://github.com/lydell/resolve-url#deprecated + dev: false + /resolve/1.20.0: resolution: {integrity: sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==} dependencies: @@ -722,6 +807,19 @@ packages: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} dev: true + /safer-buffer/2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + dev: false + + /sax/1.2.4: + resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==} + dev: false + + /semver/6.3.0: + resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==} + hasBin: true + dev: false + /setimmediate/1.0.5: resolution: {integrity: sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=} dev: true @@ -731,10 +829,29 @@ packages: engines: {node: '>=0.10.0'} dev: true + /source-map-resolve/0.5.3: + resolution: {integrity: sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==} + dependencies: + atob: 2.1.2 + decode-uri-component: 0.2.0 + resolve-url: 0.2.1 + source-map-url: 0.4.1 + urix: 0.1.0 + dev: false + + /source-map-url/0.4.1: + resolution: {integrity: sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==} + dev: false + /source-map/0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + /source-map/0.7.3: + resolution: {integrity: sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==} + engines: {node: '>= 8'} + dev: false + /sourcemap-codec/1.4.8: resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} dev: true @@ -749,6 +866,31 @@ packages: safe-buffer: 5.1.2 dev: true + /stylus-loader/3.0.2_stylus@0.54.8: + resolution: {integrity: sha512-+VomPdZ6a0razP+zinir61yZgpw2NfljeSsdUF5kJuEzlo3khXhY19Fn6l8QQz1GRJGtMCo8nG5C04ePyV7SUA==} + peerDependencies: + stylus: '>=0.52.4' + dependencies: + loader-utils: 1.4.0 + lodash.clonedeep: 4.5.0 + stylus: 0.54.8 + when: 3.6.4 + dev: false + + /stylus/0.54.8: + resolution: {integrity: sha512-vr54Or4BZ7pJafo2mpf0ZcwA74rpuYCZbxrHBsH8kbcXOwSfvBFwsRfpGO5OD5fhG5HDCFW737PKaawI7OqEAg==} + hasBin: true + dependencies: + css-parse: 2.0.0 + debug: 3.1.0 + glob: 7.1.7 + mkdirp: 1.0.4 + safer-buffer: 2.1.2 + sax: 1.2.4 + semver: 6.3.0 + source-map: 0.7.3 + dev: false + /supports-color/7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -792,6 +934,11 @@ packages: setimmediate: 1.0.5 dev: true + /urix/0.1.0: + resolution: {integrity: sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=} + deprecated: Please see https://github.com/lydell/urix#deprecated + dev: false + /util-deprecate/1.0.2: resolution: {integrity: sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=} dev: true @@ -847,6 +994,10 @@ packages: '@vue/shared': 3.1.5 dev: false + /when/3.6.4: + resolution: {integrity: sha1-RztRfsFZ4rhQBUl6E5g/CVQS404=} + dev: false + /windicss/3.1.0: resolution: {integrity: sha512-z49xITq4X1ltHIZyL4NwFTR2LXPJ0rbOOrhDXfLX+OfG4Au7+GAzqvNlzUfAaIbA8HSpnI04alQHUWH24KfNYA==} engines: {node: '>= 12'} @@ -861,7 +1012,6 @@ packages: /wrappy/1.0.2: resolution: {integrity: sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=} - dev: true /yallist/3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} diff --git a/web-new/src/App.vue b/web-new/src/App.vue index b3c9b7c912..d1ec5ea00e 100644 --- a/web-new/src/App.vue +++ b/web-new/src/App.vue @@ -1,19 +1,27 @@ @@ -25,6 +33,10 @@ body, width: 100%; height: 100%; } + +.vue-notification { + @apply rounded-lg p-2 text-lg; +} diff --git a/web-new/src/components/layout/Navbar.vue b/web-new/src/components/layout/header/Navbar.vue similarity index 75% rename from web-new/src/components/layout/Navbar.vue rename to web-new/src/components/layout/header/Navbar.vue index 0afffb09a9..48c2ab0fca 100644 --- a/web-new/src/components/layout/Navbar.vue +++ b/web-new/src/components/layout/header/Navbar.vue @@ -2,11 +2,11 @@

- + {{ version }} Docs - Administration + Repositories Repo-Settings -
- +
+ +
@@ -29,11 +30,12 @@ import useConfig from '~/compositions/useConfig'; import Button from '~/components/atomic/Button.vue'; import FluidContainer from '~/components/layout/FluidContainer.vue'; import useAuthentication from '~/compositions/useAuthentication'; +import ActiveBuilds from './ActiveBuilds.vue'; export default defineComponent({ name: 'NavBar', - components: { Button, FluidContainer }, + components: { Button, FluidContainer, ActiveBuilds }, setup() { const config = useConfig(); diff --git a/web-new/src/compositions/useBuildFeed.ts b/web-new/src/compositions/useBuildFeed.ts new file mode 100644 index 0000000000..32e9cb28ff --- /dev/null +++ b/web-new/src/compositions/useBuildFeed.ts @@ -0,0 +1,52 @@ +import { computed, ref } from 'vue'; +import useApiClient from '~/compositions/useApiClient'; +import { Build } from '~/lib/api/types'; +import useUserConfig from './useUserConfig'; + +let initilaized = false; +const builds = ref(); +const activeBuilds = computed(() => { + if (!builds.value) { + return undefined; + } + + return builds.value.filter((build) => ['pending', 'running', 'started'].includes(build.status)); +}); +const { userConfig, setUserConfig } = useUserConfig(); +const isBuildFeedOpen = computed(() => userConfig.value.isBuildFeedOpen); + +/** + * Compare two feed items by name. + * @param {Object} a - A feed item. + * @param {Object} b - A feed item. + * @returns {number} + */ +function compareFeedItem(a: Build, b: Build) { + return (b.started_at || b.created_at || -1) - (a.started_at || a.created_at || -1); +} + +async function init() { + const apiClient = useApiClient(); + + const b = await apiClient.getBuildFeed(); + builds.value = b.sort(compareFeedItem); + + // listen to build-feed changes +} + +export default () => { + if (!initilaized) { + init(); + } + + function toggle() { + setUserConfig('isBuildFeedOpen', !userConfig.value.isBuildFeedOpen); + } + + return { + toggle, + builds, + activeBuilds, + isBuildFeedOpen, + }; +}; diff --git a/web-new/src/compositions/useUserConfig.ts b/web-new/src/compositions/useUserConfig.ts new file mode 100644 index 0000000000..5b1796b1bd --- /dev/null +++ b/web-new/src/compositions/useUserConfig.ts @@ -0,0 +1,30 @@ +import { computed, ref } from 'vue'; + +const USER_CONFIG_KEY = 'woodpecker-user-config'; + +type UserConfig = { + isBuildFeedOpen: boolean; +}; + +const defaultUserConfig: UserConfig = { + isBuildFeedOpen: true, +}; + +function loadUserConfig(): UserConfig { + const lsData = localStorage.getItem(USER_CONFIG_KEY); + if (!lsData) { + return defaultUserConfig; + } + + return JSON.parse(lsData); +} + +const config = ref(loadUserConfig()); + +export default () => ({ + setUserConfig(key: T, value: UserConfig[T]): void { + config.value = { ...config.value, [key]: value }; + localStorage.setItem(USER_CONFIG_KEY, JSON.stringify(config.value)); + }, + userConfig: computed(() => config.value), +}); diff --git a/web-new/src/lib/api/types/build.ts b/web-new/src/lib/api/types/build.ts index 030ce47f7a..798c1662a4 100644 --- a/web-new/src/lib/api/types/build.ts +++ b/web-new/src/lib/api/types/build.ts @@ -63,5 +63,16 @@ export type Build = { jobs: Job[]; }; -export type BuildStatus = {}; +export type BuildStatus = + | 'blocked' + | 'declined' + | 'error' + | 'failure' + | 'killed' + | 'pending' + | 'running' + | 'skipped' + | 'started' + | 'success'; + export type Job = {}; diff --git a/web-new/src/router.ts b/web-new/src/router.ts index a8a8833d38..f13bb7170a 100644 --- a/web-new/src/router.ts +++ b/web-new/src/router.ts @@ -6,18 +6,18 @@ const routes: RouteRecordRaw[] = [ { path: '/', name: 'home', - component: (): Component => import('~/views/Home.vue'), + redirect: '/repos', }, { path: '/repos', name: 'repos', - component: (): Component => import('~/views/repo/Repos.vue'), + component: (): Component => import('~/views/Repos.vue'), meta: { authentication: 'required' }, }, { path: '/repo/add', name: 'repo-add', - component: (): Component => import('~/views/repo/RepoAdd.vue'), + component: (): Component => import('~/views/RepoAdd.vue'), meta: { authentication: 'required' }, }, { diff --git a/web-new/src/views/Home.vue b/web-new/src/views/Home.vue deleted file mode 100644 index 44d96a5588..0000000000 --- a/web-new/src/views/Home.vue +++ /dev/null @@ -1,18 +0,0 @@ - - - diff --git a/web-new/src/views/NotFound.vue b/web-new/src/views/NotFound.vue index 1ac2d494e4..552e4a1dfd 100644 --- a/web-new/src/views/NotFound.vue +++ b/web-new/src/views/NotFound.vue @@ -1,7 +1,7 @@ diff --git a/web-new/src/views/repo/RepoAdd.vue b/web-new/src/views/RepoAdd.vue similarity index 100% rename from web-new/src/views/repo/RepoAdd.vue rename to web-new/src/views/RepoAdd.vue diff --git a/web-new/src/views/repo/Repos.vue b/web-new/src/views/Repos.vue similarity index 100% rename from web-new/src/views/repo/Repos.vue rename to web-new/src/views/Repos.vue From 4419c335a1fafd6b24e4c95186a1a9c2c4103764 Mon Sep 17 00:00:00 2001 From: Anton Bracke Date: Wed, 21 Jul 2021 01:39:45 +0200 Subject: [PATCH 016/143] support emoji commit messages --- web-new/package.json | 5 +++- web-new/pnpm-lock.yaml | 36 +++++++++++++++++++++-- web-new/src/components/repo/BuildItem.vue | 5 ++-- web-new/src/utils/emoji.ts | 5 ++++ 4 files changed, 46 insertions(+), 5 deletions(-) create mode 100644 web-new/src/utils/emoji.ts diff --git a/web-new/package.json b/web-new/package.json index 19b30217a7..ff678eff17 100644 --- a/web-new/package.json +++ b/web-new/package.json @@ -10,15 +10,18 @@ "dependencies": { "@kyvg/vue3-notification": "^2.3.3", "@meforma/vue-toaster": "^1.2.2", - "@types/javascript-time-ago": "^2.0.3", "humanize-duration": "^3.27.0", "javascript-time-ago": "^2.3.8", + "node-emoji": "^1.10.0", + "pinia": "^2.0.0-beta.5", "vue": "^3.0.5", "vue-router": "4.0.10" }, "devDependencies": { + "@types/javascript-time-ago": "^2.0.3", "@types/humanize-duration": "^3.25.1", "@types/node": "^16.3.3", + "@types/node-emoji": "^1.8.1", "@vitejs/plugin-vue": "^1.2.5", "@vue/compiler-sfc": "^3.0.5", "typescript": "^4.3.2", diff --git a/web-new/pnpm-lock.yaml b/web-new/pnpm-lock.yaml index ef139647b7..df4934d3e8 100644 --- a/web-new/pnpm-lock.yaml +++ b/web-new/pnpm-lock.yaml @@ -6,10 +6,13 @@ specifiers: '@types/humanize-duration': ^3.25.1 '@types/javascript-time-ago': ^2.0.3 '@types/node': ^16.3.3 + '@types/node-emoji': ^1.8.1 '@vitejs/plugin-vue': ^1.2.5 '@vue/compiler-sfc': ^3.0.5 humanize-duration: ^3.27.0 javascript-time-ago: ^2.3.8 + node-emoji: ^1.10.0 + pinia: ^2.0.0-beta.5 typescript: ^4.3.2 vite: ^2.4.2 vite-plugin-windicss: ^1.2.4 @@ -21,15 +24,18 @@ specifiers: dependencies: '@kyvg/vue3-notification': 2.3.3_vue@3.1.5 '@meforma/vue-toaster': 1.2.2 - '@types/javascript-time-ago': 2.0.3 humanize-duration: 3.27.0 javascript-time-ago: 2.3.8 + node-emoji: 1.10.0 + pinia: 2.0.0-beta.5_typescript@4.3.5 vue: 3.1.5 vue-router: 4.0.10_vue@3.1.5 devDependencies: '@types/humanize-duration': 3.25.1 + '@types/javascript-time-ago': 2.0.3 '@types/node': 16.3.3 + '@types/node-emoji': 1.8.1 '@vitejs/plugin-vue': 1.2.5_@vue+compiler-sfc@3.1.5 '@vue/compiler-sfc': 3.1.5_vue@3.1.5 typescript: 4.3.5 @@ -108,7 +114,11 @@ packages: /@types/javascript-time-ago/2.0.3: resolution: {integrity: sha512-G6SdYh6gHxgCTU0s4cMIRHwRO4p3f7jQSZbDPfUOZpUAG1od3rTjT0e8rxGThUiTTWQHwpBRws8eHO8D2QqfkA==} - dev: false + dev: true + + /@types/node-emoji/1.8.1: + resolution: {integrity: sha512-0fRfA90FWm6KJfw6P9QGyo0HDTCmthZ7cWaBQndITlaWLTZ6njRyKwrwpzpg+n6kBXBIGKeUHEQuBx7bphGJkA==} + dev: true /@types/node/16.3.3: resolution: {integrity: sha512-8h7k1YgQKxKXWckzFCMfsIwn0Y61UK6tlD6y2lOb3hTOIMlK3t9/QwHOhc81TwU+RMf0As5fj7NPjroERCnejQ==} @@ -579,6 +589,10 @@ packages: resolution: {integrity: sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=} dev: false + /lodash.toarray/4.4.0: + resolution: {integrity: sha1-JMS/zWsvuji/0FlNsRedjptlZWE=} + dev: false + /lru-cache/5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} dependencies: @@ -645,6 +659,12 @@ packages: hasBin: true dev: true + /node-emoji/1.10.0: + resolution: {integrity: sha512-Yt3384If5H6BYGVHiHwTL+99OzJKHhgp82S8/dktEK73T26BazdgZ4JZh92xSVtGNJvz9UbXdNAc5hcrXV42vw==} + dependencies: + lodash.toarray: 4.4.0 + dev: false + /once/1.4.0: resolution: {integrity: sha1-WDsap3WWHUsROsF9nFC6753Xa9E=} dependencies: @@ -663,6 +683,18 @@ packages: engines: {node: '>=8.6'} dev: true + /pinia/2.0.0-beta.5_typescript@4.3.5: + resolution: {integrity: sha512-0XvufXNkEvl7Fk6wrg5DH/JYPihkoknet950SQNIlWxXpeI7omwR0H00QPIiEYkrdbsiHXJyvI2XndWGvD4v5A==} + peerDependencies: + typescript: ^4.3.5 + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@vue/devtools-api': 6.0.0-beta.15 + typescript: 4.3.5 + dev: false + /postcss-modules-extract-imports/3.0.0_postcss@8.3.5: resolution: {integrity: sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==} engines: {node: ^10 || ^12 || >= 14} diff --git a/web-new/src/components/repo/BuildItem.vue b/web-new/src/components/repo/BuildItem.vue index 8892cfccb4..59ba7c15b3 100644 --- a/web-new/src/components/repo/BuildItem.vue +++ b/web-new/src/components/repo/BuildItem.vue @@ -21,7 +21,7 @@
- {{ build.message }} + {{ convertEmojis(build.message) }}
@@ -55,6 +55,7 @@ import { computed, defineComponent, PropType, ref, toRef } from 'vue'; import { Build } from '~/lib/api/types'; import humanizeDuration from 'humanize-duration'; import TimeAgo from 'javascript-time-ago'; +import { convertEmojis } from '~/utils/emoji'; export default defineComponent({ name: 'BuildItem', @@ -97,7 +98,7 @@ export default defineComponent({ return humanizeDuration(build.value.finished_at - build.value.started_at); }); - return { since, duration }; + return { since, duration, convertEmojis }; }, }); diff --git a/web-new/src/utils/emoji.ts b/web-new/src/utils/emoji.ts new file mode 100644 index 0000000000..1eac595629 --- /dev/null +++ b/web-new/src/utils/emoji.ts @@ -0,0 +1,5 @@ +import { emojify } from 'node-emoji'; + +export function convertEmojis(input: string): string { + return emojify(input); +} From 9890611b37a902ef649ddfa1eccf0639ba04981c Mon Sep 17 00:00:00 2001 From: Anton Bracke Date: Wed, 21 Jul 2021 14:05:12 +0200 Subject: [PATCH 017/143] more changes --- web-new/package.json | 6 +- web-new/pnpm-lock.yaml | 54 +++++++ web-new/src/App.vue | 8 +- .../components/build-feed/BuildFeedItem.vue | 50 ++++++ .../build-feed/BuildFeedSidebar.vue | 41 +++++ .../src/components/build-feed/FeedSidebar.vue | 38 ----- web-new/src/components/layout/Breadcrumbs.vue | 2 +- .../components/layout/header/ActiveBuilds.vue | 6 +- web-new/src/components/repo/BuildItem.vue | 89 ++++------- web-new/src/components/repo/BuildLogs.vue | 52 +++++++ .../src/components/repo/BuildStatusIcon.vue | 59 ++++++++ web-new/src/compositions/useBuild.ts | 50 ++++++ web-new/src/compositions/useBuildFeed.ts | 78 ++++++---- web-new/src/lib/api/client.ts | 2 +- web-new/src/lib/api/index.ts | 6 +- web-new/src/lib/api/types/build.ts | 63 ++++++-- web-new/src/main.ts | 6 +- web-new/src/router.ts | 2 +- web-new/src/utils/duration.ts | 35 +++++ web-new/src/utils/timeAgo.ts | 7 + web-new/src/views/Login.vue | 8 +- web-new/src/views/RepoAdd.vue | 2 +- web-new/src/views/Repos.vue | 2 +- web-new/src/views/repo/Repo.vue | 17 ++- web-new/src/views/repo/RepoSettings.vue | 4 +- web-new/src/views/repo/RepoWrapper.vue | 8 +- web-new/src/views/repo/build/Build.vue | 143 ++++++++++++++---- web-new/vite.config.ts | 3 +- web-new/windi.config.ts | 17 ++- 29 files changed, 645 insertions(+), 213 deletions(-) create mode 100644 web-new/src/components/build-feed/BuildFeedItem.vue create mode 100644 web-new/src/components/build-feed/BuildFeedSidebar.vue delete mode 100644 web-new/src/components/build-feed/FeedSidebar.vue create mode 100644 web-new/src/components/repo/BuildLogs.vue create mode 100644 web-new/src/components/repo/BuildStatusIcon.vue create mode 100644 web-new/src/compositions/useBuild.ts create mode 100644 web-new/src/utils/duration.ts create mode 100644 web-new/src/utils/timeAgo.ts diff --git a/web-new/package.json b/web-new/package.json index ff678eff17..e80c5db1d8 100644 --- a/web-new/package.json +++ b/web-new/package.json @@ -10,6 +10,7 @@ "dependencies": { "@kyvg/vue3-notification": "^2.3.3", "@meforma/vue-toaster": "^1.2.2", + "ansi_up": "^5.0.1", "humanize-duration": "^3.27.0", "javascript-time-ago": "^2.3.8", "node-emoji": "^1.10.0", @@ -18,14 +19,17 @@ "vue-router": "4.0.10" }, "devDependencies": { - "@types/javascript-time-ago": "^2.0.3", + "@iconify/json": "^1.1.378", "@types/humanize-duration": "^3.25.1", + "@types/javascript-time-ago": "^2.0.3", "@types/node": "^16.3.3", "@types/node-emoji": "^1.8.1", "@vitejs/plugin-vue": "^1.2.5", "@vue/compiler-sfc": "^3.0.5", + "ansi-to-html": "^0.7.0", "typescript": "^4.3.2", "vite": "^2.4.2", + "vite-plugin-icons": "^0.6.5", "vite-plugin-windicss": "^1.2.4", "vue-tsc": "^0.0.24", "windicss": "3.1.0" diff --git a/web-new/pnpm-lock.yaml b/web-new/pnpm-lock.yaml index df4934d3e8..09c3be53af 100644 --- a/web-new/pnpm-lock.yaml +++ b/web-new/pnpm-lock.yaml @@ -1,6 +1,7 @@ lockfileVersion: 5.3 specifiers: + '@iconify/json': ^1.1.378 '@kyvg/vue3-notification': ^2.3.3 '@meforma/vue-toaster': ^1.2.2 '@types/humanize-duration': ^3.25.1 @@ -9,12 +10,15 @@ specifiers: '@types/node-emoji': ^1.8.1 '@vitejs/plugin-vue': ^1.2.5 '@vue/compiler-sfc': ^3.0.5 + ansi-to-html: ^0.7.0 + ansi_up: ^5.0.1 humanize-duration: ^3.27.0 javascript-time-ago: ^2.3.8 node-emoji: ^1.10.0 pinia: ^2.0.0-beta.5 typescript: ^4.3.2 vite: ^2.4.2 + vite-plugin-icons: ^0.6.5 vite-plugin-windicss: ^1.2.4 vue: ^3.0.5 vue-router: 4.0.10 @@ -24,6 +28,7 @@ specifiers: dependencies: '@kyvg/vue3-notification': 2.3.3_vue@3.1.5 '@meforma/vue-toaster': 1.2.2 + ansi_up: 5.0.1 humanize-duration: 3.27.0 javascript-time-ago: 2.3.8 node-emoji: 1.10.0 @@ -32,14 +37,17 @@ dependencies: vue-router: 4.0.10_vue@3.1.5 devDependencies: + '@iconify/json': 1.1.378 '@types/humanize-duration': 3.25.1 '@types/javascript-time-ago': 2.0.3 '@types/node': 16.3.3 '@types/node-emoji': 1.8.1 '@vitejs/plugin-vue': 1.2.5_@vue+compiler-sfc@3.1.5 '@vue/compiler-sfc': 3.1.5_vue@3.1.5 + ansi-to-html: 0.7.0 typescript: 4.3.5 vite: 2.4.2 + vite-plugin-icons: 0.6.5_1f67253aff4c3363bde9022f44aa38ca vite-plugin-windicss: 1.2.4_vite@2.4.2 vue-tsc: 0.0.24 windicss: 3.1.0 @@ -68,6 +76,14 @@ packages: '@babel/helper-validator-identifier': 7.14.5 to-fast-properties: 2.0.0 + /@iconify/json-tools/1.0.10: + resolution: {integrity: sha512-LFelJDOLZ6JHlmlAkgrvmcu4hpNPB91KYcr4f60D/exzU1eNOb4/KCVHIydGHIQFaOacIOD+Xy+B7P1z812cZg==} + dev: true + + /@iconify/json/1.1.378: + resolution: {integrity: sha512-d0N/zpmRXBHGd0yjRaZYxRvbmcStFvVQINRnu9BuixSruJE5nh7pYesOQJU2yDe8Q4EBzMEcosgE2cjt3MUUag==} + dev: true + /@kyvg/vue3-notification/2.3.3_vue@3.1.5: resolution: {integrity: sha512-NxFejSs7FlaUzsPQ4jJyrB+UCFfsBUqICcQH70wHId52Hc9G9i0rBp2Wj3UsNj21H/HitP3r92rfz4ly2ImyMA==} peerDependencies: @@ -243,6 +259,18 @@ packages: color-convert: 2.0.1 dev: true + /ansi-to-html/0.7.0: + resolution: {integrity: sha512-XYAN+0+3Bi+nmTQBq717bGToQVpzq2AoukAaDnQQvpmqIbedjsdSDuGD7+YLYOPBh7rjc7A6ko5cLLDp06QNjw==} + engines: {node: '>=8.0.0'} + hasBin: true + dependencies: + entities: 2.2.0 + dev: true + + /ansi_up/5.0.1: + resolution: {integrity: sha512-HGOTjFQECRKZM9fIlGhJfR2pcK8PMUWzFOqcPwqBEnNIa4P2r0Di+g2hxCX0hL0n1NUtAHGRA+fUyA/OajZYFw==} + dev: false + /atob/2.1.2: resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==} engines: {node: '>= 4.5.0'} @@ -399,6 +427,10 @@ packages: resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==} engines: {node: '>= 4'} + /entities/2.2.0: + resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} + dev: true + /esbuild/0.12.15: resolution: {integrity: sha512-72V4JNd2+48eOVCXx49xoSWHgC3/cCy96e7mbXKY+WOWghN00cCmlGnwVLRhRHorvv0dgCyuMYBZlM2xDM5OQw==} hasBin: true @@ -975,6 +1007,24 @@ packages: resolution: {integrity: sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=} dev: true + /vite-plugin-icons/0.6.5_1f67253aff4c3363bde9022f44aa38ca: + resolution: {integrity: sha512-lfePr8juO2ajp0571iLL+9zIoyBD9nSSSHlC4JYXbAeMOJB6WTP+Vdc2gze8yI8JRO+Z0TXCCvvL9bPgvkI4Cg==} + peerDependencies: + '@iconify/json': '*' + '@vue/compiler-sfc': ^3.0.2 + vue-template-compiler: ^2.6.12 + peerDependenciesMeta: + '@vue/compiler-sfc': + optional: true + vue-template-compiler: + optional: true + dependencies: + '@iconify/json': 1.1.378 + '@iconify/json-tools': 1.0.10 + '@vue/compiler-sfc': 3.1.5_vue@3.1.5 + vue-template-es2015-compiler: 1.9.1 + dev: true + /vite-plugin-windicss/1.2.4_vite@2.4.2: resolution: {integrity: sha512-U+mW8AiPRgC5wbUqjtvEIbZR3LzOwhNU0wnYQueT2SjjTfjlP74vcQg37yrULxycKibpdTYVHZuDuW4QkglPng==} peerDependencies: @@ -1011,6 +1061,10 @@ packages: vue: 3.1.5 dev: false + /vue-template-es2015-compiler/1.9.1: + resolution: {integrity: sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==} + dev: true + /vue-tsc/0.0.24: resolution: {integrity: sha512-Qx0V7jkWMtvddtaWa1SA8YKkBCRmjq9zZUB2UIMZiso6JSH538oHD2VumSzkoDnAfFbY3t0/j1mB2abpA0bGWA==} hasBin: true diff --git a/web-new/src/App.vue b/web-new/src/App.vue index 7c630f6ca6..5fe618cf88 100644 --- a/web-new/src/App.vue +++ b/web-new/src/App.vue @@ -5,23 +5,23 @@
- +
diff --git a/web-new/src/components/build-feed/BuildFeedItem.vue b/web-new/src/components/build-feed/BuildFeedItem.vue new file mode 100644 index 0000000000..c5fbafa645 --- /dev/null +++ b/web-new/src/components/build-feed/BuildFeedItem.vue @@ -0,0 +1,50 @@ + + + diff --git a/web-new/src/components/build-feed/BuildFeedSidebar.vue b/web-new/src/components/build-feed/BuildFeedSidebar.vue new file mode 100644 index 0000000000..f52968dc6e --- /dev/null +++ b/web-new/src/components/build-feed/BuildFeedSidebar.vue @@ -0,0 +1,41 @@ + + + diff --git a/web-new/src/components/build-feed/FeedSidebar.vue b/web-new/src/components/build-feed/FeedSidebar.vue deleted file mode 100644 index 904b328c57..0000000000 --- a/web-new/src/components/build-feed/FeedSidebar.vue +++ /dev/null @@ -1,38 +0,0 @@ - - - diff --git a/web-new/src/components/layout/Breadcrumbs.vue b/web-new/src/components/layout/Breadcrumbs.vue index 8f502e685f..f0d462994f 100644 --- a/web-new/src/components/layout/Breadcrumbs.vue +++ b/web-new/src/components/layout/Breadcrumbs.vue @@ -2,7 +2,7 @@
diff --git a/web-new/src/components/layout/header/ActiveBuilds.vue b/web-new/src/components/layout/header/ActiveBuilds.vue index e678685e31..7a6158f534 100644 --- a/web-new/src/components/layout/header/ActiveBuilds.vue +++ b/web-new/src/components/layout/header/ActiveBuilds.vue @@ -27,7 +27,7 @@ diff --git a/web-new/src/components/repo/BuildItem.vue b/web-new/src/components/repo/BuildItem.vue index 59ba7c15b3..8ce476f076 100644 --- a/web-new/src/components/repo/BuildItem.vue +++ b/web-new/src/components/repo/BuildItem.vue @@ -1,19 +1,14 @@ diff --git a/web-new/src/components/repo/BuildLogs.vue b/web-new/src/components/repo/BuildLogs.vue new file mode 100644 index 0000000000..48f508bf3a --- /dev/null +++ b/web-new/src/components/repo/BuildLogs.vue @@ -0,0 +1,52 @@ + + + diff --git a/web-new/src/components/repo/BuildStatusIcon.vue b/web-new/src/components/repo/BuildStatusIcon.vue new file mode 100644 index 0000000000..22bf7b3668 --- /dev/null +++ b/web-new/src/components/repo/BuildStatusIcon.vue @@ -0,0 +1,59 @@ + + + diff --git a/web-new/src/compositions/useBuild.ts b/web-new/src/compositions/useBuild.ts new file mode 100644 index 0000000000..1f4b629182 --- /dev/null +++ b/web-new/src/compositions/useBuild.ts @@ -0,0 +1,50 @@ +import { computed, Ref } from 'vue'; +import { Build } from '~/lib/api/types'; +import timeAgo from '~/utils/timeAgo'; +import { convertEmojis } from '~/utils/emoji'; +import { prettyDuration } from '~/utils/duration'; + +export default (build: Ref) => { + const since = computed(() => { + if (!build.value) { + return null; + } + + const start = build.value.started_at || 0; + + if (start === 0) { + return 'not started yet'; + } + + return timeAgo.format(start * 1000); + }); + + const duration = computed(() => { + if (!build.value) { + return null; + } + + const start = build.value.started_at || 0; + const end = build.value.finished_at || 0; + + if (start === 0 && end == 0) { + return 'not started yet'; + } + + if (end === 0) { + return prettyDuration(Date.now() - start * 1000); + } + + return prettyDuration((end - start) * 1000); + }); + + const message = computed(() => { + if (!build.value) { + return ''; + } + + return convertEmojis(build.value.message); + }); + + return { since, duration, message }; +}; diff --git a/web-new/src/compositions/useBuildFeed.ts b/web-new/src/compositions/useBuildFeed.ts index 32e9cb28ff..011d90a419 100644 --- a/web-new/src/compositions/useBuildFeed.ts +++ b/web-new/src/compositions/useBuildFeed.ts @@ -1,17 +1,9 @@ -import { computed, ref } from 'vue'; +import { defineStore } from 'pinia'; +import { computed } from 'vue'; import useApiClient from '~/compositions/useApiClient'; import { Build } from '~/lib/api/types'; -import useUserConfig from './useUserConfig'; +import useUserConfig from '~/compositions/useUserConfig'; -let initilaized = false; -const builds = ref(); -const activeBuilds = computed(() => { - if (!builds.value) { - return undefined; - } - - return builds.value.filter((build) => ['pending', 'running', 'started'].includes(build.status)); -}); const { userConfig, setUserConfig } = useUserConfig(); const isBuildFeedOpen = computed(() => userConfig.value.isBuildFeedOpen); @@ -25,28 +17,48 @@ function compareFeedItem(a: Build, b: Build) { return (b.started_at || b.created_at || -1) - (a.started_at || a.created_at || -1); } -async function init() { - const apiClient = useApiClient(); +export default defineStore({ + id: 'builds', - const b = await apiClient.getBuildFeed(); - builds.value = b.sort(compareFeedItem); + state: () => ({ + loaded: false, + builds: [] as Build[], + }), - // listen to build-feed changes -} + getters: { + activeBuilds(state) { + if (!state.builds) { + return undefined; + } + + return state.builds.filter((build) => ['pending', 'running', 'started'].includes(build.status)); + }, + isBuildFeedOpen() { + return isBuildFeedOpen.value; + }, + }, + + actions: { + async loadBuilds() { + if (this.loaded) { + return; + } -export default () => { - if (!initilaized) { - init(); - } - - function toggle() { - setUserConfig('isBuildFeedOpen', !userConfig.value.isBuildFeedOpen); - } - - return { - toggle, - builds, - activeBuilds, - isBuildFeedOpen, - }; -}; + this.loaded = true; + + const apiClient = useApiClient(); + + const b = await apiClient.getBuildFeed(); + this.builds = b.sort(compareFeedItem); + + // listen to build-feed changes + apiClient.on((data: any) => { + console.log('on data', data); + const { repo, build } = data; + }); + }, + toggle() { + setUserConfig('isBuildFeedOpen', !userConfig.value.isBuildFeedOpen); + }, + }, +}); diff --git a/web-new/src/lib/api/client.ts b/web-new/src/lib/api/client.ts index 3fa57bff72..416d73a98f 100644 --- a/web-new/src/lib/api/client.ts +++ b/web-new/src/lib/api/client.ts @@ -92,7 +92,7 @@ export default class ApiClient { return this._request('DELETE', path, null); } - _subscribe(path: string, callback, opts) { + _subscribe(path: string, callback: (data: string) => void, opts = { reconnect: true }) { var query = encodeQueryString({ access_token: this.token, }); diff --git a/web-new/src/lib/api/index.ts b/web-new/src/lib/api/index.ts index 298b36d228..60349ff750 100644 --- a/web-new/src/lib/api/index.ts +++ b/web-new/src/lib/api/index.ts @@ -37,7 +37,7 @@ export default class WoodpeckerClient extends ApiClient { return this._get('/api/repos/' + owner + '/' + repo + '/builds?' + query); } - getBuild(owner: string, repo: string, number: number) { + getBuild(owner: string, repo: string, number: string | 'latest') { return this._get('/api/repos/' + owner + '/' + repo + '/builds/' + number); } @@ -107,13 +107,13 @@ export default class WoodpeckerClient extends ApiClient { return this._post('/api/user/token'); } - on(callback) { + on(callback: (data: string) => void) { return this._subscribe('/stream/events', callback, { reconnect: true, }); } - stream(owner: string, repo: string, build: string, proc: string, callback) { + stream(owner: string, repo: string, build: string, proc: string, callback: (data: string) => void) { return this._subscribe('/stream/logs/' + owner + '/' + repo + '/' + build + '/' + proc, callback, { reconnect: false, }); diff --git a/web-new/src/lib/api/types/build.ts b/web-new/src/lib/api/types/build.ts index 798c1662a4..cbd7e1baed 100644 --- a/web-new/src/lib/api/types/build.ts +++ b/web-new/src/lib/api/types/build.ts @@ -6,9 +6,15 @@ export type Build = { // This number is specified within the context of the repository the build belongs to and is unique within that. number: number; + parent: number; + + event: 'push' | 'tag'; + // The current status of the build. status: BuildStatus; + error: string; + // When the build request was received. created_at: number; @@ -21,46 +27,60 @@ export type Build = { // When the build was finished. finished_at: number; - // description: Where the deployment should go. + // Where the deployment should go. deploy_to: string; - // description: The commit for the build. + // The commit for the build. commit: string; - // description: The branch the commit was pushed to. + // The branch the commit was pushed to. branch: string; - // description: The commit message. + // The commit message. message: string; - // description: When the commit was created. + // When the commit was created. timestamp: number; - // description: The alias for the commit. + // The alias for the commit. ref: string; - // description: The mapping from the local repository to a branch in the remote. + // The mapping from the local repository to a branch in the remote. refspec: string; - // description: The remote repository. + // The remote repository. remote: string; - // description: The login for the author of the commit. + title: string; + + sender: string; + + // The login for the author of the commit. author: string; - // description: The avatar for the author of the commit. + // The avatar for the author of the commit. author_avatar: string; - // description: The email for the author of the commit. + // email for the author of the commit. author_email: string; - // The link to view the repository. - // This link will point to the repository state associated with the build's commit. + // The link to view the repository. + // This link will point to the repository state associated with the build's commit. link_url: string; + signed: boolean; + + verified: boolean; + + reviewed_by: string; + + reviewed_at: number; + // The jobs associated with this build. // A build will have multiple jobs if a matrix build was used or if a rebuild was requested. - jobs: Job[]; + procs: BuildJob[]; + + changed_files: string[]; }; export type BuildStatus = @@ -75,4 +95,17 @@ export type BuildStatus = | 'started' | 'success'; -export type Job = {}; +export type BuildJob = { + id: number; + build_id: number; + pid: number; + ppid: number; + pgid: number; + name: string; + state: BuildStatus; + exit_code: number; + start_time: number; + end_time: number; + machine: string; + children?: BuildJob[]; +}; diff --git a/web-new/src/main.ts b/web-new/src/main.ts index 4ca07794a3..319f8bcfdb 100644 --- a/web-new/src/main.ts +++ b/web-new/src/main.ts @@ -1,17 +1,15 @@ import 'windi.css'; import { createApp } from 'vue'; +import { createPinia } from 'pinia'; import App from '~/App.vue'; import router from '~/router'; import { notifications } from '~/compositions/useNotifications'; -import TimeAgo from 'javascript-time-ago'; -import en from 'javascript-time-ago/locale/en'; -TimeAgo.addDefaultLocale(en); - const app = createApp(App); app.use(router); app.use(notifications); +app.use(createPinia()); app.mount('#app'); diff --git a/web-new/src/router.ts b/web-new/src/router.ts index f13bb7170a..5d67165811 100644 --- a/web-new/src/router.ts +++ b/web-new/src/router.ts @@ -21,7 +21,7 @@ const routes: RouteRecordRaw[] = [ meta: { authentication: 'required' }, }, { - path: '/repo/:repoOwner/:repoId', + path: '/repo/:repoOwner/:repoName', name: 'repo-wrapper', component: (): Component => import('~/views/repo/RepoWrapper.vue'), props: true, diff --git a/web-new/src/utils/duration.ts b/web-new/src/utils/duration.ts new file mode 100644 index 0000000000..c203ffe725 --- /dev/null +++ b/web-new/src/utils/duration.ts @@ -0,0 +1,35 @@ +import humanizeDuration from 'humanize-duration'; + +const en_short = { + h: (count?: number) => 'h', + m: (count?: number) => 'min', + s: (count?: number) => 'sec', +}; +const durationOptions: humanizeDuration.HumanizerOptions = { + round: true, + languages: { en_short }, + language: 'en_short', +}; + +export function prettyDuration(durationMs: number) { + return humanizeDuration(durationMs, durationOptions); +} + +function leadingZeros(n: number, length: number): string { + let res = n.toString(); + while (res.length < length) res = '0' + res; + return res; +} + +export function durationAsNumber(durationMs: number): string { + const durationSeconds = durationMs / 1000; + const seconds = leadingZeros(Math.floor(durationSeconds % 60), 2); + const minutes = leadingZeros(Math.floor(durationSeconds / 60) % 60, 2); + const hours = Math.floor(durationSeconds / 3600); + + if (hours !== 0) { + return `${hours}:${minutes}:${seconds}`; + } + + return `${minutes}:${seconds}`; +} diff --git a/web-new/src/utils/timeAgo.ts b/web-new/src/utils/timeAgo.ts new file mode 100644 index 0000000000..437114dcee --- /dev/null +++ b/web-new/src/utils/timeAgo.ts @@ -0,0 +1,7 @@ +import TimeAgo from 'javascript-time-ago'; +import en from 'javascript-time-ago/locale/en'; +TimeAgo.addDefaultLocale(en); + +const timeAgo = new TimeAgo('en-US'); + +export default timeAgo; diff --git a/web-new/src/views/Login.vue b/web-new/src/views/Login.vue index c5b72a8fd2..f52b40911b 100644 --- a/web-new/src/views/Login.vue +++ b/web-new/src/views/Login.vue @@ -7,7 +7,7 @@ diff --git a/web-new/vite.config.ts b/web-new/vite.config.ts index c682152529..0a8b96ccbf 100644 --- a/web-new/vite.config.ts +++ b/web-new/vite.config.ts @@ -2,10 +2,11 @@ import { defineConfig } from 'vite'; import path from 'path'; import vue from '@vitejs/plugin-vue'; import WindiCSS from 'vite-plugin-windicss'; +import Icons from 'vite-plugin-icons'; // https://vitejs.dev/config/ export default defineConfig({ - plugins: [vue(), WindiCSS()], + plugins: [vue(), WindiCSS(), Icons()], resolve: { alias: { '~/': `${path.resolve(__dirname, 'src')}/`, diff --git a/web-new/windi.config.ts b/web-new/windi.config.ts index 80829811ac..a1c671574d 100644 --- a/web-new/windi.config.ts +++ b/web-new/windi.config.ts @@ -1,6 +1,6 @@ -import colors from 'windicss/colors'; import { defineConfig } from 'windicss/helpers'; import typography from 'windicss/plugin/typography'; +import colors from 'windicss/colors'; export default defineConfig({ darkMode: 'class', @@ -8,7 +8,22 @@ export default defineConfig({ extend: { colors: { green: '#4caf50', + link: colors.blue[400], + + // status colors + 'status-blocked': colors.red[400], + 'status-declined': colors.red[400], + 'status-error': colors.red[400], + 'status-failure': colors.red[400], + 'status-killed': colors.red[400], + 'status-pending': colors.gray[400], + 'status-running': colors.blue[400], + 'status-skipped': colors.gray[400], + 'status-started': colors.blue[400], + 'status-success': '#4caf50', }, + stroke: (theme) => theme('colors'), + fill: (theme) => theme('colors'), }, }, plugins: [typography], From 57cb83bf8c20b29323632bc29a948dd6acf72d93 Mon Sep 17 00:00:00 2001 From: Anton Bracke Date: Fri, 23 Jul 2021 21:52:34 +0200 Subject: [PATCH 018/143] further improvements --- web-new/src/components/repo/BuildItem.vue | 10 ++- web-new/src/components/repo/BuildLogs.vue | 60 ++++++++++----- .../src/components/repo/BuildStatusIcon.vue | 26 +++---- web-new/src/compositions/useBuildFeed.ts | 52 ++----------- web-new/src/compositions/useBuildProc.ts | 77 +++++++++++++++++++ web-new/src/compositions/useEvents.ts | 39 ++++++++++ web-new/src/compositions/useRepo.ts | 13 ++++ web-new/src/lib/api/client.ts | 2 +- web-new/src/lib/api/index.ts | 17 ++-- web-new/src/lib/api/types/build.ts | 13 +++- web-new/src/main.ts | 3 + web-new/src/router.ts | 2 +- web-new/src/store/builds.ts | 57 ++++++++++++++ web-new/src/store/repos.ts | 28 +++++++ web-new/src/views/repo/build/Build.vue | 41 ++++++---- web-new/windi.config.ts | 14 +--- 16 files changed, 334 insertions(+), 120 deletions(-) create mode 100644 web-new/src/compositions/useBuildProc.ts create mode 100644 web-new/src/compositions/useEvents.ts create mode 100644 web-new/src/compositions/useRepo.ts create mode 100644 web-new/src/store/builds.ts create mode 100644 web-new/src/store/repos.ts diff --git a/web-new/src/components/repo/BuildItem.vue b/web-new/src/components/repo/BuildItem.vue index 8ce476f076..9b96b9cf3c 100644 --- a/web-new/src/components/repo/BuildItem.vue +++ b/web-new/src/components/repo/BuildItem.vue @@ -1,7 +1,15 @@ diff --git a/web-new/src/components/repo/BuildStatusIcon.vue b/web-new/src/components/repo/BuildStatusIcon.vue index 22bf7b3668..6d12c1921f 100644 --- a/web-new/src/components/repo/BuildStatusIcon.vue +++ b/web-new/src/components/repo/BuildStatusIcon.vue @@ -1,21 +1,15 @@ diff --git a/web-new/src/compositions/useBuildFeed.ts b/web-new/src/compositions/useBuildFeed.ts index 011d90a419..72d4a1be4d 100644 --- a/web-new/src/compositions/useBuildFeed.ts +++ b/web-new/src/compositions/useBuildFeed.ts @@ -1,6 +1,4 @@ -import { defineStore } from 'pinia'; import { computed } from 'vue'; -import useApiClient from '~/compositions/useApiClient'; import { Build } from '~/lib/api/types'; import useUserConfig from '~/compositions/useUserConfig'; @@ -17,48 +15,8 @@ function compareFeedItem(a: Build, b: Build) { return (b.started_at || b.created_at || -1) - (a.started_at || a.created_at || -1); } -export default defineStore({ - id: 'builds', - - state: () => ({ - loaded: false, - builds: [] as Build[], - }), - - getters: { - activeBuilds(state) { - if (!state.builds) { - return undefined; - } - - return state.builds.filter((build) => ['pending', 'running', 'started'].includes(build.status)); - }, - isBuildFeedOpen() { - return isBuildFeedOpen.value; - }, - }, - - actions: { - async loadBuilds() { - if (this.loaded) { - return; - } - - this.loaded = true; - - const apiClient = useApiClient(); - - const b = await apiClient.getBuildFeed(); - this.builds = b.sort(compareFeedItem); - - // listen to build-feed changes - apiClient.on((data: any) => { - console.log('on data', data); - const { repo, build } = data; - }); - }, - toggle() { - setUserConfig('isBuildFeedOpen', !userConfig.value.isBuildFeedOpen); - }, - }, -}); +export default () => { + function toggle() { + setUserConfig('isBuildFeedOpen', !userConfig.value.isBuildFeedOpen); + } +}; diff --git a/web-new/src/compositions/useBuildProc.ts b/web-new/src/compositions/useBuildProc.ts new file mode 100644 index 0000000000..773062dd37 --- /dev/null +++ b/web-new/src/compositions/useBuildProc.ts @@ -0,0 +1,77 @@ +import { computed, ref, Ref, watch } from 'vue'; +import { Build, BuildProc, BuildLog } from '~/lib/api/types'; +import useApiClient from './useApiClient'; + +const apiClient = useApiClient(); + +export function findProc(procs: BuildProc[], pid: number): BuildProc | null { + for (const proc of procs) { + if (proc.pid === pid) { + return proc; + } + if (proc.children) { + const result = findProc(proc.children, pid); + if (result) { + return result; + } + } + } + + return null; +} + +/** + * Returns true if the process is in a completed state. + * + * @param {Object} proc - The process object. + * @returns {boolean} + */ +export function isProcFinished(proc: BuildProc): boolean { + return proc.state !== 'running' && proc.state !== 'pending'; +} + +/** + * Returns true if the process is running. + * + * @param {Object} proc - The process object. + * @returns {boolean} + */ +export function isProcRunning(proc: BuildProc): boolean { + return proc.state === 'running'; +} + +export default () => { + const logs = ref(); + const proc = ref(); + let stream: EventSource | undefined; + + function onLogsUpdate(data: BuildLog) { + if (data.proc === proc.value?.name) { + logs.value = [...(logs.value || []), data]; + } + } + + async function load(owner: string, repo: string, build: number, _proc: BuildProc) { + unload(); + + proc.value = _proc; + + try { + logs.value = await apiClient.getLogs(owner, repo, build, _proc.pid); + } catch (err) { + logs.value = []; + } + + if (isProcRunning(_proc)) { + stream = apiClient.streamLogs(owner, repo, build, _proc.ppid, onLogsUpdate); + } + } + + function unload() { + if (stream) { + stream.close(); + } + } + + return { logs, load, unload }; +}; diff --git a/web-new/src/compositions/useEvents.ts b/web-new/src/compositions/useEvents.ts new file mode 100644 index 0000000000..408c7558c5 --- /dev/null +++ b/web-new/src/compositions/useEvents.ts @@ -0,0 +1,39 @@ +import useApiClient from './useApiClient'; +import RepoStore from '~/store/repos'; +import BuildStore from '~/store/builds'; + +const apiClient = useApiClient(); +let initialized = false; + +export default () => { + if (initialized) { + return; + } + const repoStore = RepoStore(); + const buildStore = BuildStore(); + + initialized = true; + + apiClient.on((data) => { + // contains repo update + if (!data.repo) { + return; + } + const { repo } = data; + repoStore.setRepo(repo); + + // contains build update + if (!data.build) { + return; + } + const { build } = data; + buildStore.setBuild(repo.owner, repo.name, build); + + // contains proc update + if (!data.proc) { + return; + } + const { proc } = data; + buildStore.setProc(repo.owner, repo.name, build.number, proc); + }); +}; diff --git a/web-new/src/compositions/useRepo.ts b/web-new/src/compositions/useRepo.ts new file mode 100644 index 0000000000..71ffcca92a --- /dev/null +++ b/web-new/src/compositions/useRepo.ts @@ -0,0 +1,13 @@ +import { Repo } from '~/lib/api/types'; + +export function repoSlug(ownerOrRepo: string | Repo, name?: string): string { + if (typeof ownerOrRepo === 'string') { + if (!name) { + throw new Error('Please provide a name as well'); + } + + return `${ownerOrRepo}/${name}`; + } + + return `${ownerOrRepo.owner}/${ownerOrRepo.name}`; +} diff --git a/web-new/src/lib/api/client.ts b/web-new/src/lib/api/client.ts index 416d73a98f..40e13cdce5 100644 --- a/web-new/src/lib/api/client.ts +++ b/web-new/src/lib/api/client.ts @@ -92,7 +92,7 @@ export default class ApiClient { return this._request('DELETE', path, null); } - _subscribe(path: string, callback: (data: string) => void, opts = { reconnect: true }) { + _subscribe(path: string, callback: (data: any) => void, opts = { reconnect: true }) { var query = encodeQueryString({ access_token: this.token, }); diff --git a/web-new/src/lib/api/index.ts b/web-new/src/lib/api/index.ts index 60349ff750..a44ee7f8b9 100644 --- a/web-new/src/lib/api/index.ts +++ b/web-new/src/lib/api/index.ts @@ -1,11 +1,10 @@ import ApiClient, { encodeQueryString } from './client'; -import { Repo } from './types'; +import { Build, BuildProc, BuildLog, Repo } from './types'; type RepoListOptions = { all?: boolean; flush?: boolean; }; - export default class WoodpeckerClient extends ApiClient { constructor(server: string, token: string, csrf: string) { super(server, token, csrf); @@ -63,7 +62,7 @@ export default class WoodpeckerClient extends ApiClient { return this._post('/api/repos/' + owner + '/' + repo + '/builds/' + build + '?' + query); } - getLogs(owner: string, repo: string, build: string, proc: string) { + getLogs(owner: string, repo: string, build: number, proc: number): Promise { return this._get('/api/repos/' + owner + '/' + repo + '/logs/' + build + '/' + proc); } @@ -107,15 +106,21 @@ export default class WoodpeckerClient extends ApiClient { return this._post('/api/user/token'); } - on(callback: (data: string) => void) { + on(callback: (data: { build?: Build; repo?: Repo; proc?: BuildProc }) => void) { return this._subscribe('/stream/events', callback, { reconnect: true, }); } - stream(owner: string, repo: string, build: string, proc: string, callback: (data: string) => void) { + streamLogs( + owner: string, + repo: string, + build: number, + proc: number, + callback: (data: BuildLog) => void, + ): EventSource { return this._subscribe('/stream/logs/' + owner + '/' + repo + '/' + build + '/' + proc, callback, { - reconnect: false, + reconnect: true, }); } } diff --git a/web-new/src/lib/api/types/build.ts b/web-new/src/lib/api/types/build.ts index cbd7e1baed..126bfb2a5f 100644 --- a/web-new/src/lib/api/types/build.ts +++ b/web-new/src/lib/api/types/build.ts @@ -78,7 +78,7 @@ export type Build = { // The jobs associated with this build. // A build will have multiple jobs if a matrix build was used or if a rebuild was requested. - procs: BuildJob[]; + procs: BuildProc[]; changed_files: string[]; }; @@ -95,7 +95,7 @@ export type BuildStatus = | 'started' | 'success'; -export type BuildJob = { +export type BuildProc = { id: number; build_id: number; pid: number; @@ -107,5 +107,12 @@ export type BuildJob = { start_time: number; end_time: number; machine: string; - children?: BuildJob[]; + children?: BuildProc[]; +}; + +export type BuildLog = { + proc: string; + pos: number; + out: string; + time?: number; }; diff --git a/web-new/src/main.ts b/web-new/src/main.ts index 319f8bcfdb..0812f4912f 100644 --- a/web-new/src/main.ts +++ b/web-new/src/main.ts @@ -6,6 +6,7 @@ import { createPinia } from 'pinia'; import App from '~/App.vue'; import router from '~/router'; import { notifications } from '~/compositions/useNotifications'; +import useEvents from '~/compositions/useEvents'; const app = createApp(App); @@ -13,3 +14,5 @@ app.use(router); app.use(notifications); app.use(createPinia()); app.mount('#app'); + +useEvents(); diff --git a/web-new/src/router.ts b/web-new/src/router.ts index 5d67165811..c99ab7c2dc 100644 --- a/web-new/src/router.ts +++ b/web-new/src/router.ts @@ -34,7 +34,7 @@ const routes: RouteRecordRaw[] = [ props: true, }, { - path: 'build/:buildId', + path: 'build/:buildId/:procId?', name: 'repo-build', component: (): Component => import('~/views/repo/build/Build.vue'), meta: { authentication: 'required' }, diff --git a/web-new/src/store/builds.ts b/web-new/src/store/builds.ts new file mode 100644 index 0000000000..904b21bda9 --- /dev/null +++ b/web-new/src/store/builds.ts @@ -0,0 +1,57 @@ +import { defineStore } from 'pinia'; +import useApiClient from '~/compositions/useApiClient'; +import { repoSlug } from '~/compositions/useRepo'; +import { Build, BuildProc } from '~/lib/api/types'; + +const apiClient = useApiClient(); + +/** + * Compare two feed items by name. + * @param {Object} a - A feed item. + * @param {Object} b - A feed item. + * @returns {number} + */ +function compareFeedItem(a: Build, b: Build) { + return (b.started_at || b.created_at || -1) - (a.started_at || a.created_at || -1); +} + +export default defineStore({ + id: 'builds', + + state: () => ({ + builds: {} as Record>, + }), + + getters: { + getActiveBuilds: (state) => (owner: string, repo: string) => { + return Object.values(state.builds[repoSlug(owner, repo)]).filter((build) => + ['pending', 'running', 'started'].includes(build.status), + ); + }, + getBuild: (state) => (owner: string, repo: string, build: number) => state.builds[repoSlug(owner, repo)][build], + }, + + actions: { + async setBuild(owner: string, repo: string, build: Build) { + const _repoSlug = repoSlug(owner, repo); + if (!this.builds[_repoSlug]) { + this.builds[_repoSlug] = {}; + } + + this.builds[_repoSlug][build.number] = build; + }, + async setProc(owner: string, repo: string, build: number, proc: BuildProc) { + const _repoSlug = repoSlug(owner, repo); + if (!this.builds[_repoSlug] || !this.builds[_repoSlug][build]) { + throw new Error("Can't find build"); + } + + const procs = this.builds[_repoSlug][build].procs.filter((p) => p.pid !== proc.pid); + this.builds[_repoSlug][build].procs = [...procs, proc]; + }, + async loadBuilds(owner: string, repo: string) { + const b = await apiClient.getBuildList(owner, repo); + this.builds[repoSlug(owner, repo)] = b.sort(compareFeedItem); + }, + }, +}); diff --git a/web-new/src/store/repos.ts b/web-new/src/store/repos.ts new file mode 100644 index 0000000000..be092ecf9f --- /dev/null +++ b/web-new/src/store/repos.ts @@ -0,0 +1,28 @@ +import { defineStore } from 'pinia'; +import useApiClient from '~/compositions/useApiClient'; +import { repoSlug } from '~/compositions/useRepo'; +import { Repo } from '~/lib/api/types'; + +const apiClient = useApiClient(); + +export default defineStore({ + id: 'repos', + + state: () => ({ + repos: {} as Record, + }), + + getters: { + repo: (state) => (owner: string, name: string) => state.repos[repoSlug(owner, name)], + }, + + actions: { + setRepo(repo: Repo) { + this.repos[repoSlug(repo)] = repo; + }, + async loadRepo(owner: string, name: string) { + const repo = await apiClient.getRepo(owner, name); + this.repos[repoSlug(repo)] = repo; + }, + }, +}); diff --git a/web-new/src/views/repo/build/Build.vue b/web-new/src/views/repo/build/Build.vue index e2d2186aa2..5a8219e5b7 100644 --- a/web-new/src/views/repo/build/Build.vue +++ b/web-new/src/views/repo/build/Build.vue @@ -44,23 +44,25 @@ v-for="job in proc.children" :key="job.pid" class="flex p-2 pl-6 cursor-pointer items-center" - :class="{ 'bg-gray-800': selectedJob === job.pid }" - @click="selectedJob = job.pid" + :class="{ 'bg-gray-800': procId && parseInt(procId) === job.pid }" + @click="selectProc(job)" > -
-
+
+
-
+
{{ job.name }} - {{ jobDuration(job) }} + {{ + jobDuration(job) + }}
- +
@@ -69,7 +71,7 @@ diff --git a/web-new/windi.config.ts b/web-new/windi.config.ts index a1c671574d..8b59ecd3b8 100644 --- a/web-new/windi.config.ts +++ b/web-new/windi.config.ts @@ -11,16 +11,10 @@ export default defineConfig({ link: colors.blue[400], // status colors - 'status-blocked': colors.red[400], - 'status-declined': colors.red[400], - 'status-error': colors.red[400], - 'status-failure': colors.red[400], - 'status-killed': colors.red[400], - 'status-pending': colors.gray[400], - 'status-running': colors.blue[400], - 'status-skipped': colors.gray[400], - 'status-started': colors.blue[400], - 'status-success': '#4caf50', + 'status-red': colors.red[400], + 'status-gray': colors.gray[400], + 'status-blue': colors.blue[400], + 'status-green': '#4caf50', }, stroke: (theme) => theme('colors'), fill: (theme) => theme('colors'), From 4ff7b3f65638587fead72614b90e59e8bf512374 Mon Sep 17 00:00:00 2001 From: Anton Bracke Date: Thu, 29 Jul 2021 18:40:27 +0200 Subject: [PATCH 019/143] ui improvements --- web-new/package.json | 2 +- web-new/pecker.html | 158 ------------------ web-new/pnpm-lock.yaml | 10 +- web-new/src/App.vue | 2 +- .../build-feed/BuildFeedSidebar.vue | 12 +- web-new/src/components/layout/Breadcrumbs.vue | 2 +- .../components/layout/header/ActiveBuilds.vue | 17 +- web-new/src/components/repo/BuildLogs.vue | 10 +- web-new/src/components/repo/BuildProcs.vue | 77 +++++++++ web-new/src/compositions/useApiClient.ts | 8 +- web-new/src/compositions/useBuildFeed.ts | 29 ++-- web-new/src/compositions/useBuildProc.ts | 4 +- web-new/src/compositions/useNotifications.ts | 2 +- web-new/src/lib/api/client.ts | 8 +- web-new/src/lib/api/index.ts | 6 +- web-new/src/lib/api/types/build.ts | 4 +- web-new/src/store/builds.ts | 79 +++++++-- web-new/src/store/repos.ts | 22 ++- web-new/src/views/Repos.vue | 11 +- web-new/src/views/repo/Repo.vue | 44 ++--- web-new/src/views/repo/RepoSettings.vue | 7 +- web-new/src/views/repo/RepoWrapper.vue | 16 +- web-new/src/views/repo/build/Build.vue | 127 +++++++------- 23 files changed, 313 insertions(+), 344 deletions(-) delete mode 100644 web-new/pecker.html create mode 100644 web-new/src/components/repo/BuildProcs.vue diff --git a/web-new/package.json b/web-new/package.json index e80c5db1d8..8b8bee3605 100644 --- a/web-new/package.json +++ b/web-new/package.json @@ -14,7 +14,7 @@ "humanize-duration": "^3.27.0", "javascript-time-ago": "^2.3.8", "node-emoji": "^1.10.0", - "pinia": "^2.0.0-beta.5", + "pinia": "^2.0.0-rc.0", "vue": "^3.0.5", "vue-router": "4.0.10" }, diff --git a/web-new/pecker.html b/web-new/pecker.html deleted file mode 100644 index 05cce16911..0000000000 --- a/web-new/pecker.html +++ /dev/null @@ -1,158 +0,0 @@ - - - - - - Page Title - - - - -
- - - - - - - - - - - - -
- - diff --git a/web-new/pnpm-lock.yaml b/web-new/pnpm-lock.yaml index 09c3be53af..88a468c64c 100644 --- a/web-new/pnpm-lock.yaml +++ b/web-new/pnpm-lock.yaml @@ -15,7 +15,7 @@ specifiers: humanize-duration: ^3.27.0 javascript-time-ago: ^2.3.8 node-emoji: ^1.10.0 - pinia: ^2.0.0-beta.5 + pinia: ^2.0.0-rc.0 typescript: ^4.3.2 vite: ^2.4.2 vite-plugin-icons: ^0.6.5 @@ -32,7 +32,7 @@ dependencies: humanize-duration: 3.27.0 javascript-time-ago: 2.3.8 node-emoji: 1.10.0 - pinia: 2.0.0-beta.5_typescript@4.3.5 + pinia: 2.0.0-rc.0_typescript@4.3.5+vue@3.1.5 vue: 3.1.5 vue-router: 4.0.10_vue@3.1.5 @@ -715,16 +715,18 @@ packages: engines: {node: '>=8.6'} dev: true - /pinia/2.0.0-beta.5_typescript@4.3.5: - resolution: {integrity: sha512-0XvufXNkEvl7Fk6wrg5DH/JYPihkoknet950SQNIlWxXpeI7omwR0H00QPIiEYkrdbsiHXJyvI2XndWGvD4v5A==} + /pinia/2.0.0-rc.0_typescript@4.3.5+vue@3.1.5: + resolution: {integrity: sha512-AaUMv6GPZjc+BDph3r2taL7BbGrvouW6YDC66k5giTZ01Mz0bslq7xwOn+rlAYAH2JPhVxYWRBTXHd5Lp/wK+Q==} peerDependencies: typescript: ^4.3.5 + vue: ^3.2.0 || ^3.2.0-beta.4 peerDependenciesMeta: typescript: optional: true dependencies: '@vue/devtools-api': 6.0.0-beta.15 typescript: 4.3.5 + vue: 3.1.5 dev: false /postcss-modules-extract-imports/3.0.0_postcss@8.3.5: diff --git a/web-new/src/App.vue b/web-new/src/App.vue index 5fe618cf88..e1202daea5 100644 --- a/web-new/src/App.vue +++ b/web-new/src/App.vue @@ -2,7 +2,7 @@
-
+
diff --git a/web-new/src/components/build-feed/BuildFeedSidebar.vue b/web-new/src/components/build-feed/BuildFeedSidebar.vue index f52968dc6e..96e30fe23d 100644 --- a/web-new/src/components/build-feed/BuildFeedSidebar.vue +++ b/web-new/src/components/build-feed/BuildFeedSidebar.vue @@ -1,7 +1,7 @@ diff --git a/web-new/src/components/layout/header/ActiveBuilds.vue b/web-new/src/components/layout/header/ActiveBuilds.vue index 7a6158f534..141a5a49ba 100644 --- a/web-new/src/components/layout/header/ActiveBuilds.vue +++ b/web-new/src/components/layout/header/ActiveBuilds.vue @@ -1,6 +1,6 @@ diff --git a/web-new/src/components/repo/BuildLogs.vue b/web-new/src/components/repo/BuildLogs.vue index 080111846d..6587df66e1 100644 --- a/web-new/src/components/repo/BuildLogs.vue +++ b/web-new/src/components/repo/BuildLogs.vue @@ -2,17 +2,17 @@
{{ logLine.pos + 1 }}
-
+
{{ logLine.time || 0 }}s
-
+
exit code {{ proc.exit_code }}
diff --git a/web-new/src/compositions/useApiClient.ts b/web-new/src/compositions/useApiClient.ts index d53c1f8593..5aaf1fd184 100644 --- a/web-new/src/compositions/useApiClient.ts +++ b/web-new/src/compositions/useApiClient.ts @@ -7,12 +7,8 @@ export default (): WoodpeckerClient => { if (!apiClient) { const config = useConfig(); const server = 'http://localhost:8000'; - const token = ''; - const csrf = config.csrf; - - if (!csrf) { - throw new Error('CSRF unknown'); - } + const token = null; + const csrf = config.csrf || null; apiClient = new WoodpeckerClient(server, token, csrf); } diff --git a/web-new/src/compositions/useBuildFeed.ts b/web-new/src/compositions/useBuildFeed.ts index 72d4a1be4d..c8f8a51a79 100644 --- a/web-new/src/compositions/useBuildFeed.ts +++ b/web-new/src/compositions/useBuildFeed.ts @@ -1,22 +1,25 @@ -import { computed } from 'vue'; -import { Build } from '~/lib/api/types'; +import { computed, toRef } from 'vue'; +import BuildStore from '~/store/builds'; import useUserConfig from '~/compositions/useUserConfig'; const { userConfig, setUserConfig } = useUserConfig(); -const isBuildFeedOpen = computed(() => userConfig.value.isBuildFeedOpen); - -/** - * Compare two feed items by name. - * @param {Object} a - A feed item. - * @param {Object} b - A feed item. - * @returns {number} - */ -function compareFeedItem(a: Build, b: Build) { - return (b.started_at || b.created_at || -1) - (a.started_at || a.created_at || -1); -} +const isOpen = computed(() => userConfig.value.isBuildFeedOpen); export default () => { + const buildStore = BuildStore(); + function toggle() { setUserConfig('isBuildFeedOpen', !userConfig.value.isBuildFeedOpen); } + + const sortedBuilds = toRef(buildStore, 'sortedBuildFeed'); + const activeBuilds = toRef(buildStore, 'activeBuilds'); + + return { + toggle, + isOpen, + sortedBuilds, + activeBuilds, + load: buildStore.loadBuildFeed, + }; }; diff --git a/web-new/src/compositions/useBuildProc.ts b/web-new/src/compositions/useBuildProc.ts index 773062dd37..ad5ae56a38 100644 --- a/web-new/src/compositions/useBuildProc.ts +++ b/web-new/src/compositions/useBuildProc.ts @@ -1,5 +1,5 @@ -import { computed, ref, Ref, watch } from 'vue'; -import { Build, BuildProc, BuildLog } from '~/lib/api/types'; +import { ref } from 'vue'; +import { BuildProc, BuildLog } from '~/lib/api/types'; import useApiClient from './useApiClient'; const apiClient = useApiClient(); diff --git a/web-new/src/compositions/useNotifications.ts b/web-new/src/compositions/useNotifications.ts index 8d386420b9..9b8edefeb8 100644 --- a/web-new/src/compositions/useNotifications.ts +++ b/web-new/src/compositions/useNotifications.ts @@ -1,4 +1,4 @@ -import Notifications, { notify, NotificationsOptions } from '@kyvg/vue3-notification'; +import Notifications, { notify } from '@kyvg/vue3-notification'; export const notifications = Notifications; diff --git a/web-new/src/lib/api/client.ts b/web-new/src/lib/api/client.ts index 40e13cdce5..0af9e17c64 100644 --- a/web-new/src/lib/api/client.ts +++ b/web-new/src/lib/api/client.ts @@ -17,12 +17,12 @@ export function encodeQueryString(params: Record void; - constructor(server: string, token: string, csrf: string) { - this.server = server || ''; + constructor(server: string, token: string | null, csrf: string | null) { + this.server = server; this.token = token; this.csrf = csrf; } diff --git a/web-new/src/lib/api/index.ts b/web-new/src/lib/api/index.ts index a44ee7f8b9..c09a55b69e 100644 --- a/web-new/src/lib/api/index.ts +++ b/web-new/src/lib/api/index.ts @@ -6,7 +6,7 @@ type RepoListOptions = { flush?: boolean; }; export default class WoodpeckerClient extends ApiClient { - constructor(server: string, token: string, csrf: string) { + constructor(server: string, token: string | null, csrf: string | null) { super(server, token, csrf); } @@ -31,7 +31,7 @@ export default class WoodpeckerClient extends ApiClient { return this._delete('/api/repos/' + owner + '/' + repo); } - getBuildList(owner: string, repo: string, opts?: Record) { + getBuildList(owner: string, repo: string, opts?: Record): Promise { var query = encodeQueryString(opts); return this._get('/api/repos/' + owner + '/' + repo + '/builds?' + query); } @@ -40,7 +40,7 @@ export default class WoodpeckerClient extends ApiClient { return this._get('/api/repos/' + owner + '/' + repo + '/builds/' + number); } - getBuildFeed(opts?: Record) { + getBuildFeed(opts?: Record): Promise { var query = encodeQueryString(opts); return this._get('/api/user/feed?' + query); } diff --git a/web-new/src/lib/api/types/build.ts b/web-new/src/lib/api/types/build.ts index 126bfb2a5f..6f5ed7a8ed 100644 --- a/web-new/src/lib/api/types/build.ts +++ b/web-new/src/lib/api/types/build.ts @@ -78,9 +78,9 @@ export type Build = { // The jobs associated with this build. // A build will have multiple jobs if a matrix build was used or if a rebuild was requested. - procs: BuildProc[]; + procs?: BuildProc[]; - changed_files: string[]; + changed_files?: string[]; }; export type BuildStatus = diff --git a/web-new/src/store/builds.ts b/web-new/src/store/builds.ts index 904b21bda9..fcea79abd5 100644 --- a/web-new/src/store/builds.ts +++ b/web-new/src/store/builds.ts @@ -1,3 +1,4 @@ +import { computed, toRef, Ref, ref } from 'vue'; import { defineStore } from 'pinia'; import useApiClient from '~/compositions/useApiClient'; import { repoSlug } from '~/compositions/useRepo'; @@ -15,43 +16,93 @@ function compareFeedItem(a: Build, b: Build) { return (b.started_at || b.created_at || -1) - (a.started_at || a.created_at || -1); } +function isBuildActive(build: Build) { + return ['pending', 'running', 'started'].includes(build.status); +} + export default defineStore({ id: 'builds', state: () => ({ builds: {} as Record>, + buildFeed: [] as Build[], }), getters: { - getActiveBuilds: (state) => (owner: string, repo: string) => { - return Object.values(state.builds[repoSlug(owner, repo)]).filter((build) => - ['pending', 'running', 'started'].includes(build.status), - ); + sortedBuildFeed(state) { + return state.buildFeed.sort(compareFeedItem); + }, + activeBuilds(state) { + return state.buildFeed.filter(isBuildActive); }, - getBuild: (state) => (owner: string, repo: string, build: number) => state.builds[repoSlug(owner, repo)][build], }, actions: { - async setBuild(owner: string, repo: string, build: Build) { + // setters + setBuild(owner: string, repo: string, build: Build) { const _repoSlug = repoSlug(owner, repo); if (!this.builds[_repoSlug]) { this.builds[_repoSlug] = {}; } - this.builds[_repoSlug][build.number] = build; + // const repoBuilds = [...this.builds[_repoSlug].filter((b) => b.id !== build.id), build]; + const repoBuilds = this.builds[_repoSlug]; + repoBuilds[build.number] = build; + + this.builds = { + ...this.builds, + [_repoSlug]: repoBuilds, + }; }, - async setProc(owner: string, repo: string, build: number, proc: BuildProc) { - const _repoSlug = repoSlug(owner, repo); - if (!this.builds[_repoSlug] || !this.builds[_repoSlug][build]) { + setProc(owner: string, repo: string, buildNumber: number, proc: BuildProc) { + const build = this.getBuild(ref(owner), ref(repo), ref(buildNumber.toString())).value; + if (!build) { throw new Error("Can't find build"); } - const procs = this.builds[_repoSlug][build].procs.filter((p) => p.pid !== proc.pid); - this.builds[_repoSlug][build].procs = [...procs, proc]; + if (!build.procs) { + build.procs = []; + } + + build.procs = [...build.procs.filter((p) => p.pid !== proc.pid), proc]; + this.setBuild(owner, repo, build); }, + + // getters + getBuilds(owner: Ref, repo: Ref) { + return computed(() => { + const slug = repoSlug(owner.value, repo.value); + return toRef(this.builds, slug).value; + }); + }, + getSortedBuilds(owner: Ref, repo: Ref) { + return computed(() => Object.values(this.getBuilds(owner, repo).value || []).sort(compareFeedItem)); + }, + getActiveBuilds(owner: Ref, repo: Ref) { + const builds = this.getBuilds(owner, repo); + return computed(() => Object.values(builds.value).filter(isBuildActive)); + }, + getBuild(owner: Ref, repo: Ref, buildNumber: Ref) { + const builds = this.getBuilds(owner, repo); + return computed(() => { + return (builds.value || {})[parseInt(buildNumber.value)]; + }); + }, + + // loading async loadBuilds(owner: string, repo: string) { - const b = await apiClient.getBuildList(owner, repo); - this.builds[repoSlug(owner, repo)] = b.sort(compareFeedItem); + const builds = await apiClient.getBuildList(owner, repo); + builds.forEach((build) => { + this.setBuild(owner, repo, build); + }); + }, + async loadBuild(owner: string, repo: string, buildNumber: number) { + const build = await apiClient.getBuild(owner, repo, buildNumber.toString()); + this.setBuild(owner, repo, build); + }, + async loadBuildFeed() { + const builds = await apiClient.getBuildFeed(); + this.buildFeed = builds; }, }, }); diff --git a/web-new/src/store/repos.ts b/web-new/src/store/repos.ts index be092ecf9f..c35687f260 100644 --- a/web-new/src/store/repos.ts +++ b/web-new/src/store/repos.ts @@ -1,3 +1,4 @@ +import { toRef, Ref, computed } from 'vue'; import { defineStore } from 'pinia'; import useApiClient from '~/compositions/useApiClient'; import { repoSlug } from '~/compositions/useRepo'; @@ -12,17 +13,30 @@ export default defineStore({ repos: {} as Record, }), - getters: { - repo: (state) => (owner: string, name: string) => state.repos[repoSlug(owner, name)], - }, - actions: { + // getter + getRepo(owner: Ref, name: Ref) { + return computed(() => { + const slug = repoSlug(owner.value, name.value); + return toRef(this.repos, slug).value; + }); + }, + + // setter setRepo(repo: Repo) { this.repos[repoSlug(repo)] = repo; }, + + // loading async loadRepo(owner: string, name: string) { const repo = await apiClient.getRepo(owner, name); this.repos[repoSlug(repo)] = repo; }, + async loadRepos() { + const repos = await apiClient.getRepoList(); + repos.forEach((repo) => { + this.repos[repoSlug(repo.owner, repo.name)] = repo; + }); + }, }, }); diff --git a/web-new/src/views/Repos.vue b/web-new/src/views/Repos.vue index c54a5509a7..ba0ca6a534 100644 --- a/web-new/src/views/Repos.vue +++ b/web-new/src/views/Repos.vue @@ -14,11 +14,10 @@ From 89f62739b750467b3ca53a9fdb8e59a3e91c37f8 Mon Sep 17 00:00:00 2001 From: Anton Bracke Date: Fri, 30 Jul 2021 09:56:16 +0200 Subject: [PATCH 020/143] add user page and logout --- .../src/components/layout/header/Navbar.vue | 4 +- web-new/src/lib/api/index.ts | 2 +- web-new/src/router.ts | 6 ++ web-new/src/views/User.vue | 77 +++++++++++++++++++ 4 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 web-new/src/views/User.vue diff --git a/web-new/src/components/layout/header/Navbar.vue b/web-new/src/components/layout/header/Navbar.vue index 48c2ab0fca..58eeecd3f8 100644 --- a/web-new/src/components/layout/header/Navbar.vue +++ b/web-new/src/components/layout/header/Navbar.vue @@ -16,7 +16,9 @@ >
- + + +
diff --git a/web-new/src/lib/api/index.ts b/web-new/src/lib/api/index.ts index c09a55b69e..0add5405d2 100644 --- a/web-new/src/lib/api/index.ts +++ b/web-new/src/lib/api/index.ts @@ -102,7 +102,7 @@ export default class WoodpeckerClient extends ApiClient { return this._get('/api/user'); } - getToken() { + getToken(): Promise { return this._post('/api/user/token'); } diff --git a/web-new/src/router.ts b/web-new/src/router.ts index c99ab7c2dc..5bd8c82693 100644 --- a/web-new/src/router.ts +++ b/web-new/src/router.ts @@ -55,6 +55,12 @@ const routes: RouteRecordRaw[] = [ component: (): Component => import('~/views/Admin.vue'), props: true, }, + { + path: '/user', + name: 'user', + component: (): Component => import('~/views/User.vue'), + props: true, + }, { path: '/do-login/:origin?', name: 'login', diff --git a/web-new/src/views/User.vue b/web-new/src/views/User.vue new file mode 100644 index 0000000000..dc86ecf417 --- /dev/null +++ b/web-new/src/views/User.vue @@ -0,0 +1,77 @@ + + + + + From d552597b307e96523bec64c09ba49a49a741f8e8 Mon Sep 17 00:00:00 2001 From: Anton Bracke Date: Fri, 30 Jul 2021 10:36:41 +0200 Subject: [PATCH 021/143] support build cancel and restart and fix bug while loading logs --- web-new/src/components/repo/BuildLogs.vue | 12 +++-- web-new/src/compositions/useBuildProc.ts | 37 +-------------- web-new/src/store/builds.ts | 2 +- web-new/src/store/repos.ts | 2 +- web-new/src/utils/proc.ts | 37 +++++++++++++++ .../useRepo.ts => utils/repo.ts} | 0 web-new/src/views/repo/RepoSettings.vue | 2 +- web-new/src/views/repo/build/Build.vue | 45 ++++++++++++++++++- 8 files changed, 92 insertions(+), 45 deletions(-) create mode 100644 web-new/src/utils/proc.ts rename web-new/src/{compositions/useRepo.ts => utils/repo.ts} (100%) diff --git a/web-new/src/components/repo/BuildLogs.vue b/web-new/src/components/repo/BuildLogs.vue index 6587df66e1..39fd7eb7f6 100644 --- a/web-new/src/components/repo/BuildLogs.vue +++ b/web-new/src/components/repo/BuildLogs.vue @@ -15,7 +15,8 @@ import { computed, defineComponent, inject, onBeforeUnmount, onMounted, PropType, Ref, toRef, watch } from 'vue'; import { Build, Repo } from '~/lib/api/types'; import AnsiConvert from 'ansi-to-html'; -import useBuildProc, { findProc } from '~/compositions/useBuildProc'; +import useBuildProc from '~/compositions/useBuildProc'; +import { findProc } from '~/utils/proc'; export default defineComponent({ name: 'BuildLogs', @@ -36,15 +37,18 @@ export default defineComponent({ setup(props) { const build = toRef(props, 'build'); const procId = toRef(props, 'procId'); - const repo = computed(() => inject>('repo')?.value || null); - + const repo = inject>('repo'); const buildProc = useBuildProc(); var ansiConvert = new AnsiConvert(); const logLines = computed(() => buildProc.logs.value?.map((l) => ({ ...l, out: ansiConvert.toHtml(l.out) }))); - const proc = computed(() => build && findProc(build.value.procs, procId.value)); + const proc = computed(() => build && findProc(build.value.procs || [], procId.value)); function loadBuildProc() { + if (!repo) { + throw new Error('Unexpected: "repo" should be provided at this place'); + } + if (!repo.value || !build.value || !proc.value) { return; } diff --git a/web-new/src/compositions/useBuildProc.ts b/web-new/src/compositions/useBuildProc.ts index ad5ae56a38..f886681748 100644 --- a/web-new/src/compositions/useBuildProc.ts +++ b/web-new/src/compositions/useBuildProc.ts @@ -1,45 +1,10 @@ import { ref } from 'vue'; import { BuildProc, BuildLog } from '~/lib/api/types'; +import { isProcRunning } from '~/utils/proc'; import useApiClient from './useApiClient'; const apiClient = useApiClient(); -export function findProc(procs: BuildProc[], pid: number): BuildProc | null { - for (const proc of procs) { - if (proc.pid === pid) { - return proc; - } - if (proc.children) { - const result = findProc(proc.children, pid); - if (result) { - return result; - } - } - } - - return null; -} - -/** - * Returns true if the process is in a completed state. - * - * @param {Object} proc - The process object. - * @returns {boolean} - */ -export function isProcFinished(proc: BuildProc): boolean { - return proc.state !== 'running' && proc.state !== 'pending'; -} - -/** - * Returns true if the process is running. - * - * @param {Object} proc - The process object. - * @returns {boolean} - */ -export function isProcRunning(proc: BuildProc): boolean { - return proc.state === 'running'; -} - export default () => { const logs = ref(); const proc = ref(); diff --git a/web-new/src/store/builds.ts b/web-new/src/store/builds.ts index fcea79abd5..c731b3a886 100644 --- a/web-new/src/store/builds.ts +++ b/web-new/src/store/builds.ts @@ -1,7 +1,7 @@ import { computed, toRef, Ref, ref } from 'vue'; import { defineStore } from 'pinia'; import useApiClient from '~/compositions/useApiClient'; -import { repoSlug } from '~/compositions/useRepo'; +import { repoSlug } from '~/utils/repo'; import { Build, BuildProc } from '~/lib/api/types'; const apiClient = useApiClient(); diff --git a/web-new/src/store/repos.ts b/web-new/src/store/repos.ts index c35687f260..1d66601282 100644 --- a/web-new/src/store/repos.ts +++ b/web-new/src/store/repos.ts @@ -1,7 +1,7 @@ import { toRef, Ref, computed } from 'vue'; import { defineStore } from 'pinia'; import useApiClient from '~/compositions/useApiClient'; -import { repoSlug } from '~/compositions/useRepo'; +import { repoSlug } from '~/utils/repo'; import { Repo } from '~/lib/api/types'; const apiClient = useApiClient(); diff --git a/web-new/src/utils/proc.ts b/web-new/src/utils/proc.ts new file mode 100644 index 0000000000..43e77836e5 --- /dev/null +++ b/web-new/src/utils/proc.ts @@ -0,0 +1,37 @@ +import { BuildProc } from '~/lib/api/types'; + +export function findProc(procs: BuildProc[], pid: number): BuildProc | null { + for (const proc of procs) { + if (proc.pid === pid) { + return proc; + } + if (proc.children) { + const result = findProc(proc.children, pid); + if (result) { + return result; + } + } + } + + return null; +} + +/** + * Returns true if the process is in a completed state. + * + * @param {Object} proc - The process object. + * @returns {boolean} + */ +export function isProcFinished(proc: BuildProc): boolean { + return proc.state !== 'running' && proc.state !== 'pending'; +} + +/** + * Returns true if the process is running. + * + * @param {Object} proc - The process object. + * @returns {boolean} + */ +export function isProcRunning(proc: BuildProc): boolean { + return proc.state === 'running'; +} diff --git a/web-new/src/compositions/useRepo.ts b/web-new/src/utils/repo.ts similarity index 100% rename from web-new/src/compositions/useRepo.ts rename to web-new/src/utils/repo.ts diff --git a/web-new/src/views/repo/RepoSettings.vue b/web-new/src/views/repo/RepoSettings.vue index 333ceb7b44..274d92a842 100644 --- a/web-new/src/views/repo/RepoSettings.vue +++ b/web-new/src/views/repo/RepoSettings.vue @@ -45,7 +45,7 @@ export default defineComponent({ const repo = inject>('repo'); const badgeUrl = computed(() => { if (!repo) { - return null; + throw new Error('Unexpected: "repo" should be provided at this place'); } return `/api/badges/${repo.value.owner}/${repo.value.name}/status.svg`; diff --git a/web-new/src/views/repo/build/Build.vue b/web-new/src/views/repo/build/Build.vue index ebc8f0e358..cf71b5efc7 100644 --- a/web-new/src/views/repo/build/Build.vue +++ b/web-new/src/views/repo/build/Build.vue @@ -15,7 +15,13 @@ ]" /> -
-
{{ repo }}
+ + + + + + + + + + + + + + + + + + minutes + + +
+ +
diff --git a/web-new/src/components/repo/settings/RegistriesTab.vue b/web-new/src/components/repo/settings/RegistriesTab.vue index 05eaf08e8a..be1bcb9c24 100644 --- a/web-new/src/components/repo/settings/RegistriesTab.vue +++ b/web-new/src/components/repo/settings/RegistriesTab.vue @@ -17,9 +17,17 @@
- - - + + + + + + + + + + +
diff --git a/web/src/components/form/InputField.vue b/web/src/components/form/InputField.vue index 3ac28ea7ac..3a84852455 100644 --- a/web/src/components/form/InputField.vue +++ b/web/src/components/form/InputField.vue @@ -1,7 +1,10 @@ diff --git a/web/src/components/form/RadioField.vue b/web/src/components/form/RadioField.vue index 024c391747..17d6e04cfb 100644 --- a/web/src/components/form/RadioField.vue +++ b/web/src/components/form/RadioField.vue @@ -17,7 +17,7 @@ :checked="innerValue.includes(option.value)" @click="innerValue = option.value" /> - +
diff --git a/web/src/components/form/SelectField.vue b/web/src/components/form/SelectField.vue index 54d431fd22..c8ce6509ad 100644 --- a/web/src/components/form/SelectField.vue +++ b/web/src/components/form/SelectField.vue @@ -8,7 +8,7 @@ }" > - diff --git a/web/src/components/form/TextField.vue b/web/src/components/form/TextField.vue index 166cacef50..e155454649 100644 --- a/web/src/components/form/TextField.vue +++ b/web/src/components/form/TextField.vue @@ -2,7 +2,7 @@
From 48409eb6718a16794d29f164492e50b8fa2ffba6 Mon Sep 17 00:00:00 2001 From: Anton Bracke Date: Sat, 18 Sep 2021 17:51:08 +0200 Subject: [PATCH 069/143] remove daisy ui again --- web/package.json | 1 - web/src/components/layout/header/Navbar.vue | 14 +++++--------- web/windi.config.ts | 4 ++-- web/yarn.lock | 5 ----- 4 files changed, 7 insertions(+), 17 deletions(-) diff --git a/web/package.json b/web/package.json index 546fcb54ac..31d0a04fe4 100644 --- a/web/package.json +++ b/web/package.json @@ -19,7 +19,6 @@ "@kyvg/vue3-notification": "2.3.4", "@meforma/vue-toaster": "1.2.2", "ansi-to-html": "0.7.1", - "daisyui": "1.14.0", "humanize-duration": "3.27.0", "javascript-time-ago": "2.3.8", "node-emoji": "1.11.0", diff --git a/web/src/components/layout/header/Navbar.vue b/web/src/components/layout/header/Navbar.vue index 992ea8c989..90601da5e0 100644 --- a/web/src/components/layout/header/Navbar.vue +++ b/web/src/components/layout/header/Navbar.vue @@ -1,24 +1,20 @@ diff --git a/web/windi.config.ts b/web/windi.config.ts index 1e7fa457f9..600f8f3eee 100644 --- a/web/windi.config.ts +++ b/web/windi.config.ts @@ -1,7 +1,7 @@ /* eslint-disable import/no-extraneous-dependencies */ import daisyColors from 'daisyui/colors/index.js'; import colors from 'windicss/colors'; -import { defineConfig, transform } from 'windicss/helpers'; +import { defineConfig } from 'windicss/helpers'; import typography from 'windicss/plugin/typography'; export default defineConfig({ @@ -24,5 +24,5 @@ export default defineConfig({ fill: (theme) => theme('colors'), }, }, - plugins: [typography, transform('daisyui')], + plugins: [typography], }); diff --git a/web/yarn.lock b/web/yarn.lock index a4ac58deb8..bfc77d4ad9 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -822,11 +822,6 @@ csstype@^2.6.8: resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.17.tgz#4cf30eb87e1d1a005d8b6510f95292413f6a1c0e" integrity sha512-u1wmTI1jJGzCJzWndZo8mk4wnPTZd1eOIYTYvuEyOQGfmDl3TrabCCfKnOC86FZwW/9djqTl933UF/cS425i9A== -daisyui@1.14.0: - version "1.14.0" - resolved "https://registry.yarnpkg.com/daisyui/-/daisyui-1.14.0.tgz#1d63658c0dd87f9795d333e7068cedbb0b0a52a6" - integrity sha512-50ccXdJMi0TS7raItKfUOJppPRvluOUCO4m6i7QPOVOdDZVP6awjqUoGmLzqFepO6b6QpVotswDvaXIKSZ/2aw== - debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" From 292a517d4c0035da1f3d77ce4f598a117dfd089d Mon Sep 17 00:00:00 2001 From: Anton Bracke Date: Sat, 18 Sep 2021 17:51:23 +0200 Subject: [PATCH 070/143] improve repo settings --- .../components/repo/settings/ActionsTab.vue | 54 +++++++++++++++++ .../components/repo/settings/GeneralTab.vue | 58 ++++++------------- web/src/lib/api/types/repo.ts | 25 +------- web/src/views/repo/RepoSettings.vue | 5 ++ 4 files changed, 78 insertions(+), 64 deletions(-) create mode 100644 web/src/components/repo/settings/ActionsTab.vue diff --git a/web/src/components/repo/settings/ActionsTab.vue b/web/src/components/repo/settings/ActionsTab.vue new file mode 100644 index 0000000000..fb246c5b6f --- /dev/null +++ b/web/src/components/repo/settings/ActionsTab.vue @@ -0,0 +1,54 @@ + + + diff --git a/web/src/components/repo/settings/GeneralTab.vue b/web/src/components/repo/settings/GeneralTab.vue index 2a101e324c..ac7d3d3348 100644 --- a/web/src/components/repo/settings/GeneralTab.vue +++ b/web/src/components/repo/settings/GeneralTab.vue @@ -6,17 +6,24 @@
- - - - - - - - + + + @@ -28,27 +35,17 @@
- minutes + minutes
- -
- Actions -
diff --git a/web/windi.config.ts b/web/windi.config.ts index 600f8f3eee..7459095af5 100644 --- a/web/windi.config.ts +++ b/web/windi.config.ts @@ -1,5 +1,4 @@ /* eslint-disable import/no-extraneous-dependencies */ -import daisyColors from 'daisyui/colors/index.js'; import colors from 'windicss/colors'; import { defineConfig } from 'windicss/helpers'; import typography from 'windicss/plugin/typography'; @@ -9,7 +8,6 @@ export default defineConfig({ theme: { extend: { colors: { - green: '#4caf50', link: colors.blue[400], // status colors @@ -17,8 +15,6 @@ export default defineConfig({ 'status-gray': colors.gray[400], 'status-blue': colors.blue[400], 'status-green': '#4caf50', - - ...daisyColors, }, stroke: (theme) => theme('colors'), fill: (theme) => theme('colors'), From 92840b9e387c7e49580b7792acfc50f42db2e991 Mon Sep 17 00:00:00 2001 From: Anton Bracke Date: Sun, 19 Sep 2021 01:00:39 +0200 Subject: [PATCH 072/143] add docs-links --- web/src/App.vue | 2 +- web/src/components/atomic/DocsLink.vue | 32 +++++++++++++++++++ web/src/components/atomic/Icon.vue | 6 +++- .../components/repo/settings/GeneralTab.vue | 9 ++---- .../repo/settings/RegistriesTab.vue | 16 +++++++--- .../components/repo/settings/SecretsTab.vue | 12 +++++-- 6 files changed, 63 insertions(+), 14 deletions(-) create mode 100644 web/src/components/atomic/DocsLink.vue diff --git a/web/src/App.vue b/web/src/App.vue index d1fb190adb..b00a5b1905 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -39,7 +39,7 @@ export default defineComponent({ const notifications = useNotifications(); // eslint-disable-next-line promise/prefer-await-to-callbacks apiClient.setErrorHandler((err) => { - notifications.notify({ title: err.message, type: 'error' }); + notifications.notify({ title: err.message || 'An unkown error occurred', type: 'error' }); }); const blank = computed(() => route.meta.blank); diff --git a/web/src/components/atomic/DocsLink.vue b/web/src/components/atomic/DocsLink.vue new file mode 100644 index 0000000000..5d31bf1a90 --- /dev/null +++ b/web/src/components/atomic/DocsLink.vue @@ -0,0 +1,32 @@ + + + diff --git a/web/src/components/atomic/Icon.vue b/web/src/components/atomic/Icon.vue index 0e7dc321dd..c9c38f9503 100644 --- a/web/src/components/atomic/Icon.vue +++ b/web/src/components/atomic/Icon.vue @@ -22,6 +22,7 @@ + diff --git a/web/src/components/atomic/DocsLink.vue b/web/src/components/atomic/DocsLink.vue index 5d31bf1a90..edf7c00e03 100644 --- a/web/src/components/atomic/DocsLink.vue +++ b/web/src/components/atomic/DocsLink.vue @@ -1,6 +1,6 @@ diff --git a/web/src/components/atomic/Icon.vue b/web/src/components/atomic/Icon.vue index c9c38f9503..645ec460c1 100644 --- a/web/src/components/atomic/Icon.vue +++ b/web/src/components/atomic/Icon.vue @@ -1,59 +1,38 @@ diff --git a/web/src/components/repo/build/BuildConfig.vue b/web/src/components/repo/build/BuildConfig.vue new file mode 100644 index 0000000000..16a23badd9 --- /dev/null +++ b/web/src/components/repo/build/BuildConfig.vue @@ -0,0 +1,66 @@ + + + diff --git a/web/src/components/repo/BuildItem.vue b/web/src/components/repo/build/BuildItem.vue similarity index 81% rename from web/src/components/repo/BuildItem.vue rename to web/src/components/repo/build/BuildItem.vue index 6ea8f53733..0fab925200 100644 --- a/web/src/components/repo/BuildItem.vue +++ b/web/src/components/repo/build/BuildItem.vue @@ -4,11 +4,11 @@
@@ -25,7 +25,7 @@
- {{ message }} + {{ message }}
@@ -62,12 +62,11 @@ import { defineComponent, PropType, toRef } from 'vue'; import Icon from '~/components/atomic/Icon.vue'; import ListItem from '~/components/atomic/ListItem.vue'; +import { buildStatusColors } from '~/components/repo/build/build-status'; import BuildStatusIcon from '~/components/repo/build/BuildStatusIcon.vue'; import useBuild from '~/compositions/useBuild'; import { Build } from '~/lib/api/types'; -import { buildStatusColors } from './build/build-status'; - export default defineComponent({ name: 'BuildItem', diff --git a/web/src/components/repo/build/BuildLogs.vue b/web/src/components/repo/build/BuildLogs.vue index b37d6300a4..1c57582e29 100644 --- a/web/src/components/repo/build/BuildLogs.vue +++ b/web/src/components/repo/build/BuildLogs.vue @@ -1,5 +1,5 @@