diff --git a/admin/src/App.tsx b/admin/src/App.tsx index 906704e2b..283e28feb 100644 --- a/admin/src/App.tsx +++ b/admin/src/App.tsx @@ -2,8 +2,6 @@ import React from "react"; import { BrowserRouter, Routes, Route } from "react-router-dom"; import DashboardLayout from "./DashboardLayout"; import Dashboard from "./pages/Dashboard"; -import View from "./pages/View"; -import ViewList from "./pages/ViewList"; import DAGDetails from "./pages/DAGDetails"; import DAGs from "./pages/DAGs"; @@ -23,8 +21,6 @@ function App({ config }: Props) { } /> } /> - } /> - } /> } /> } /> diff --git a/admin/src/DashboardLayout.tsx b/admin/src/DashboardLayout.tsx index 8bc116872..d30814669 100644 --- a/admin/src/DashboardLayout.tsx +++ b/admin/src/DashboardLayout.tsx @@ -155,7 +155,6 @@ function DashboardContent({ {[ ["/", "Dashboard"], ["/dags", "DAGs"], - ["/views", "Views"], ].map((v) => ( {children} - {/* - - */} diff --git a/admin/src/components/CreateViewButton.tsx b/admin/src/components/CreateViewButton.tsx deleted file mode 100644 index 8b6edf7fb..000000000 --- a/admin/src/components/CreateViewButton.tsx +++ /dev/null @@ -1,154 +0,0 @@ -import { Button, Stack, TextField, Typography } from "@mui/material"; -import React from "react"; -import { Modal } from "@mui/material"; -import Box from "@mui/material/Box"; -import { View } from "../models/View"; - -type Props = { - refresh: () => void; -}; - -type EditingView = { - name: string; - desc: string; - tags: string; -}; - -function CreateViewButton({ refresh }: Props) { - const [modalOpen, setModalOpen] = React.useState(false); - const [editingView, setEditingView] = React.useState>( - {} - ); - const onCreateView = React.useCallback(async () => { - const { name, tags, desc } = editingView; - if (!name || name.trim() === "") { - alert("View name must be input"); - return; - } - if (!tags || tags.trim() === "") { - alert("Tags must be input"); - return; - } - const view: View = { - Name: name, - Desc: desc || "", - ContainTags: tags.split(",").map((t) => t.trim().toLowerCase()), - }; - const url = `${API_URL}/views`; - const resp = await fetch(url, { - method: "PUT", - mode: "cors", - headers: { - Accept: "application/json", - }, - body: JSON.stringify(view), - }); - setModalOpen(false); - if (resp.ok) { - refresh(); - } else { - const e = await resp.text(); - alert(e); - } - }, [editingView]); - return ( - - - setModalOpen(false)} - aria-labelledby="modal-modal-title" - aria-describedby="modal-modal-description" - > - - - New View - - - { - setEditingView({ - ...editingView, - name: e.target.value, - }); - }} - /> - { - setEditingView({ - ...editingView, - desc: e.target.value, - }); - }} - /> - { - setEditingView({ - ...editingView, - tags: e.target.value, - }); - }} - /> - - - - - - ); -} - -export default CreateViewButton; - -const style = { - position: "absolute" as "absolute", - top: "50%", - left: "50%", - transform: "translate(-50%, -50%)", - width: 400, - bgcolor: "background.paper", - border: "2px solid #000", - boxShadow: 24, - p: 4, -}; diff --git a/admin/src/components/ViewActions.tsx b/admin/src/components/ViewActions.tsx deleted file mode 100644 index 32444e467..000000000 --- a/admin/src/components/ViewActions.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { Button, IconButton, Stack } from "@mui/material"; -import React, { ReactElement } from "react"; -import { SchedulerStatus, Status } from "../models/Status"; - -type Props = { - name: string; - refresh?: () => any; -}; - -function ViewActions({ name, refresh = () => {} }: Props) { - const onSubmit = React.useCallback( - async (warn: string) => { - if (!confirm(warn)) { - return; - } - const url = `${API_URL}/views/${encodeURI(name)}`; - const ret = await fetch(url, { - method: "DELETE", - mode: "cors", - }); - if (ret.ok) { - refresh(); - } else { - const e = await ret.text(); - alert(e); - } - }, - [refresh] - ); - return ( - - - - - } - onClick={() => onSubmit("Do you want to delete the view?")} - > - - ); -} -export default ViewActions; - -interface ActionButtonProps { - children?: string; - label?: boolean; - icon: ReactElement; - disabled?: boolean; - onClick: () => void; -} - -function ActionButton({ - icon, - onClick, - children, - disabled = false, - label = false, -}: ActionButtonProps) { - return label ? ( - - ) : ( - - {icon} - - ); -} diff --git a/admin/src/components/ViewTable.tsx b/admin/src/components/ViewTable.tsx deleted file mode 100644 index 704321a04..000000000 --- a/admin/src/components/ViewTable.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import React, { CSSProperties } from "react"; -import { - Paper, - Table, - TableBody, - TableCell, - TableHead, - TableRow, -} from "@mui/material"; -import { Link } from "react-router-dom"; -import { View } from "../models/View"; -import StyledTableRow from "./StyledTableRow"; -import ViewActions from "./ViewActions"; - -type Props = { - views: View[]; - refreshFn: () => Promise; -}; - -function ViewTable({ views, refreshFn }: Props) { - return ( - - - - - - Name - Description - Actions - - - - {views.map((v) => ( - - - {v.Name} - - {v.Desc} - - - - - ))} - -
-
-
- ); -} -const tableStyle: CSSProperties = { - tableLayout: "fixed", - wordWrap: "break-word", -}; - -export default ViewTable; diff --git a/admin/src/menu.tsx b/admin/src/menu.tsx index 47fe66ccb..e24f20a39 100644 --- a/admin/src/menu.tsx +++ b/admin/src/menu.tsx @@ -3,7 +3,7 @@ import ListItemButton from "@mui/material/ListItemButton"; import ListItemIcon from "@mui/material/ListItemIcon"; import ListItemText from "@mui/material/ListItemText"; import LayersIcon from "@mui/icons-material/Layers"; -import FilterAltIcon from '@mui/icons-material/FilterAlt'; +import FilterAltIcon from "@mui/icons-material/FilterAlt"; import DashboardIcon from "@mui/icons-material/Dashboard"; import { Link } from "react-router-dom"; @@ -25,13 +25,5 @@ export const mainListItems = ( - - - - - - - - ); diff --git a/admin/src/pages/Dashboard.tsx b/admin/src/pages/Dashboard.tsx index 3b285f578..8a4a0c0e4 100644 --- a/admin/src/pages/Dashboard.tsx +++ b/admin/src/pages/Dashboard.tsx @@ -72,7 +72,6 @@ function Dashboard() { ))} - {/* {data?.DAGs ? ( */} - {/* ) : null} */} ); } diff --git a/admin/src/pages/View.tsx b/admin/src/pages/View.tsx deleted file mode 100644 index 2ab77af58..000000000 --- a/admin/src/pages/View.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import React from "react"; -import DAGErrors from "../components/DAGErrors"; -import Box from "@mui/material/Box"; -import WithLoading from "../components/WithLoading"; -import DAGTable from "../components/DAGTable"; -import Title from "../components/Title"; -import Paper from "@mui/material/Paper"; -import { useDAGGetAPI } from "../hooks/useDAGGetAPI"; -import { DAGItem, DAGDataType } from "../models/DAGData"; -import { useParams } from "react-router-dom"; -import { DAG } from "../models/DAGData"; - -export type ApiResponse = { - Title: string; - Charset: string; - DAGs: DAG[]; - Errors: string[]; - HasError: boolean; -}; - -type Params = { - name: string; -}; - -function View() { - const params = useParams(); - - const { data, doGet } = useDAGGetAPI( - `/views/${params.name}?format=json`, - {} - ); - - React.useEffect(() => { - doGet(); - const timer = setInterval(doGet, 10000); - return () => clearInterval(timer); - }, []); - - const DAGs = React.useMemo(() => { - const ret: DAGItem[] = []; - if (!data) { - return ret; - } - for (const val of data.DAGs) { - if (!val.Error) { - ret.push({ - Type: DAGDataType.DAG, - Name: val.Config.Name, - DAG: val, - }); - } - } - return ret; - }, [data]); - - return ( - - - {`${decodeURI(params.name || "View")}`} - - - - {data && ( - - - - - )} - - - - ); -} -export default View; diff --git a/admin/src/pages/ViewList.tsx b/admin/src/pages/ViewList.tsx deleted file mode 100644 index 1047385e8..000000000 --- a/admin/src/pages/ViewList.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React from "react"; -import Box from "@mui/material/Box"; -import WithLoading from "../components/WithLoading"; -import Title from "../components/Title"; -import Paper from "@mui/material/Paper"; -import { useDAGGetAPI } from "../hooks/useDAGGetAPI"; -import CreateViewButton from "../components/CreateViewButton"; -import ViewTable from "../components/ViewTable"; -import { View } from "../models/View"; - -function ViewList() { - const { data, doGet } = useDAGGetAPI<{ Views: View[] }>("/views", {}); - - React.useEffect(() => { - doGet(); - }, []); - - return ( - - - Views - - - - - {data && } - - - - ); -} -export default ViewList; diff --git a/internal/admin/handlers/list.go b/internal/admin/handlers/list.go index 2a27e6764..2a77acc78 100644 --- a/internal/admin/handlers/list.go +++ b/internal/admin/handlers/list.go @@ -5,7 +5,6 @@ import ( "path" "path/filepath" - "github.com/yohamta/dagu/internal/admin/views" "github.com/yohamta/dagu/internal/controller" ) @@ -15,7 +14,6 @@ type dagListResponse struct { DAGs []*controller.DAG Errors []string HasError bool - Views []*views.View } type DAGListHandlerConfig struct { @@ -48,7 +46,6 @@ func HandleGetList(hc *DAGListHandlerConfig) http.HandlerFunc { DAGs: dags, Errors: errs, HasError: hasErr, - Views: views.GetViews(), } if r.Header.Get("Accept") == "application/json" { renderJson(w, data) diff --git a/internal/admin/handlers/view.go b/internal/admin/handlers/view.go deleted file mode 100644 index 7a27573e2..000000000 --- a/internal/admin/handlers/view.go +++ /dev/null @@ -1,118 +0,0 @@ -package handlers - -import ( - "fmt" - "net/http" - "path/filepath" - "regexp" - - "github.com/yohamta/dagu/internal/admin/views" - "github.com/yohamta/dagu/internal/config" - "github.com/yohamta/dagu/internal/controller" -) - -type viewResponse struct { - Title string - Charset string - DAGs []*controller.DAG - Errors []string - HasError bool -} - -type ViewHandlerConfig struct { - DAGsDir string -} - -func HandleGetView(hc *ViewHandlerConfig) http.HandlerFunc { - renderFunc := useTemplate("index.gohtml", "index") - - return func(w http.ResponseWriter, r *http.Request) { - p, err := getViewParameter(r) - if err != nil { - encodeError(w, err) - return - } - - view, err := views.GetView(p) - if err != nil { - encodeError(w, err) - return - } - - dir := filepath.Join(hc.DAGsDir, "") - dags, errs, err := controller.GetDAGs(dir) - if err != nil { - encodeError(w, err) - return - } - - filteredDags := []*controller.DAG{} - filter := &config.ContainTagsMatcher{ - Tags: view.ContainTags, - } - for _, d := range dags { - if filter.Matches(d.Config) { - filteredDags = append(filteredDags, d) - } - } - - hasErr := false - for _, j := range dags { - if j.Error != nil { - hasErr = true - break - } - } - if len(errs) > 0 { - hasErr = true - } - - data := &viewResponse{ - Title: "View", - DAGs: filteredDags, - Errors: errs, - HasError: hasErr, - } - - if isJsonRequest(r) { - renderJson(w, data) - } else { - renderFunc(w, data) - } - } -} - -func HandleDeleteView() http.HandlerFunc { - - return func(w http.ResponseWriter, r *http.Request) { - p, err := getViewParameter(r) - if err != nil { - encodeError(w, err) - return - } - - view, err := views.GetView(p) - if err != nil { - encodeError(w, err) - return - } - - err = views.DeleteView(view) - if err != nil { - encodeError(w, err) - return - } - - w.WriteHeader(http.StatusOK) - w.Write([]byte("OK")) - } -} - -func getViewParameter(r *http.Request) (string, error) { - re := regexp.MustCompile(`/views/([^/\?]+)/?$`) - m := re.FindStringSubmatch(r.URL.Path) - if len(m) < 2 { - return "", fmt.Errorf("invalid URL") - } - return m[1], nil -} diff --git a/internal/admin/handlers/views.go b/internal/admin/handlers/views.go deleted file mode 100644 index 32cff20d6..000000000 --- a/internal/admin/handlers/views.go +++ /dev/null @@ -1,50 +0,0 @@ -package handlers - -import ( - "encoding/json" - "net/http" - - "github.com/yohamta/dagu/internal/admin/views" -) - -type viewListResponse struct { - Title string - Charset string - Views []*views.View -} - -func HandleGetViewList() http.HandlerFunc { - renderFunc := useTemplate("index.gohtml", "index") - return func(w http.ResponseWriter, r *http.Request) { - data := &viewListResponse{ - Title: "Views", - Views: views.GetViews(), - } - if r.Header.Get("Accept") == "application/json" { - renderJson(w, data) - } else { - renderFunc(w, data) - } - } -} - -func HandlePutView() http.HandlerFunc { - - return func(w http.ResponseWriter, r *http.Request) { - var v views.View - err := json.NewDecoder(r.Body).Decode(&v) - if err != nil { - encodeError(w, err) - return - } - - err = views.SaveView(&v) - if err != nil { - encodeError(w, err) - return - } - - w.WriteHeader(http.StatusOK) - w.Write([]byte("OK")) - } -} diff --git a/internal/admin/routes.go b/internal/admin/routes.go index 69ee91969..4e93b1833 100644 --- a/internal/admin/routes.go +++ b/internal/admin/routes.go @@ -29,14 +29,6 @@ func defaultRoutes(cfg *Config) []*route { DAGsDir: cfg.DAGs, }, )}, - {http.MethodGet, `^/views/?$`, handlers.HandleGetViewList()}, - {http.MethodPut, `^/views/?$`, handlers.HandlePutView()}, - {http.MethodGet, `^/views/([^/]+)?$`, handlers.HandleGetView( - &handlers.ViewHandlerConfig{ - DAGsDir: cfg.DAGs, - }, - )}, - {http.MethodDelete, `^/views/([^/]+)?$`, handlers.HandleDeleteView()}, {http.MethodPost, `^/dags/?$`, handlers.HandlePostList( &handlers.DAGListHandlerConfig{ DAGsDir: cfg.DAGs, diff --git a/internal/admin/views.go b/internal/admin/views.go deleted file mode 100644 index e6002d3b7..000000000 --- a/internal/admin/views.go +++ /dev/null @@ -1,84 +0,0 @@ -package admin - -import ( - "encoding/json" - "fmt" - - "github.com/yohamta/dagu/internal/settings" - "github.com/yohamta/dagu/internal/storage" - "github.com/yohamta/dagu/internal/utils" -) - -type View struct { - Name string - Desc string - ContainTags []string -} - -func ViewFromJson(b []byte) (*View, error) { - v := &View{} - err := json.Unmarshal(b, v) - return v, err -} - -func (v *View) ToJson() ([]byte, error) { - return json.Marshal(v) -} - -func GetViews() []*View { - s := storage.NewStorage( - settings.MustGet(settings.SETTING__VIEWS_DIR), - ) - fis, err := s.List() - if err != nil { - fmt.Println(err) - return nil - } - ret := make([]*View, 0, len(fis)) - for _, fi := range fis { - dat := s.MustRead(fi.Name()) - if dat != nil { - v, err := ViewFromJson(dat) - utils.LogErr("Controller: get views", err) - if err == nil { - ret = append(ret, v) - } - } - } - return ret -} - -func SaveView(view *View) error { - s := storage.NewStorage( - settings.MustGet(settings.SETTING__VIEWS_DIR), - ) - if view.Name == "" { - return ErrInvalidName - } - b, err := view.ToJson() - if err != nil { - return err - } - return s.Save(fmt.Sprintf("%s.json", view.Name), b) -} - -func DeleteView(view *View) error { - s := storage.NewStorage( - settings.MustGet(settings.SETTING__VIEWS_DIR), - ) - return s.Delete(fmt.Sprintf("%s.json", view.Name)) -} - -var ErrInvalidName = fmt.Errorf("view's name is invalid or empty") -var ErrNotFound = fmt.Errorf("not found") - -func GetView(name string) (*View, error) { - s := storage.NewStorage( - settings.MustGet(settings.SETTING__VIEWS_DIR), - ) - dat := s.MustRead(fmt.Sprintf("%s.json", name)) - if dat == nil { - return nil, ErrNotFound - } - return ViewFromJson(dat) -} diff --git a/internal/admin/views/views.go b/internal/admin/views/views.go deleted file mode 100644 index eb96e3273..000000000 --- a/internal/admin/views/views.go +++ /dev/null @@ -1,84 +0,0 @@ -package views - -import ( - "encoding/json" - "fmt" - - "github.com/yohamta/dagu/internal/settings" - "github.com/yohamta/dagu/internal/storage" - "github.com/yohamta/dagu/internal/utils" -) - -type View struct { - Name string - Desc string - ContainTags []string -} - -func ViewFromJson(b []byte) (*View, error) { - v := &View{} - err := json.Unmarshal(b, v) - return v, err -} - -func (v *View) ToJson() ([]byte, error) { - return json.Marshal(v) -} - -func GetViews() []*View { - s := storage.NewStorage( - settings.MustGet(settings.SETTING__VIEWS_DIR), - ) - fis, err := s.List() - if err != nil { - fmt.Println(err) - return nil - } - ret := make([]*View, 0, len(fis)) - for _, fi := range fis { - dat := s.MustRead(fi.Name()) - if dat != nil { - v, err := ViewFromJson(dat) - utils.LogErr("Controller: get views", err) - if err == nil { - ret = append(ret, v) - } - } - } - return ret -} - -func SaveView(view *View) error { - s := storage.NewStorage( - settings.MustGet(settings.SETTING__VIEWS_DIR), - ) - if view.Name == "" { - return ErrInvalidName - } - b, err := view.ToJson() - if err != nil { - return err - } - return s.Save(fmt.Sprintf("%s.json", view.Name), b) -} - -func DeleteView(view *View) error { - s := storage.NewStorage( - settings.MustGet(settings.SETTING__VIEWS_DIR), - ) - return s.Delete(fmt.Sprintf("%s.json", view.Name)) -} - -var ErrInvalidName = fmt.Errorf("view's name is invalid or empty") -var ErrNotFound = fmt.Errorf("not found") - -func GetView(name string) (*View, error) { - s := storage.NewStorage( - settings.MustGet(settings.SETTING__VIEWS_DIR), - ) - dat := s.MustRead(fmt.Sprintf("%s.json", name)) - if dat == nil { - return nil, ErrNotFound - } - return ViewFromJson(dat) -} diff --git a/internal/admin/views/views_test.go b/internal/admin/views/views_test.go deleted file mode 100644 index 97dfbc307..000000000 --- a/internal/admin/views/views_test.go +++ /dev/null @@ -1,72 +0,0 @@ -package views - -import ( - "os" - "path" - "testing" - - "github.com/stretchr/testify/require" - "github.com/yohamta/dagu/internal/settings" - "github.com/yohamta/dagu/internal/utils" -) - -func TestMain(m *testing.M) { - tmpDir := utils.MustTempDir("test-views") - os.Setenv("HOST", "localhost") - settings.ChangeHomeDir(tmpDir) - code := m.Run() - os.RemoveAll(tmpDir) - os.Exit(code) -} - -func TestView(t *testing.T) { - viewsDir := settings.MustGet(settings.SETTING__VIEWS_DIR) - defer func() { - os.RemoveAll(viewsDir) - }() - - view := &View{ - Name: "", - ContainTags: []string{"a", "b"}, - } - - err := SaveView(view) - require.EqualError(t, err, ErrInvalidName.Error()) - - view = &View{ - Name: "test", - ContainTags: []string{"a", "b"}, - } - - err = SaveView(view) - require.NoError(t, err) - - require.True(t, utils.FileExists(path.Join(viewsDir, "test.json"))) - - views := GetViews() - require.Equal(t, 1, len(views)) - require.Equal(t, view, views[0]) - - v2, err := GetView("test") - require.NoError(t, err) - require.Equal(t, view, v2) - - err = DeleteView(v2) - require.NoError(t, err) - - _, err = GetView("test") - require.Error(t, err) -} - -func TestViewMarshaling(t *testing.T) { - v := &View{ - Name: "test", - ContainTags: []string{"a", "b"}, - } - js, err := v.ToJson() - require.NoError(t, err) - - v2, err := ViewFromJson(js) - require.NoError(t, err) - require.Equal(t, v, v2) -} diff --git a/internal/storage/storage.go b/internal/storage/storage.go deleted file mode 100644 index ec6c1d57c..000000000 --- a/internal/storage/storage.go +++ /dev/null @@ -1,48 +0,0 @@ -package storage - -import ( - "os" - "path" - - "github.com/yohamta/dagu/internal/utils" -) - -// Storage is the interface to save / load / delete arbitrary files. -type Storage struct { - Dir string -} - -// NewStorage creates a new storage. -func NewStorage(dir string) *Storage { - os.MkdirAll(dir, 0755) - return &Storage{ - Dir: dir, - } -} - -// List returns a list of files in the storage. -func (s *Storage) List() ([]os.FileInfo, error) { - f, err := os.Open(s.Dir) - if err != nil { - return nil, err - } - defer f.Close() - return f.Readdir(0) -} - -// Save writes the given data to the given file. -func (s *Storage) Save(file string, b []byte) error { - return os.WriteFile(path.Join(s.Dir, file), b, 0644) -} - -// Delete deletes the given file. -func (s *Storage) Delete(file string) error { - return os.Remove(path.Join(s.Dir, file)) -} - -// MustRead reads the given file and returns the content. -func (s *Storage) MustRead(file string) []byte { - b, err := os.ReadFile(path.Join(s.Dir, file)) - utils.LogErr("storage: read file", err) - return b -} diff --git a/internal/storage/storage_test.go b/internal/storage/storage_test.go deleted file mode 100644 index 8a529684a..000000000 --- a/internal/storage/storage_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package storage - -import ( - "os" - "path" - "testing" - - "github.com/stretchr/testify/require" - "github.com/yohamta/dagu/internal/utils" -) - -func TestStorage(t *testing.T) { - tmpDir := utils.MustTempDir("test-storage") - defer os.RemoveAll(tmpDir) - - // create data for test - data := "{ \"Name\": \"test\" }" - f, _ := utils.CreateFile(path.Join(tmpDir, "test.json")) - _, _ = f.WriteString(data) - f.Sync() - f.Close() - - // confirm data is saved - s := &Storage{tmpDir} - fis, err := s.List() - require.NoError(t, err) - - require.Equal(t, 1, len(fis)) - require.Equal(t, "test.json", fis[0].Name()) - - // save data with same name - data2 := "{ \"Name\": \"test\" }" - err = s.Save(fis[0].Name(), []byte(data2)) - require.NoError(t, err) - - // confirm data is overwritten - b2 := s.MustRead(fis[0].Name()) - require.Equal(t, data2, string(b2)) - - // test delete - err = s.Delete(fis[0].Name()) - require.NoError(t, err) - require.False(t, utils.FileExists(path.Join(tmpDir, "test.json"))) -}