3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[Elm] AlertmanagerにDatePickerを導入した話

Last updated at Posted at 2020-07-29

Web UIを作る時に何かと需要があるdate picker
最近ElmでDatePicker触れる機会があったので、PurometheusのAlertmanagerにもdatepicker入った方が良くない?
と思って入れてみた。

ElmのDatePicker

DatePickerについては
Elmで日付を扱うならとりあえずこれ、なパッケージたち。
この記事が分かりやすくまとまってたので、参考にさせてもらった。

話の前段として、Elmで自由にカスタマイズできるDatePickerは少ない。というよりほぼ無い。
cssがelm-cssで埋め込まれていたり、入力にいinput fieldしか使えなかったり、、
既存のシステムに組み込むにはかなり怠いこと間違いなし。

CurrySoftware/elm-datepicker

https://package.elm-lang.org/packages/CurrySoftware/elm-datepicker/latest
色々調べて一番良さそうだったのがこれ。
見た目が何よりもシンプル。そしてカスタマイズがしやすい。
↓な感じで、AttributesやFormatを自分で定義できるからほぼ思い通りのDatePickerが表示できるのが魅力。


type alias Settings =
    { placeholder : String
    , classNamespace : String
    , containerClassList : List ( String, Bool )
    , inputClassList : List ( String, Bool )
    , inputName : Maybe String
    , inputId : Maybe String
    , inputAttributes : List (Attribute Msg)
    , isDisabled : Date -> Bool
    , parser : String -> Result String Date
    , dateFormatter : Date -> String
    , dayFormatter : Weekday -> String
    , monthFormatter : Month -> String
    , yearFormatter : Int -> String
    , cellFormatter : String -> Html Msg
    , firstDayOfWeek : Weekday
    , changeYear : YearRange
    }

割と重要なのが、Posixで全部管理されていること。
Elmで日付を扱う0.19編
の記事にあるように最近は標準のdateパッケージが無い。
ので、できるだけ出所不明のパッケージに依存しないという意味で最高。

基本的にはelm/timetime-extraで事足りるけど、いろんなdateパッケージが出てて何使って良いのやらって感じ。
Elmerの人たちはDateパッケージの導入嫌うよね。独自のDate型に依存するくらいなら、時間は全部Posixで管理しようということなのだろう(と思う。この辺は正直分からない。)

というわけで 「CurrySoftware/elm-datepicker」をalertmanagerに入れてPR投げてみる。
https://github.com/prometheus/alertmanager/pull/2262

82245921-c946a280-997e-11ea-89a6-d2ad73c65059.gif

が、速攻でTimePickerもクレとのレスがついた

I guess it would be good to support time too but probably it isn't that easy (at least the component supports only dates)?

Time Pickerってそんなに必要じゃなく無い?
個人的には時間まできたら手入力で良いと思ってる。

が、あった方が便利なのは間違いないので、今度はDateTimePickerを探す

ElmのDateTimePicker

DateTimePickerとなるとグッと選択肢が狭まる。
これが最高!というものは無いが、良さそうなのはいくつか。

mercurymedia/elm-datetime-picker

最初にw0rmにお勧めされたのがこれ
https://package.elm-lang.org/packages/mercurymedia/elm-datetime-picker/latest/
DateTimePickerとしては申し分ない作り。最高だね。これ使おう。
と思ったが、
Dateを選択するとtimeが0クリアされる仕様
人によってどう思うか違うところだが、せっかく設定した時間が日付変更で0クリアされるのは個人的には頂けない。

他のも探してみる。

PanagiotisGeorgiadis/elm-datepicker

次点で良さそうなのが
PanagiotisGeorgiadis/elm-datepicker
見た目もカッコいいし、duration pickerがついてるからAlertmanagerにはあってるかな。
「せっかくなのでこれ使って、duration pickerにしよう」ということになったが、1点致命的な欠陥が。。。
Duration Pickerでは、開始日/終了日に同じ日付が選べない仕様
これは仕様なのか?と疑いたくなる様な挙動。Alertmanager的にはこれはNG。

色々あって、最終的には自分で作った方が早くね?となったのでした。

DateTimePickerを作る

コードはここ
https://github.com/prometheus/alertmanager/pull/2262/files
で以上なのだが、
意外と簡単にできたのでメモ。

DatePicker

肝になるのが下のアップデート文。DatePickerに関してはほぼこれが全て


