フリーランス 技術調査ブログ

フリーランス/エンジニア Ruby Python Nodejs Vuejs React Dockerなどの調査技術調査の備忘録

Redux Toolkitを調べる

はじめに

  • Redux Toolkitを調べてみる。

インストール

  • 下記のようにインストールすると、Redux Toolkitをインストールすることができる。
npx create-react-app my-app --template redux-typescript

Redux DevToolsインストール

  • Reduxを開発する際はRedux DevToolsをインストールすると実行されたアクションの履歴やstoreの状態を確認できるので開発時には便利なのでインストールした方が良い。

chrome.google.com

f:id:PX-WING:20201231085552p:plain

Redux Toolkitで覚えておく必要がある関数

useSelector

  • セレクター機能を使用して、Reduxストアの状態からデータを抽出できます。
import { shallowEqual, useSelector } from 'react-redux'

// later
const selectedData = useSelector(selectorReturningObject, shallowEqual)

react-redux.js.org

useDispatch

  • Reduxストアからディスパッチ関数への参照を返します。必要に応じてアクションをディスパッチするために使用できます。
import React from 'react'
import { useDispatch } from 'react-redux'

export const CounterComponent = ({ value }) => {
  const dispatch = useDispatch()

  return (
    <div>
      <span>{value}</span>
      <button onClick={() => dispatch({ type: 'increment-counter' })}>
        Increment counter
      </button>
    </div>
  )
}

react-redux.js.org

configureStore

  • 標準のReduxcreateStore関数をわかりやすく抽象化したもので、ストアのセットアップに適切なデフォルトを追加して、開発エクスペリエンスを向上させます。

https://redux-toolkit.js.org/api/configureStore

createSlice

  • 初期状態、reducers関数でいっぱいのオブジェクト、「スライス名」を受け入れ、レデューサーと状態に対応するアクションクリエーターとアクションタイプを自動的に生成する関数。
import { createSlice, PayloadAction } from '@reduxjs/toolkit'

interface CounterState {
  value: number
}

const initialState = { value: 0 } as CounterState

const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment(state) {
      state.value++
    },
    decrement(state) {
      state.value--
    },
    incrementByAmount(state, action: PayloadAction<number>) {
      state.value += action.payload
    },
  },
})

