タイトル : Next.js FastAPIと通信 その4 選択用ダイアログ
更新日 : 2024-02-23
カテゴリ : プログラミング
タグ :
frontend   
nextjs   
zustand   
python   
fastapi   

画面

ダイアログで選択する例です。

テーブル名一覧と選択したカラムを表示 画像1

カラムを選択するためのダイアログ 画像2

ボタンの文字は大文字になってしまうのね...以下で解決。

【Material ui】Button配下の英字を自動的に大文字になることを防ぐ

FastAPIの方

テーブル名とカラム名を適当に返すだけ

@dataclass
class Field:
    name: str
    dtype: str
    size: int = None

@dataclass
class Table:
    name: str
    fields: List[Field]
    # groups: List[GroupItem]

class TableInfos(BaseModel):
    version: str
    # groupInfo: GroupInfo
    tables: List[Table]

@app.get("/tables", response_model=TableInfos)
async def get_tables():

    tables = []
    for t_index in range(1, 15 + 1):
        t_name = f"Table{t_index}"

        fields = []
        for f_index in range(1, t_index + 5 + 1):
            size = None
            if f_index%4 == 0:
                type = "int"
            elif f_index%4 == 1:
                type = "str"
                size = f_index
            elif f_index%4 == 2:
                type = "float"
            else:
                type = "bool"

            fields.append(
                Field(f"t{t_index}_col{f_index}_{type}", type, size)
            )
        tables.append(
            Table(name=t_name, fields=fields)
        )

    return TableInfos(version="0.9.1", tables=tables)

Next.jsの方

page.tsx

"use client"

import { Typography, Stack, Box, Paper } from "@mui/material";
import * as React from 'react';
import Button from '@mui/material/Button';
import Grid from '@mui/material/Grid';

import { useEffect, useState } from "react";

import {useTableInfoStore} from "@/store/tableinfostore"; 
import ColumnSelectDialog from "./columnSelectDialog";

export default function Home() {
  // テーブル情報取得ストア
  const {tableinfo, fetchTables} = useTableInfoStore()
  // ダイアログの開閉用
  const [showColumnSelectDialog, setColumnSelectDialog] = useState(false);
  // 選択したテーブル名
  const [tableName, setTableName] = useState("");
  // 選択したカラム名(全体)
  const [selectedColumn, setSelectedColumn] = useState([""]);

  useEffect(() => {
    fetchTables();
    setSelectedColumn([]);
  }, []);

  const clickTableName = (tname:string) => {
    setTableName(tname)
    setColumnSelectDialog(true)
  }

  const handleClose = (values: string[]) => {
    console.log(values)
    setColumnSelectDialog(false);
  };

  return (
    <Box width={600}>
      <Stack spacing={1} padding={2}>
        <Typography>テーブル ダイアログのページ(カラムの選択をダイアログで行います)</Typography>
        <Grid container spacing={2}>
        {
          tableinfo?.tables.map((tinfo) => {
            let selected_col = false
            tinfo.fields.map((field) => {
              if ( selectedColumn.includes(field.name)) {
                  selected_col = true
              }
            })
            return (
              <Grid item xs={2} key={`g-${tinfo.name}`}>
                <Button key={tinfo.name} 
                  variant={selected_col? "contained" : "outlined"} color="secondary"
                  sx={{textTransform: "none"}}
                  onClick={ () => clickTableName(tinfo.name)}>
                                {tinfo.name}
                </Button>
              </Grid> 
            )
          })
        }
        </Grid>
        {/* カラム選択ダイアログ openの真偽値を更新してダイアログを開く */}
        <ColumnSelectDialog
          tableName={tableName}
          tableInfo={tableinfo}
          selectedColumn={selectedColumn}
          open={showColumnSelectDialog}
          onClose={handleClose}
        />
      </Stack>

      <Stack spacing={2} padding={2}>
        <Typography>選択したカラムを以下に表示します</Typography>
        <Grid container spacing={1}>
        {
          selectedColumn.map((scinfo) => {
            return (
              <Grid item xs={4} key={`g-${scinfo}`}>
                <Button key={scinfo} variant="outlined" 
                 color="secondary" sx={{textTransform: "none"}}>
                {scinfo}
                </Button>
              </Grid> 
            )
          })
        }
        </Grid>
      </Stack>
    </Box>
  );

}

ダイアログ

import * as React from 'react';
import Dialog from '@mui/material/Dialog';
import Button from '@mui/material/Button';
import { Typography, Stack } from "@mui/material";
import Grid from '@mui/material/Grid';
import { TableInfos } from "@/openapi_generated";
import { useState } from "react";

export interface ColumnSelectDialogProps {
    open: boolean;
    tableName: string;
    onClose: (values: string[]) => void;
    tableInfo?: TableInfos;
    selectedColumn: string[]
}

export default function columnSelectDialog(props: ColumnSelectDialogProps) {
    const { onClose, tableName, tableInfo, open, selectedColumn } = props;
    // 状態管理用
    const [selectedCount, setSelectedCount] = useState(0);

    const handleClose = () => {
        onClose(selectedColumn);
    };

    const clickColumnName = (columnName:string) => {
        if ( selectedColumn.includes(columnName)) {
            const index = selectedColumn.indexOf(columnName);
            selectedColumn.splice(index, 1)
        } else {
            selectedColumn.push(columnName)
        }
        // 状態更新
        setSelectedCount(selectedCount+1)
    }

    return (
        <Dialog onClose={handleClose} open={open} sx={{width: 900}}>
        <Stack spacing={1} margin={2}>
        <Typography>テーブル名 : {tableName}</Typography>
        <Typography>カラムを選択してください</Typography>
        {
            tableInfo?.tables.filter((tinfo) => tinfo.name == tableName).map((tinfo) => {
                return (
                    <Grid key={`g-${tinfo.name}`} container spacing={1}>
                    {
                        tinfo.fields.map((field) => {
                            let selected_col = false
                            if ( selectedColumn.includes(field.name)) {
                                selected_col = true
                            }
                            return (
                                <Grid item xs={4} key={`g-${tinfo.name}-${field.name}`}>
                                    <Button key={`${tinfo.name}-${field.name}`} 
                                        variant={selected_col? "contained" : "outlined"} 
                                        color={"secondary"} sx={{textTransform: "none"}}
                                        onClick={() => clickColumnName(field.name)}>
                                        {field.name}
                                    </Button>
                                </Grid>
                            )
                        })
                    }
                    </Grid>
                )
            })
        }
        <Button variant='contained' onClick={handleClose}>閉じる</Button>
        </Stack>
        </Dialog>
    );
}

ストア

import { create } from "zustand";

import { TableInfos } from "@/openapi_generated";
import { API_URL } from "@/store/settings";

type Tables = {
    tableinfo?: TableInfos;
    fetchTables: () => void;
};

export const useTableInfoStore = create<Tables>((set) => ({
    tableinfo: undefined,
    fetchTables: async () => {
        try {
            const response = await fetch(`${API_URL}/tables`);
            const res_json = await response.json();
            set({tableinfo: res_json})
        } catch (error) {
            console.error("Error fetching tables:", error);
        }
    },
}));