-- UPDATE
....
        MouseOverDay time ->
            { dateTimePicker | mouseOverDay = Just time }

        ClearMouseOverDay ->
            { dateTimePicker | mouseOverDay = Nothing }

        OnClickDay ->
            let
                addDateTime_ : Posix -> Maybe Posix -> Posix
                addDateTime_ date maybeTime =
                    case maybeTime of
                        Just time ->
                            floorDate date
                                |> Time.posixToMillis
                                |> (\d ->
                                        trimTime time
                                            |> Time.posixToMillis
                                            |> (\t -> d + t)
                                   )
                                |> Time.millisToPosix

                        Nothing ->
                            floorDate date

                updateTime_ : Maybe Posix -> Maybe Posix -> Maybe Posix
                updateTime_ maybeDate maybeTime =
                    case maybeDate of
                        Just date ->
                            Just <| addDateTime_ date maybeTime

                        Nothing ->
                            maybeTime

                ( startDate, endDate ) =
                    case dateTimePicker.mouseOverDay of
                        Just m ->
                            case ( dateTimePicker.startDate, dateTimePicker.endDate ) of
                                ( Nothing, Nothing ) ->
                                    ( Just m
                                    , Nothing
                                    )

                                ( Just start, Nothing ) ->
                                    case
                                        compare (floorDate m |> Time.posixToMillis)
                                            (floorDate start |> Time.posixToMillis)
                                    of
                                        LT ->
                                            ( Just m
                                            , Just start
                                            )

                                        _ ->
                                            ( Just start
                                            , Just m
                                            )

                                ( Nothing, Just end ) ->
                                    ( Just m
                                    , Just end
                                    )

                                ( Just start, Just end ) ->
                                    ( Just m
                                    , Nothing
                                    )

                        _ ->
                            ( dateTimePicker.startDate
                            , dateTimePicker.endDate
                            )
            in
            { dateTimePicker
                | startDate = startDate
                , endDate = endDate
                , startTime = updateTime_ startDate dateTimePicker.startTime
                , endTime = updateTime_ endDate dateTimePicker.endTime
            }

-- VIEW

-- カレンダー1マス分のviewを抜粋
        [ div
            [ class ("date front" ++ mouseoverClass ++ startClass ++ endClass ++ thisMonthClass)
            , onMouseOver <| MouseOverDay day
            , onClick OnClickDay
            ]
            [ text (Time.toDay utc day |> String.fromInt) ]
        ]

MouseOverDay timeでマウスが置かれているマス(日付)をモデルmouseOverDayに記録する。
OnClickDayでクリックされた日付(mouseOverDayに設定された日付)をpick済みの日付としてセットする(startDate, endDate)。

あとは、Duration Pickerなので、クリックした2つの日付比べて、startDateにするかendDateにするかを長々と判定している。

TimePicker

TimePicker側はDateと明確に分けるためにstartTime,endTimeの入れ物を定義している。


-- UPDATE
....
        SetInputTime startOrEnd inputHourOrMinute num ->
            let
                limit_ : Int -> Int -> Int
                limit_ limit n =
                    if n < 0 then
                        0

                    else
                        modBy limit n

                updateHourOrMinute_ : InputHourOrMinute -> Posix -> Posix
                updateHourOrMinute_ ihom s =
                    case ihom of
                        InputHour ->
                            updateHour (limit_ 24 num) s

                        InputMinute ->
                            updateMinute (limit_ 60 num) s

                ( startTime, endTime ) =
                    setTime_ startOrEnd inputHourOrMinute updateHourOrMinute_
            in
            { dateTimePicker | startTime = startTime, endTime = endTime }

        IncrementTime startOrEnd inputHourOrMinute num ->
            let
                updateHourOrMinute_ : InputHourOrMinute -> Posix -> Posix
                updateHourOrMinute_ ihom s =
                    let
                        compare_ : Posix -> Posix
                        compare_ a =
                            if
                                (floorDate s |> Time.posixToMillis)
                                    == (floorDate a |> Time.posixToMillis)
                            then
                                a

                            else
                                s
                    in
                    case ihom of
                        InputHour ->
                            addHour num s
                                |> compare_

                        InputMinute ->
                            addMinute num s
                                |> compare_

                ( startTime, endTime ) =
                    setTime_ startOrEnd inputHourOrMinute updateHourOrMinute_
            in
            { dateTimePicker | startTime = startTime, endTime = endTime }

-- VIEW
        , div [ class "minute" ]
            [ button
                [ class "up-button d-flex-center"
                , onClick <| IncrementTime startOrEnd InputMinute 1
                ]
                [ i
                    [ class "fa fa-angle-up" ]
                    []
                ]
            , input
                [ on "blur" <| Decode.map (SetInputTime startOrEnd InputMinute) targetValueIntParse
                , value
                    (case startOrEnd of
                        Start ->
                            case dateTimePicker.startTime of
                                Just t ->
                                    Time.toMinute utc t |> String.fromInt

                                Nothing ->
                                    "0"

                        End ->
                            case dateTimePicker.endTime of
                                Just t ->
                                    Time.toMinute utc t |> String.fromInt

                                Nothing ->
                                    "0"
                    )
                , maxlength 2
                , class "view"
                ]
                []

ボタンクリックで時間をインクリメントしたい場合はIncrementTimeを、テキスト入力で時間を指定したい場合はSetInputTimeを、
という形で若干こだわったので長い。
全部Posixで管理するためには、Time.floorとかTime.posixToMillisとか頻繁に使ってDateとTimeの足算をしてあげないといけない。
この辺りは無駄にコードが長くなるので、全て別ファイルUtils.elmにまとめた。

というわけで完成品
87542666-5176c900-c6de-11ea-9a7b-64b11e2acf48.gif

色々とコードを直してもらったが無事にマージされた。
image.png

所感

ElmのDateTimePickerは外部ライブラリが多くあり、それなりに使えそう。自由にカスタマイズできるものは少ない。
どれも若干クセがあるので、選定の前に要件をはっきりさせておかないと路頭に迷う。
(↑はElmに限った話では無いが、どのライブラリもサクッと導入できる物ではないので、トライ&エラーで選ぶと労力が半端ない)

ElmでDatePickerを作るのはかなり簡単。気に入ったライブラリがなければ、自分で作るのは選択肢の1つだと思う。
DatePickerSDKとかDatePickerHelperとかが欲しい気分

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?