export const { increment, decrement, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer

redux-toolkit.js.org

Laravel-admin View部分を編集できるようにする

Laravel-admin テンプレート(view)を変更する方法

  • \resources\views\フォルダ配下にlaravel-adminというフォルダを作成する。

  • \vendor\encore\laravel-admin\resources\views\以下の全ファイル・全フォルダを「\resources\views\laravel-admin」フォルダへコピーする

Laravel-adminのログイン情報を取得できるオブジェクト

オブジェクト 概要
Admin::user(); 現在のユーザーオブジェクトを取得します。
Admin::user()->id; 現在のユーザーIDを取得します。
Admin::user()->roles; ユーザーの役割を取得します。
Admin::user()->permissions; ユーザーの権限を取得します。
Admin::user()->isRole('developer'); ユーザーは役割です。
Admin::user()->can('create-post'); ユーザーには権限があります。
Admin::user()->cannot('delete-post'); ユーザーには権限がありません。
Admin::user()->isAdministrator(); ユーザースーパー管理者です。
Admin::user()->inRoles(['editor', 'developer']); いずれかの役割のユーザーです。

github.com

サンプルコード

  • サイドメニューにログインしたアカウントを表示する例
  • サイドメニュー のテンプレートはresources/views/laravel-admin/partials/sidebar.blade.phpファイルとなるのでこちらのファイルを修正する
            <li class="header">ダウンロード</li>
            <li class="treeview">
                <a href="#">
                    <i class="fa fa-bars"></i>{{ Admin::user()->name }}<span>様</span>
                    <i class="fa fa-angle-left pull-right"></i>
                </a>
                <ul class="treeview-menu">
                    <li>
                        <a href="/admin/housing/{{ Admin::user()->username }}">
                            <i class="fa fa-bars"></i>
                            <span>一覧</span>
                        </a>
                    </li>
                    <li>
                        <a href="/admin/housing/{{ Admin::user()->username }}/dropbox">
                        <i class="fa fa-bars"></i>
                            <span>ダウンロード</span>
                        </a>
                    </li>
                </ul>
            </li>

Laravel-adminのUserControllerを変更する

はじめに

  • Laravel adminのadmin_usersテーブルにカラム追加と画面を変更する

手順

  • vendorフォルダにあるlaravel-adminのコントローラーを自身のappフォルダ内に移動させる。ファイル名は変更する。
$ cp ./vendor/encore/laravel-admin/src/Controllers/UserController.php ./app/Admin/Controllers/AdminUserController.php
  • app/Admin/routes.phpファイルに下記の記述を追記する
Route::group([
    'prefix'        => config('admin.route.prefix'),
    'namespace'     => config('admin.route.namespace'),
    'middleware'    => config('admin.route.middleware'),
], function (Router $router) {
     (省略)
    $router->resource('auth/users', AdminUserController::class);
     (省略)
}

- migrationでadmin_usersテーブルにカラムを追加する指定をする

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class AddSponsorIdToAdminUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('admin_users', function (Blueprint $table) {
            $table->integer('hogehoge_id')->nullable()->default(0);
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('admin_users', function (Blueprint $table) {
            $table->dropColumn(['hogehoge_id']);
        });
    }
}

- 最初にコピーしたコントローラーのファイルに追加したカラムを追加する。

    public function form()
    {
        return Administrator::form(function (Form $form) {
                (省略)
          $form->number('hogehoge_id','HOGEHOGE_ID'); 
                (省略)
  }
    }

実行結果

f:id:PX-WING:20201229150136p:plain

React/TypeScriptでグラフを作成する①

はじめに

利用するライブラリ

  • 下記のreact-chartjs-2を使ってみる github.com

インストール

  • 下記のコマンドを実行する
yarn add react-chartjs-2 chart.js

折れ線グラフのコンポーネント

import React from 'react'
import {Line} from 'react-chartjs-2'

const data ={
    labels: ["Mon","Tue","Wed","Thu","Fir","Sat","Sun"],
    datasets: [
        {
            label: "Demo line plot",
            backgroundColor: "#008080",
            borderColor: "#7fffd4",
            pointBorderWidth: 10,
            data: [5,6,9,15,30,40,80]

        }
    ]
}

export const LinePlot: React.FC = () => {
    return (
        <div>
            <Line data={data} />
        </div>
    )
}

円グラフのコンポーネント

import React from 'react'
import {Pie, Doughnut } from 'react-chartjs-2'

const data = {
    labels: ['Windows', 'Mac', 'Linux'],
    datasets: [
        {
            data: [60,30,10],
            backgroundColor: ["#4169e1","#ff1493","#FFCE56"],
            hoverBackgroundColor:  ["#36A2EB","#FF6384","#FFCE56"],
            borderColor: ["transparent","transparent","transparent"]
        }
    ]
} 

export const PiePlot: React.FC = () => {
    return (
        <div>
            <Pie data={data} />
            <Doughnut data={data} />
        </div>
    )
}

App.tsxファイルにコンポーネントを指定する

import React from 'react';
import './App.css';
import { LinePlot } from './components/LinePlot';
import { PiePlot } from './components/PiePlot';

const App: React.FC = () => {
  return (
    <div className="App">
      <LinePlot />
      <PiePlot />
    </div>
  );
}

export default App;

実行結果

f:id:PX-WING:20201228221502p:plain

TypeScriptでReactコンポートを作成する

はじめに

  • 前回発生したエラーを解消できたので、今回はTypeScriptでfunctional componentを作成してみる。 px-wing.hatenablog.com

コード

App.tsx

  • 下記コードでTypeScriptを利用している箇所はReact.FCの部分のみです。
  • React.FC はReact.FunctionComponentのことを指しております。
import React from 'react';
import './App.css';
import TestComponent from './components/TestComponent';

const App: React.FC = () => {
  return (
    <div className="App">
      <header className="App-header">
        <TestComponent text="test" />
      </header>
    </div>
  );
}

export default App;

/components/TestComponent.tsxコンポーネント作成

  • /components/TestComponent.tsxファイルを作成して下記のコードを記述する
  • 特別な箇所はイベントの箇所で、vscodeを利用していると作成したイベントにカーソルを当てると型を表示されるので、その表示された型をイベントメソッドの型として利用する f:id:PX-WING:20201228203425p:plain
import React, { useState } from 'react'

interface Props {
    text: string
}

interface UserDate {
    id: number;
    name: string;
}

const TestComponent: React.FC<Props> = (props) => {
    const [count, setCount] = useState<number | null>(0)
    const [user, setUser] = useState<UserDate>({id: 1, name: "Yamada Tarou"})
    const [inputDate, setInputDate] = useState("")

    const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
      setInputDate(e.target.value)
    }   

    return (
        <div>
            <h1>{props.text}</h1>
            <h1>{count}</h1>
            <h1>{user.id} {user.name}</h1>
            <input type="text" value={inputDate} onChange={handleInputChange} />
            {inputDate}
        </div>
    )
}

export default TestComponent

実際の画面

f:id:PX-WING:20201228203519p:plain

おまけ

Reactのfunctional componentのアロー関数の雛形

f:id:PX-WING:20201228093819p:plain

  • vscode上でrafceと入力するとReactのfunctional componentのアロー関数版のテンプレートが出てくる
import React from 'react'

const TestComponent = () => {
    return (
        <div>
            
        </div>
    )
}

export default TestComponent

React yarn start時にbabel-loaderのエラーを解消する

はじめに

  • 久しぶりにReactを触ろうとした時に、yarn startの時にエラーが発生したので、そのエラーの対処方法をまとめる

reactのプロジェクトの作成

  • 下記の手順でプロジェクトを作成して
# npx create-react-app <my_app> --template typescript
# cd <my_app>
# yarn start

エラーの内容

  • yarn start時に下記のエラーが発生した
yarn run v1.22.5
$ react-scripts start

There might be a problem with the project dependency tree.
It is likely not a bug in Create React App, but something you need to fix locally.

The react-scripts package provided by Create React App requires a dependency:

  "babel-loader": "8.1.0"

Don't try to install it manually: your package manager does it automatically.
However, a different version of babel-loader was detected higher up in the tree:

  /srv/frontend/app/node_modules/babel-loader (version: 8.2.2) 

Manually installing incompatible versions is known to cause hard-to-debug issues.

If you would prefer to ignore this check, add SKIP_PREFLIGHT_CHECK=true to an .env file in your project.
That will permanently disable this message but you might encounter other issues.

To fix the dependency tree, try following the steps below in the exact order:

  1. Delete package-lock.json (not package.json!) and/or yarn.lock in your project folder.
  2. Delete node_modules in your project folder.
  3. Remove "babel-loader" from dependencies and/or devDependencies in the package.json file in your project folder.
  4. Run npm install or yarn, depending on the package manager you use.

In most cases, this should be enough to fix the problem.
If this has not helped, there are a few other things you can try:

  5. If you used npm, install yarn (http://yarnpkg.com/) and repeat the above steps with it instead.
     This may help because npm has known issues with package hoisting which may get resolved in future versions.

  6. Check if /srv/frontend/app/node_modules/babel-loader is outside your project directory.
     For example, you might have accidentally installed something in your home folder.

  7. Try running npm ls babel-loader in your project folder.
     This will tell you which other package (apart from the expected react-scripts) installed babel-loader.

If nothing else helps, add SKIP_PREFLIGHT_CHECK=true to an .env file in your project.
That would permanently disable this preflight check in case you want to proceed anyway.

P.S. We know this message is long but please read the steps above :-) We hope you find them helpful!

error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

対処方法①

  • 下記の手順で解消する場合もあるらしいが、自分の環境では解消できなかった
# rm yarn.lock 
# rm -Rf node_modules/
# yarn install
# yarn start

対処方法②

  • .envファイルを作成して下記の記述を.envファイルに記述する
SKIP_PREFLIGHT_CHECK=true

原因

  • おそらくディレクトリ内に複数のプロジェクトがあるため、他プロジェクトで利用しているbabel-loaderが影響している可能性がある。他のプロジェクトのnode_moduleフォルダを削除すると、このエラーは発生しないかも。

再度yarn startすると起動できることを確認する

f:id:PX-WING:20201227234258p:plain

strapiの管理画面のソースの場所を確認する

はじめに

  • strapiのドキュメントに記載されている方法で管理画面のレイアウトを変更してみる。(うまくいきませんでした。) strapi.io

デフォルトの画面

  • 何も変更していない場合の画面は下記の状態になっている f:id:PX-WING:20201226224217p:plain

  • strapiの管理画面関連のファイルはApp_Root/node_modules/strapi-admin/admin/src/の中にあり、画面のView側にあたる部分は App_Root/node_modules/strapi-admin/admin/src/containers/に格納されているようです。下記がディレクトリ構成となります。

.App_Root/node_modules/strapi-admin/admin/src/containers/
├── Admin
│   ├── Logout
│   └── tests
├── App
│   ├── styles
│   └── tests
├── ApplicationInfosPage
│   └── components
│       ├── Detail
│       ├── InfoText
│       ├── Link
│       └── Wrapper
├── AuthPage
│   ├── components
│   │   ├── AuthLink
│   │   ├── Box
│   │   ├── ForgotPassword
│   │   ├── ForgotPasswordSuccess
│   │   ├── Input
│   │   ├── Login
│   │   ├── Logo
│   │   ├── Oops
│   │   ├── Register
│   │   ├── ResetPassword
│   │   └── Section
│   ├── tests
│   └── utils
├── ErrorBoundary
│   └── tests
├── HomePage
├── InstalledPluginsPage
│   └── utils
│       └── tests
├── LanguageProvider
├── LeftMenu
│   ├── tests
│   └── utils
│       └── tests
├── LocaleToggle
│   └── tests
├── MarketplacePage
│   └── PluginCard
├── NewNotification
│   ├── Notification
│   └── tests
├── NotFoundPage
├── NotificationProvider
├── Onboarding
│   ├── StaticLinks
│   └── utils
├── PluginDispatcher
│   └── tests
├── PrivateRoute
├── ProfilePage
│   └── utils
├── Roles
│   ├── CreatePage
│   ├── EditPage
│   │   └── utils
│   ├── ListPage
│   ├── ProtectedEditPage
│   └── ProtectedListPage
├── SettingsHeaderSearchContextProvider
├── SettingsPage
│   ├── components
│   │   ├── ApplicationDetailLink
│   │   ├── MenuWrapper
│   │   ├── SettingDispatcher
│   │   ├── StyledLeftMenu
│   │   └── Wrapper
│   └── utils
│       └── tests
├── Theme
├── Users
│   ├── EditPage
│   │   └── utils
│   ├── ListPage
│   │   ├── tests
│   │   └── utils
│   │       └── tests
│   ├── ProtectedEditPage
│   └── ProtectedListPage
└── Webhooks
    ├── EditView
    │   ├── tests
    │   └── utils
    ├── ListView
    │   └── tests
    ├── ProtectedCreateView
    ├── ProtectedEditView
    └── ProtectedListView

ダッシュボードを変更する場合

  • 下記のコマンドを実行して管理画面のファイルを格納するためのフォルダを作成する
mkdir -p . app_root/admin/src/containers/HomePage
  • 下記のコマンドで管理画面のベースとなるファイルを作成したフォルダにコピーする
cp ./node_modules/strapi-admin/admin/src/containers/HomePage/index.js ./admin/src/containers/HomePage/
  • コピーしたHomePage/index.jsファイルは下記のようなファイルになっている
/*
 *
 * HomePage
 *
 */
/* eslint-disable */
import React, { memo, useMemo } from 'react';
import { FormattedMessage } from 'react-intl';
import { get, upperFirst } from 'lodash';
import { auth, LoadingIndicatorPage } from 'strapi-helper-plugin';
import PageTitle from '../../components/PageTitle';
import { useModels } from '../../hooks';

import useFetch from './hooks';
import { ALink, Block, Container, LinkWrapper, P, Wave, Separator } from './components';
import BlogPost from './BlogPost';
import SocialLink from './SocialLink';

const FIRST_BLOCK_LINKS = [
  {
    link:
      'https://strapi.io/documentation/v3.x/getting-started/quick-start.html#_4-create-a-category-content-type',
    contentId: 'app.components.BlockLink.documentation.content',
    titleId: 'app.components.BlockLink.documentation',
  },
  {
    link: 'https://github.com/strapi/foodadvisor',
    contentId: 'app.components.BlockLink.code.content',
    titleId: 'app.components.BlockLink.code',
  },
];

const SOCIAL_LINKS = [
  {
    name: 'GitHub',
    link: 'https://github.com/strapi/strapi/',
  },
  {
    name: 'Slack',
    link: 'https://slack.strapi.io/',
  },
  {
    name: 'Medium',
    link: 'https://medium.com/@strapi',
  },
  {
    name: 'Twitter',
    link: 'https://twitter.com/strapijs',
  },
  {
    name: 'Reddit',
    link: 'https://www.reddit.com/r/Strapi/',
  },
  {
    name: 'Forum',
    link: 'https://forum.strapi.io',
  },
  {
    name: 'Academy',
    link: 'https://academy.strapi.io',
  },
];

const HomePage = ({ history: { push } }) => {
  const { error, isLoading, posts } = useFetch();
  // Temporary until we develop the menu API
  const { collectionTypes, singleTypes, isLoading: isLoadingForModels } = useModels();

  const handleClick = e => {
    e.preventDefault();

    push(
      '/plugins/content-type-builder/content-types/plugins::users-permissions.user?modalType=contentType&kind=collectionType&actionType=create&settingType=base&forTarget=contentType&headerId=content-type-builder.modalForm.contentType.header-create&header_icon_isCustom_1=false&header_icon_name_1=contentType&header_label_1=null'
    );
  };

  const hasAlreadyCreatedContentTypes = useMemo(() => {
    const filterContentTypes = contentTypes => contentTypes.filter(c => c.isDisplayed);

    return (
      filterContentTypes(collectionTypes).length > 1 || filterContentTypes(singleTypes).length > 0
    );
  }, [collectionTypes, singleTypes]);

  if (isLoadingForModels) {
    return <LoadingIndicatorPage />;
  }

  const headerId = hasAlreadyCreatedContentTypes
    ? 'HomePage.greetings'
    : 'app.components.HomePage.welcome';
  const username = get(auth.getUserInfo(), 'username', '');
  const linkProps = hasAlreadyCreatedContentTypes
    ? {
        id: 'app.components.HomePage.button.blog',
        href: 'https://strapi.io/blog/',
        onClick: () => {},
        type: 'blog',
        target: '_blank',
      }
    : {
        id: 'app.components.HomePage.create',
        href: '',
        onClick: handleClick,
        type: 'documentation',
      };

  return (
    <>
      <FormattedMessage id="HomePage.helmet.title">
        {title => <PageTitle title={title} />}
      </FormattedMessage>
      <Container className="container-fluid">
        <div className="row">
          <div className="col-lg-8 col-md-12">
            <Block>
              <Wave />
              <FormattedMessage
                id={headerId}
                values={{
                  name: upperFirst(username),
                }}
              >
                {msg => <h2 id="mainHeader">{msg}</h2>}
              </FormattedMessage>
              {hasAlreadyCreatedContentTypes ? (
                <FormattedMessage id="app.components.HomePage.welcomeBlock.content.again">
                  {msg => <P>{msg}</P>}
                </FormattedMessage>
              ) : (
                <FormattedMessage id="HomePage.welcome.congrats">
                  {congrats => {
                    return (
                      <FormattedMessage id="HomePage.welcome.congrats.content">
                        {content => {
                          return (
                            <FormattedMessage id="HomePage.welcome.congrats.content.bold">
                              {boldContent => {
                                return (
                                  <P>
                                    <b>{congrats}</b>&nbsp;
                                    {content}&nbsp;
                                    <b>{boldContent}</b>
                                  </P>
                                );
                              }}
                            </FormattedMessage>
                          );
                        }}
                      </FormattedMessage>
                    );
                  }}
                </FormattedMessage>
              )}
              {hasAlreadyCreatedContentTypes && (
                <div style={{ marginTop: isLoading ? 60 : 50 }}>
                  {posts.map((post, index) => (
                    <BlogPost
                      {...post}
                      key={post.link}
                      isFirst={index === 0}
                      isLoading={isLoading}
                      error={error}
                    />
                  ))}
                </div>
              )}
              <FormattedMessage id={linkProps.id}>
                {msg => (
                  <ALink
                    rel="noopener noreferrer"
                    {...linkProps}
                    style={{ verticalAlign: ' bottom', marginBottom: 5 }}
                  >
                    {msg}
                  </ALink>
                )}
              </FormattedMessage>
              <Separator style={{ marginTop: 37, marginBottom: 36 }} />
              <div style={{ display: 'flex', justifyContent: 'space-between' }}>
                {FIRST_BLOCK_LINKS.map((data, index) => {
                  const type = index === 0 ? 'doc' : 'code';

                  return (
                    <LinkWrapper href={data.link} target="_blank" key={data.link} type={type}>
                      <FormattedMessage id={data.titleId}>
                        {title => <p className="bold">{title}</p>}
                      </FormattedMessage>
                      <FormattedMessage id={data.contentId}>
                        {content => <p>{content}</p>}
                      </FormattedMessage>
                    </LinkWrapper>
                  );
                })}
              </div>
            </Block>
          </div>

          <div className="col-md-12 col-lg-4">
            <Block style={{ paddingRight: 30, paddingBottom: 0 }}>
              <FormattedMessage id="HomePage.community">{msg => <h2>{msg}</h2>}</FormattedMessage>
              <FormattedMessage id="app.components.HomePage.community.content">
                {content => <P style={{ marginTop: 7, marginBottom: 0 }}>{content}</P>}
              </FormattedMessage>
              <FormattedMessage id="HomePage.roadmap">
                {msg => (
                  <ALink
                    rel="noopener noreferrer"
                    href="https://portal.productboard.com/strapi/1-public-roadmap/tabs/2-under-consideration"
                    target="_blank"
                  >
                    {msg}
                  </ALink>
                )}
              </FormattedMessage>

              <Separator style={{ marginTop: 18 }} />
              <div
                className="row social-wrapper"
                style={{
                  display: 'flex',
                  margin: 0,
                  marginTop: 36,
                  marginLeft: -15,
                }}
              >
                {SOCIAL_LINKS.map((value, key) => (
                  <SocialLink key={key} {...value} />
                ))}
              </div>
            </Block>
          </div>
        </div>
      </Container>
    </>
  );
};

export default memo(HomePage);
  • 公式サイトのように下記の内容のファイルを設置しても管理画面のレイアウトは変更されませんでした。なぜだろう
import React, { memo } from 'react';

import { Block, Container } from './components';

const HomePage = ({ global: { plugins }, history: { push } }) => {
  return (
    <>
      <Container className="container-fluid">
        <div className="row">
          <div className="col-12">
            <Block>Hello World!</Block>
          </div>
        </div>
      </Container>
    </>
  );
};

export default memo(HomePage);

結論

  • 調査したが、失敗に終わりました。m( ) m