From b274baf67e27d79fd4e764607ded7c5aa755ee8b Mon Sep 17 00:00:00 2001
From: cpathipa <119517080+cpathipa@users.noreply.github.com>
Date: Wed, 19 Jun 2024 09:06:48 -0500
Subject: [PATCH 01/35] unit test coverage for HostNameTableCell
---
.../AccessKeyTable/HostNameTableCell.test.tsx | 115 ++++++++++++++++++
.../AccessKeyTable/HostNameTableCell.tsx | 17 +--
2 files changed, 124 insertions(+), 8 deletions(-)
create mode 100644 packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/HostNameTableCell.test.tsx
diff --git a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/HostNameTableCell.test.tsx b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/HostNameTableCell.test.tsx
new file mode 100644
index 00000000000..92c072ca4e9
--- /dev/null
+++ b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/HostNameTableCell.test.tsx
@@ -0,0 +1,115 @@
+import '@testing-library/jest-dom';
+import { waitFor } from '@testing-library/react';
+import React from 'react';
+
+import { regionFactory } from 'src/factories';
+import { makeResourcePage } from 'src/mocks/serverHandlers';
+import { HttpResponse, http, server } from 'src/mocks/testServer';
+import { renderWithThemeAndHookFormContext } from 'src/utilities/testHelpers';
+
+import { HostNameTableCell } from './HostNameTableCell';
+
+const storageKeyData = {
+ access_key: 'test_key',
+ bucket_access: null,
+ id: 12345,
+ label: 'this is regular key',
+ limited: false,
+ regions: [
+ {
+ id: 'us-east',
+ s3_endpoint: 'alpha.test.com',
+ },
+ ],
+ secret_key: '[test]',
+};
+
+describe('HostNameTableCell', () => {
+ it('should render "None" when there are no regions', () => {
+ const { getByText } = renderWithThemeAndHookFormContext({
+ component: (
+
+ ),
+ });
+
+ expect(getByText('None')).toBeInTheDocument();
+ });
+
+ test('should render "Regions/S3 Hostnames" cell when there are regions', async () => {
+ const region = regionFactory.build({
+ capabilities: ['Object Storage'],
+ id: 'us-east',
+ label: 'Newark, NJ',
+ });
+
+ server.use(
+ http.get('*/v4/regions', () => {
+ return HttpResponse.json(makeResourcePage([region]));
+ })
+ );
+ const { findByText } = renderWithThemeAndHookFormContext({
+ component: (
+
+ ),
+ });
+
+ const hostname = await findByText('Newark, NJ: alpha.test.com');
+
+ await waitFor(() => expect(hostname).toBeInTheDocument());
+ });
+ test('should render all "Regions/S3 Hostnames" in the cell when there are multiple regions', async () => {
+ const region = regionFactory.build({
+ capabilities: ['Object Storage'],
+ id: 'us-east',
+ label: 'Newark, NJ',
+ });
+
+ server.use(
+ http.get('*/v4/regions', () => {
+ return HttpResponse.json(makeResourcePage([region]));
+ })
+ );
+ const { findByText } = renderWithThemeAndHookFormContext({
+ component: (
+
+ ),
+ });
+ const hostname = await findByText('Newark, NJ: alpha.test.com');
+ const moreButton = await findByText(/and\s+1\s+more\.\.\./);
+ await waitFor(() => expect(hostname).toBeInTheDocument());
+
+ await expect(moreButton).toBeInTheDocument();
+ });
+});
diff --git a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/HostNameTableCell.tsx b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/HostNameTableCell.tsx
index e5fb3ce88db..644e2558d5d 100644
--- a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/HostNameTableCell.tsx
+++ b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/HostNameTableCell.tsx
@@ -1,7 +1,3 @@
-import {
- ObjectStorageKey,
- RegionS3EndpointAndID,
-} from '@linode/api-v4/lib/object-storage';
import { styled } from '@mui/material/styles';
import React from 'react';
@@ -11,6 +7,11 @@ import { TableCell } from 'src/components/TableCell';
import { useRegionsQuery } from 'src/queries/regions/regions';
import { getRegionsByRegionId } from 'src/utilities/regions';
+import type {
+ ObjectStorageKey,
+ RegionS3EndpointAndID,
+} from '@linode/api-v4/lib/object-storage';
+
type Props = {
setHostNames: (hostNames: RegionS3EndpointAndID[]) => void;
setShowHostNamesDrawers: (show: boolean) => void;
@@ -31,14 +32,14 @@ export const HostNameTableCell = ({
if (!regionsLookup || !regionsData || !regions || regions.length === 0) {
return None;
}
+ const label = regionsLookup[storageKeyData.regions[0].id]?.label;
+ const s3endPoint = storageKeyData?.regions[0]?.s3_endpoint;
return (
- {`${regionsLookup[storageKeyData.regions[0].id].label}: ${
- storageKeyData?.regions[0]?.s3_endpoint
- } `}
+ {`${label}: ${s3endPoint} `}
{storageKeyData?.regions?.length === 1 && (
-
+
)}
{storageKeyData.regions.length > 1 && (
Date: Wed, 19 Jun 2024 09:09:10 -0500
Subject: [PATCH 02/35] Revert "unit test coverage for HostNameTableCell"
This reverts commit b274baf67e27d79fd4e764607ded7c5aa755ee8b.
---
.../AccessKeyTable/HostNameTableCell.test.tsx | 115 ------------------
.../AccessKeyTable/HostNameTableCell.tsx | 17 ++-
2 files changed, 8 insertions(+), 124 deletions(-)
delete mode 100644 packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/HostNameTableCell.test.tsx
diff --git a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/HostNameTableCell.test.tsx b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/HostNameTableCell.test.tsx
deleted file mode 100644
index 92c072ca4e9..00000000000
--- a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/HostNameTableCell.test.tsx
+++ /dev/null
@@ -1,115 +0,0 @@
-import '@testing-library/jest-dom';
-import { waitFor } from '@testing-library/react';
-import React from 'react';
-
-import { regionFactory } from 'src/factories';
-import { makeResourcePage } from 'src/mocks/serverHandlers';
-import { HttpResponse, http, server } from 'src/mocks/testServer';
-import { renderWithThemeAndHookFormContext } from 'src/utilities/testHelpers';
-
-import { HostNameTableCell } from './HostNameTableCell';
-
-const storageKeyData = {
- access_key: 'test_key',
- bucket_access: null,
- id: 12345,
- label: 'this is regular key',
- limited: false,
- regions: [
- {
- id: 'us-east',
- s3_endpoint: 'alpha.test.com',
- },
- ],
- secret_key: '[test]',
-};
-
-describe('HostNameTableCell', () => {
- it('should render "None" when there are no regions', () => {
- const { getByText } = renderWithThemeAndHookFormContext({
- component: (
-
- ),
- });
-
- expect(getByText('None')).toBeInTheDocument();
- });
-
- test('should render "Regions/S3 Hostnames" cell when there are regions', async () => {
- const region = regionFactory.build({
- capabilities: ['Object Storage'],
- id: 'us-east',
- label: 'Newark, NJ',
- });
-
- server.use(
- http.get('*/v4/regions', () => {
- return HttpResponse.json(makeResourcePage([region]));
- })
- );
- const { findByText } = renderWithThemeAndHookFormContext({
- component: (
-
- ),
- });
-
- const hostname = await findByText('Newark, NJ: alpha.test.com');
-
- await waitFor(() => expect(hostname).toBeInTheDocument());
- });
- test('should render all "Regions/S3 Hostnames" in the cell when there are multiple regions', async () => {
- const region = regionFactory.build({
- capabilities: ['Object Storage'],
- id: 'us-east',
- label: 'Newark, NJ',
- });
-
- server.use(
- http.get('*/v4/regions', () => {
- return HttpResponse.json(makeResourcePage([region]));
- })
- );
- const { findByText } = renderWithThemeAndHookFormContext({
- component: (
-
- ),
- });
- const hostname = await findByText('Newark, NJ: alpha.test.com');
- const moreButton = await findByText(/and\s+1\s+more\.\.\./);
- await waitFor(() => expect(hostname).toBeInTheDocument());
-
- await expect(moreButton).toBeInTheDocument();
- });
-});
diff --git a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/HostNameTableCell.tsx b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/HostNameTableCell.tsx
index 644e2558d5d..e5fb3ce88db 100644
--- a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/HostNameTableCell.tsx
+++ b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/HostNameTableCell.tsx
@@ -1,3 +1,7 @@
+import {
+ ObjectStorageKey,
+ RegionS3EndpointAndID,
+} from '@linode/api-v4/lib/object-storage';
import { styled } from '@mui/material/styles';
import React from 'react';
@@ -7,11 +11,6 @@ import { TableCell } from 'src/components/TableCell';
import { useRegionsQuery } from 'src/queries/regions/regions';
import { getRegionsByRegionId } from 'src/utilities/regions';
-import type {
- ObjectStorageKey,
- RegionS3EndpointAndID,
-} from '@linode/api-v4/lib/object-storage';
-
type Props = {
setHostNames: (hostNames: RegionS3EndpointAndID[]) => void;
setShowHostNamesDrawers: (show: boolean) => void;
@@ -32,14 +31,14 @@ export const HostNameTableCell = ({
if (!regionsLookup || !regionsData || !regions || regions.length === 0) {
return None;
}
- const label = regionsLookup[storageKeyData.regions[0].id]?.label;
- const s3endPoint = storageKeyData?.regions[0]?.s3_endpoint;
return (
- {`${label}: ${s3endPoint} `}
+ {`${regionsLookup[storageKeyData.regions[0].id].label}: ${
+ storageKeyData?.regions[0]?.s3_endpoint
+ } `}
{storageKeyData?.regions?.length === 1 && (
-
+
)}
{storageKeyData.regions.length > 1 && (
Date: Thu, 26 Sep 2024 14:38:58 -0400
Subject: [PATCH 03/35] chore: [M3-8662] - Update Github Actions actions
(#11009)
* update actions
* add changeset
---------
Co-authored-by: Banks Nussman
---
.github/workflows/ci.yml | 161 ++++++++----------
.github/workflows/coverage.yml | 16 +-
.github/workflows/coverage_badge.yml | 6 +-
.github/workflows/coverage_comment.yml | 4 +-
.github/workflows/docs.yml | 10 +-
.github/workflows/e2e_schedule_and_push.yml | 8 +-
.github/workflows/security_scan.yml | 2 +-
.../pr-11009-tech-stories-1727310949305.md | 5 +
8 files changed, 102 insertions(+), 110 deletions(-)
create mode 100644 packages/manager/.changeset/pr-11009-tech-stories-1727310949305.md
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 014410c88cb..962897a5748 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -15,11 +15,11 @@ jobs:
package: ["linode-manager", "@linode/api-v4", "@linode/validation"]
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
- - uses: actions/setup-node@v3
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
with:
node-version: "20.17"
- - uses: actions/cache@v3
+ - uses: actions/cache@v4
with:
path: |
**/node_modules
@@ -30,35 +30,32 @@ jobs:
build-validation:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
- - uses: actions/setup-node@v3
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
with:
node-version: "20.17"
- - uses: actions/cache@v3
+ - uses: actions/cache@v4
with:
path: |
**/node_modules
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
- run: yarn --frozen-lockfile
- run: yarn workspace @linode/validation run build
- - uses: actions/upload-artifact@v3
+ - uses: actions/upload-artifact@v4
with:
name: packages-validation-lib
- path: |
- packages/validation/index.js
- packages/validation/lib
+ path: packages/validation/lib
publish-validation:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/master'
- needs:
- - build-validation
+ needs: build-validation
steps:
- - uses: actions/checkout@v2
- - uses: actions/download-artifact@v2
+ - uses: actions/checkout@v4
+ - uses: actions/download-artifact@v4
with:
name: packages-validation-lib
- path: packages/validation
+ path: packages/validation/lib
- uses: JS-DevTools/npm-publish@v1
id: npm-publish
with:
@@ -80,69 +77,64 @@ jobs:
runs-on: ubuntu-latest
needs: build-validation
steps:
- - uses: actions/checkout@v3
- - uses: actions/setup-node@v3
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
with:
node-version: "20.17"
- - uses: actions/cache@v3
+ - uses: actions/cache@v4
with:
path: |
**/node_modules
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
- run: yarn --frozen-lockfile
- - uses: actions/download-artifact@v3
+ - uses: actions/download-artifact@v4
with:
name: packages-validation-lib
- path: packages/validation
+ path: packages/validation/lib
- run: yarn workspace @linode/api-v4 run test
build-sdk:
runs-on: ubuntu-latest
- needs:
- - build-validation
+ needs: build-validation
steps:
- - uses: actions/checkout@v3
- - uses: actions/setup-node@v3
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
with:
node-version: "20.17"
- - uses: actions/cache@v3
+ - uses: actions/cache@v4
with:
path: |
**/node_modules
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
- - uses: actions/download-artifact@v3
+ - uses: actions/download-artifact@v4
with:
name: packages-validation-lib
- path: packages/validation
+ path: packages/validation/lib
- run: yarn --frozen-lockfile
- run: yarn workspace @linode/api-v4 run build
- - uses: actions/upload-artifact@v3
+ - uses: actions/upload-artifact@v4
with:
name: packages-api-v4-lib
- path: |
- packages/api-v4/index.js
- packages/api-v4/index.node.js
- packages/api-v4/lib
+ path: packages/api-v4/lib
validate-sdk:
runs-on: ubuntu-latest
- needs:
- - build-sdk
+ needs: build-sdk
steps:
- - uses: actions/checkout@v3
- - uses: actions/setup-node@v3
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
with:
node-version: "20.17"
# Download the validation and api-v4 artifacts (built packages)
- - uses: actions/download-artifact@v3
+ - uses: actions/download-artifact@v4
with:
name: packages-validation-lib
- path: packages/validation
- - uses: actions/download-artifact@v3
+ path: packages/validation/lib
+ - uses: actions/download-artifact@v4
with:
name: packages-api-v4-lib
- path: packages/api-v4
+ path: packages/api-v4/lib
# Create an api-v4 tarball
- run: cd packages/api-v4 && npm pack --pack-destination ../../
@@ -162,37 +154,36 @@ jobs:
test-manager:
runs-on: ubuntu-latest
- needs:
- - build-sdk
+ needs: build-sdk
steps:
- - uses: actions/checkout@v3
- - uses: actions/setup-node@v3
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
with:
node-version: "20.17"
- - uses: actions/cache@v3
+ - uses: actions/cache@v4
with:
path: |
**/node_modules
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
- - uses: actions/download-artifact@v3
+ - uses: actions/download-artifact@v4
with:
name: packages-validation-lib
- path: packages/validation
- - uses: actions/download-artifact@v3
+ path: packages/validation/lib
+ - uses: actions/download-artifact@v4
with:
name: packages-api-v4-lib
- path: packages/api-v4
+ path: packages/api-v4/lib
- run: yarn --frozen-lockfile
- run: yarn workspace linode-manager run test
test-search:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
- - uses: actions/setup-node@v3
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
with:
node-version: "20.17"
- - uses: actions/cache@v3
+ - uses: actions/cache@v4
with:
path: |
**/node_modules
@@ -202,55 +193,53 @@ jobs:
typecheck-manager:
runs-on: ubuntu-latest
- needs:
- - build-sdk
+ needs: build-sdk
steps:
- - uses: actions/checkout@v3
- - uses: actions/setup-node@v3
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
with:
node-version: "20.17"
- - uses: actions/cache@v3
+ - uses: actions/cache@v4
with:
path: |
**/node_modules
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
- - uses: actions/download-artifact@v3
+ - uses: actions/download-artifact@v4
with:
name: packages-validation-lib
- path: packages/validation
- - uses: actions/download-artifact@v3
+ path: packages/validation/lib
+ - uses: actions/download-artifact@v4
with:
name: packages-api-v4-lib
- path: packages/api-v4
+ path: packages/api-v4/lib
- run: yarn --frozen-lockfile
- run: yarn workspace linode-manager run typecheck
build-manager:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/master'
- needs:
- - build-sdk
+ needs: build-sdk
steps:
- - uses: actions/checkout@v3
- - uses: actions/setup-node@v3
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
with:
node-version: "20.17"
- - uses: actions/cache@v3
+ - uses: actions/cache@v4
with:
path: |
**/node_modules
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
- - uses: actions/download-artifact@v3
+ - uses: actions/download-artifact@v4
with:
name: packages-validation-lib
- path: packages/validation
+ path: packages/validation/lib
- uses: actions/download-artifact@v3
with:
name: packages-api-v4-lib
- path: packages/api-v4
+ path: packages/api-v4/lib
- run: yarn --frozen-lockfile
- run: yarn workspace linode-manager run build
- - uses: actions/upload-artifact@v3
+ - uses: actions/upload-artifact@v4
with:
name: packages-manager-build
path: packages/manager/build
@@ -264,11 +253,11 @@ jobs:
# If the validation publish failed we could have mismatched versions and a broken JS client
- publish-validation
steps:
- - uses: actions/checkout@v3
- - uses: actions/download-artifact@v3
+ - uses: actions/checkout@v4
+ - uses: actions/download-artifact@v4
with:
name: packages-api-v4-lib
- path: packages/api-v4
+ path: packages/api-v4/lib
- uses: JS-DevTools/npm-publish@v1
id: npm-publish
with:
@@ -288,31 +277,30 @@ jobs:
build-storybook:
runs-on: ubuntu-latest
- needs:
- - build-sdk
+ needs: build-sdk
env:
NODE_OPTIONS: --max-old-space-size=4096
steps:
- - uses: actions/checkout@v3
- - uses: actions/setup-node@v3
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
with:
node-version: "20.17"
- - uses: actions/cache@v3
+ - uses: actions/cache@v4
with:
path: |
**/node_modules
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
- - uses: actions/download-artifact@v3
+ - uses: actions/download-artifact@v4
with:
name: packages-validation-lib
- path: packages/validation
- - uses: actions/download-artifact@v3
+ path: packages/validation/lib
+ - uses: actions/download-artifact@v4
with:
name: packages-api-v4-lib
- path: packages/api-v4
+ path: packages/api-v4/lib
- run: yarn --frozen-lockfile
- run: yarn workspace linode-manager run build-storybook
- - uses: actions/upload-artifact@v3
+ - uses: actions/upload-artifact@v4
with:
name: storybook-build
path: packages/manager/storybook-static
@@ -320,11 +308,10 @@ jobs:
publish-storybook:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/master'
- needs:
- - build-storybook
+ needs: build-storybook
steps:
- - uses: actions/checkout@v3
- - uses: actions/download-artifact@v3
+ - uses: actions/checkout@v4
+ - uses: actions/download-artifact@v4
with:
name: storybook-build
path: storybook/build
diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
index 84399b8323f..144f802594d 100644
--- a/.github/workflows/coverage.yml
+++ b/.github/workflows/coverage.yml
@@ -7,16 +7,16 @@ jobs:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
with:
ref: ${{ github.base_ref }} # The base branch of the PR (develop)
- name: Use Node.js v20.17 LTS
- uses: actions/setup-node@v3
+ uses: actions/setup-node@v4
with:
node-version: "20.17"
- - uses: actions/cache@v3
+ - uses: actions/cache@v4
with:
path: |
**/node_modules
@@ -52,14 +52,14 @@ jobs:
needs: base_branch
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Use Node.js v20.17 LTS
- uses: actions/setup-node@v3
+ uses: actions/setup-node@v4
with:
node-version: "20.17"
- - uses: actions/cache@v3
+ - uses: actions/cache@v4
with:
path: |
**/node_modules
@@ -88,13 +88,13 @@ jobs:
echo "$pct" > current_code_coverage.txt
- name: Upload PR Number Artifact
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: pr_number
path: pr_number.txt
- name: Upload Current Coverage Artifact
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: current_code_coverage
path: current_code_coverage.txt
diff --git a/.github/workflows/coverage_badge.yml b/.github/workflows/coverage_badge.yml
index 0a73b196138..ca07bfd7f27 100644
--- a/.github/workflows/coverage_badge.yml
+++ b/.github/workflows/coverage_badge.yml
@@ -11,14 +11,14 @@ jobs:
steps:
- name: Checkout Code
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Use Node.js v20.17 LTS
- uses: actions/setup-node@v3
+ uses: actions/setup-node@v4
with:
node-version: "20.17"
- - uses: actions/cache@v3
+ - uses: actions/cache@v4
with:
path: |
**/node_modules
diff --git a/.github/workflows/coverage_comment.yml b/.github/workflows/coverage_comment.yml
index 07fe3682a9b..8ddd29c1ce6 100644
--- a/.github/workflows/coverage_comment.yml
+++ b/.github/workflows/coverage_comment.yml
@@ -15,10 +15,10 @@ jobs:
steps:
- name: Checkout Code
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Use Node.js v20.17 LTS
- uses: actions/setup-node@v3
+ uses: actions/setup-node@v4
with:
node-version: "20.17"
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 0f73e3e58b6..4a81cf374f9 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -13,12 +13,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Setup Pages
- uses: actions/configure-pages@v3
+ uses: actions/configure-pages@v5
- - uses: oven-sh/setup-bun@v1
+ - uses: oven-sh/setup-bun@v2
with:
bun-version: 1.0.21
@@ -26,7 +26,7 @@ jobs:
run: bunx vitepress@1.0.0-rc.35 build docs
- name: Upload artifact
- uses: actions/upload-pages-artifact@v2
+ uses: actions/upload-pages-artifact@v3
with:
path: docs/.vitepress/dist
@@ -40,4 +40,4 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deployment
- uses: actions/deploy-pages@v2
\ No newline at end of file
+ uses: actions/deploy-pages@v4
diff --git a/.github/workflows/e2e_schedule_and_push.yml b/.github/workflows/e2e_schedule_and_push.yml
index 61176d8fb15..63578c1a25c 100644
--- a/.github/workflows/e2e_schedule_and_push.yml
+++ b/.github/workflows/e2e_schedule_and_push.yml
@@ -26,11 +26,11 @@ jobs:
steps:
- name: install command line utilities
run: sudo apt-get install -y expect
- - uses: actions/checkout@v3
- - uses: actions/setup-node@v3
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
with:
node-version: "20.17"
- - uses: actions/cache@v3
+ - uses: actions/cache@v4
with:
path: |
node_modules
@@ -60,7 +60,7 @@ jobs:
yarn build
yarn start:manager:ci &
- name: Run tests
- uses: cypress-io/github-action@v5
+ uses: cypress-io/github-action@v6
with:
working-directory: packages/manager
wait-on: "http://localhost:3000"
diff --git a/.github/workflows/security_scan.yml b/.github/workflows/security_scan.yml
index a7cb1b14fbe..37ed12a241f 100644
--- a/.github/workflows/security_scan.yml
+++ b/.github/workflows/security_scan.yml
@@ -12,7 +12,7 @@ jobs:
container:
image: returntocorp/semgrep
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
# Perform scanning using Semgrep
# Pass even when it identifies issues or encounters errors.
diff --git a/packages/manager/.changeset/pr-11009-tech-stories-1727310949305.md b/packages/manager/.changeset/pr-11009-tech-stories-1727310949305.md
new file mode 100644
index 00000000000..4ad6da2dfc5
--- /dev/null
+++ b/packages/manager/.changeset/pr-11009-tech-stories-1727310949305.md
@@ -0,0 +1,5 @@
+---
+"@linode/manager": Tech Stories
+---
+
+Update Github Actions actions ([#11009](https://github.com/linode/manager/pull/11009))
From 4e25a942a09fef0e1ab56ea976ed8d6a8e39aef9 Mon Sep 17 00:00:00 2001
From: cpathipa <119517080+cpathipa@users.noreply.github.com>
Date: Wed, 23 Oct 2024 11:37:28 -0500
Subject: [PATCH 04/35] Basci date picker component
---
.../src/components/DatePicker/DatePicker.tsx | 81 +++++++++++++++++++
1 file changed, 81 insertions(+)
create mode 100644 packages/manager/src/components/DatePicker/DatePicker.tsx
diff --git a/packages/manager/src/components/DatePicker/DatePicker.tsx b/packages/manager/src/components/DatePicker/DatePicker.tsx
new file mode 100644
index 00000000000..9a1aa35f26d
--- /dev/null
+++ b/packages/manager/src/components/DatePicker/DatePicker.tsx
@@ -0,0 +1,81 @@
+import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon';
+import { DatePicker as MuiDatePicker } from '@mui/x-date-pickers/DatePicker';
+import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
+import React, { useState } from 'react';
+
+import { TextField } from 'src/components/TextField';
+
+import type { DateTime } from 'luxon';
+import type { TextFieldProps } from 'src/components/TextField';
+
+interface DatePickerProps {
+ /** If true, disables selecting future dates. Defaults to false. */
+ disableFuture?: boolean;
+ /** If true, disables selecting past dates. Defaults to false. */
+ disablePast?: boolean;
+ /** Error text to display below the input */
+ errorText?: string;
+ /** Helper text to display below the input */
+ helperText?: string;
+ /** Label to display for the date picker input */
+ label?: string;
+ /** Callback function fired when the value changes */
+ onChange?: (newDate: DateTime | null) => void;
+ /** Placeholder text for the date picker input */
+ placeholder?: string;
+ /** Additional props to pass to the underlying TextField component */
+ textFieldProps?: Omit;
+ /** The currently selected date */
+ value?: DateTime | null;
+}
+
+export const DatePicker = ({
+ disableFuture = false,
+ disablePast = false,
+ errorText,
+ helperText,
+ label = 'Select a date',
+ onChange,
+ placeholder = 'Select a date',
+ textFieldProps,
+ value,
+ ...props
+}: DatePickerProps) => {
+ const [selectedDate, setSelectedDate] = useState(
+ value || null
+ );
+
+ const onChangeHandler = (newDate: DateTime | null) => {
+ setSelectedDate(newDate);
+ if (onChange) {
+ onChange(newDate);
+ }
+ };
+
+ return (
+
+ (
+
+ ),
+ },
+ }}
+ />
+
+ );
+};
From 423f4b7d1643c4d0a91ca4932f3ac40765d45f59 Mon Sep 17 00:00:00 2001
From: cpathipa <119517080+cpathipa@users.noreply.github.com>
Date: Fri, 25 Oct 2024 14:19:53 -0500
Subject: [PATCH 05/35] Test coverage for date picker component
---
.../components/DatePicker/DatePicker.test.tsx | 80 +++++++++++++++++++
.../src/components/DatePicker/DatePicker.tsx | 53 ++++++------
2 files changed, 103 insertions(+), 30 deletions(-)
create mode 100644 packages/manager/src/components/DatePicker/DatePicker.test.tsx
diff --git a/packages/manager/src/components/DatePicker/DatePicker.test.tsx b/packages/manager/src/components/DatePicker/DatePicker.test.tsx
new file mode 100644
index 00000000000..e051d160ec5
--- /dev/null
+++ b/packages/manager/src/components/DatePicker/DatePicker.test.tsx
@@ -0,0 +1,80 @@
+import { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { DateTime } from 'luxon';
+import * as React from 'react';
+
+import { renderWithTheme } from 'src/utilities/testHelpers';
+
+import { DatePicker } from './DatePicker';
+
+import type { DatePickerProps } from './DatePicker';
+
+const props: DatePickerProps = {
+ onChange: vi.fn(),
+ placeholder: 'Pick a date',
+ textFieldProps: { errorText: 'Invalid date', label: 'Select a date' },
+ value: null,
+};
+
+describe('DatePicker', () => {
+ it('should render the DatePicker component', () => {
+ renderWithTheme();
+ const DatePickerField = screen.getByRole('textbox', {
+ name: 'Select a date',
+ });
+
+ expect(DatePickerField).toBeVisible();
+ });
+
+ it('should handle value changes', async () => {
+ renderWithTheme();
+
+ const calendarButton = screen.getByRole('button', { name: 'Choose date' });
+
+ // Click the calendar button to open the date picker
+ await userEvent.click(calendarButton);
+
+ // Find a date button to click (e.g., the 15th of the month)
+ const dateToSelect = screen.getByRole('gridcell', { name: '15' });
+ await userEvent.click(dateToSelect);
+
+ // Check if onChange was called after selecting a date
+ expect(props.onChange).toHaveBeenCalled();
+ });
+
+ it('should display the error text when provided', () => {
+ renderWithTheme();
+ const errorMessage = screen.getByText('Invalid date');
+ expect(errorMessage).toBeVisible();
+ });
+
+ it('should display the helper text when provided', () => {
+ renderWithTheme();
+ const helperText = screen.getByText('Choose a valid date');
+ expect(helperText).toBeVisible();
+ });
+
+ it('should use the default format when no format is specified', () => {
+ renderWithTheme(
+
+ );
+ const datePickerField = screen.getByRole('textbox', {
+ name: 'Select a date',
+ });
+ expect(datePickerField).toHaveValue('2024-10-25');
+ });
+
+ it('should handle the custom format correctly', () => {
+ renderWithTheme(
+
+ );
+ const datePickerField = screen.getByRole('textbox', {
+ name: 'Select a date',
+ });
+ expect(datePickerField).toHaveValue('25/10/2024');
+ });
+});
diff --git a/packages/manager/src/components/DatePicker/DatePicker.tsx b/packages/manager/src/components/DatePicker/DatePicker.tsx
index 9a1aa35f26d..89e26d7afff 100644
--- a/packages/manager/src/components/DatePicker/DatePicker.tsx
+++ b/packages/manager/src/components/DatePicker/DatePicker.tsx
@@ -1,20 +1,20 @@
import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon';
import { DatePicker as MuiDatePicker } from '@mui/x-date-pickers/DatePicker';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
-import React, { useState } from 'react';
+import React from 'react';
import { TextField } from 'src/components/TextField';
+import type { DatePickerProps as MuiDatePickerProps } from '@mui/x-date-pickers/DatePicker';
import type { DateTime } from 'luxon';
import type { TextFieldProps } from 'src/components/TextField';
-interface DatePickerProps {
- /** If true, disables selecting future dates. Defaults to false. */
- disableFuture?: boolean;
- /** If true, disables selecting past dates. Defaults to false. */
- disablePast?: boolean;
+export interface DatePickerProps
+ extends Omit, 'onChange' | 'value'> {
/** Error text to display below the input */
errorText?: string;
+ /** Format of the date when rendered in the input field. */
+ format?: string;
/** Helper text to display below the input */
helperText?: string;
/** Label to display for the date picker input */
@@ -30,23 +30,17 @@ interface DatePickerProps {
}
export const DatePicker = ({
- disableFuture = false,
- disablePast = false,
errorText,
- helperText,
+ format = 'yyyy-MM-dd',
+ helperText = '',
label = 'Select a date',
onChange,
- placeholder = 'Select a date',
+ placeholder = 'Pick a date',
textFieldProps,
- value,
+ value = null,
...props
}: DatePickerProps) => {
- const [selectedDate, setSelectedDate] = useState(
- value || null
- );
-
const onChangeHandler = (newDate: DateTime | null) => {
- setSelectedDate(newDate);
if (onChange) {
onChange(newDate);
}
@@ -55,26 +49,25 @@ export const DatePicker = ({
return (
(
-
- ),
+ ...textFieldProps,
+ InputProps: {
+ ...textFieldProps?.InputProps,
+ },
+ error: Boolean(errorText),
+ helperText,
+ label,
+ placeholder,
},
}}
+ slots={{
+ textField: TextField, // Use custom TextField
+ }}
/>
);
From f87fbce3cbbd325a357dfe466d56670644e6ae64 Mon Sep 17 00:00:00 2001
From: cpathipa <119517080+cpathipa@users.noreply.github.com>
Date: Thu, 31 Oct 2024 09:54:24 -0500
Subject: [PATCH 06/35] DatePicker Stories
---
.../DatePicker/DatePicker.stories.tsx | 43 +++++++++++++++++++
1 file changed, 43 insertions(+)
create mode 100644 packages/manager/src/components/DatePicker/DatePicker.stories.tsx
diff --git a/packages/manager/src/components/DatePicker/DatePicker.stories.tsx b/packages/manager/src/components/DatePicker/DatePicker.stories.tsx
new file mode 100644
index 00000000000..db3925b03eb
--- /dev/null
+++ b/packages/manager/src/components/DatePicker/DatePicker.stories.tsx
@@ -0,0 +1,43 @@
+import { action } from '@storybook/addon-actions';
+import { DateTime } from 'luxon';
+import * as React from 'react';
+
+import { DatePicker } from './DatePicker';
+
+import type { Meta, StoryObj } from '@storybook/react';
+
+type Story = StoryObj;
+
+export const ControlledExample: Story = {
+ render: () => {
+ const ControlledDatePicker = () => {
+ const [selectedDate, setSelectedDate] = React.useState(
+ DateTime.now()
+ );
+
+ const handleChange = (newDate: DateTime | null) => {
+ setSelectedDate(newDate);
+ action('Controlled date change')(newDate?.toISO());
+ };
+
+ return (
+
+ );
+ };
+
+ return ;
+ },
+};
+
+const meta: Meta = {
+ component: DatePicker,
+ title: 'Components/DatePicker',
+};
+
+export default meta;
From f47f91948da1fe48fc73890e90f9edfbf9232316 Mon Sep 17 00:00:00 2001
From: cpathipa <119517080+cpathipa@users.noreply.github.com>
Date: Thu, 31 Oct 2024 10:00:05 -0500
Subject: [PATCH 07/35] Custom DateTimePicker component
---
.../src/components/DatePicker/DatePicker.tsx | 4 +-
.../DatePicker/DateTimePicker.test.tsx | 138 ++++++++++++
.../components/DatePicker/DateTimePicker.tsx | 199 ++++++++++++++++++
3 files changed, 339 insertions(+), 2 deletions(-)
create mode 100644 packages/manager/src/components/DatePicker/DateTimePicker.test.tsx
create mode 100644 packages/manager/src/components/DatePicker/DateTimePicker.tsx
diff --git a/packages/manager/src/components/DatePicker/DatePicker.tsx b/packages/manager/src/components/DatePicker/DatePicker.tsx
index 89e26d7afff..006bcb426d9 100644
--- a/packages/manager/src/components/DatePicker/DatePicker.tsx
+++ b/packages/manager/src/components/DatePicker/DatePicker.tsx
@@ -30,7 +30,6 @@ export interface DatePickerProps
}
export const DatePicker = ({
- errorText,
format = 'yyyy-MM-dd',
helperText = '',
label = 'Select a date',
@@ -59,14 +58,15 @@ export const DatePicker = ({
InputProps: {
...textFieldProps?.InputProps,
},
- error: Boolean(errorText),
helperText,
label,
placeholder,
},
+ ...props?.slotProps,
}}
slots={{
textField: TextField, // Use custom TextField
+ ...props?.slots,
}}
/>
diff --git a/packages/manager/src/components/DatePicker/DateTimePicker.test.tsx b/packages/manager/src/components/DatePicker/DateTimePicker.test.tsx
new file mode 100644
index 00000000000..bc19b643ad3
--- /dev/null
+++ b/packages/manager/src/components/DatePicker/DateTimePicker.test.tsx
@@ -0,0 +1,138 @@
+import { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { DateTime } from 'luxon';
+import * as React from 'react';
+
+import { renderWithTheme } from 'src/utilities/testHelpers';
+
+import { DateTimePicker } from './DateTimePicker';
+
+import type { DateTimePickerProps } from './DateTimePicker';
+
+const defaultProps: DateTimePickerProps = {
+ label: 'Select Date and Time',
+ onApply: vi.fn(),
+ onCancel: vi.fn(),
+ onChange: vi.fn(),
+ placeholder: 'yyyy-MM-dd HH:mm',
+ value: DateTime.fromISO('2024-10-25T15:30:00'),
+};
+
+describe('DateTimePicker Component', () => {
+ it('should render the DateTimePicker component with the correct label and placeholder', () => {
+ renderWithTheme();
+ const textField = screen.getByRole('textbox', {
+ name: 'Select Date and Time',
+ });
+ expect(textField).toBeVisible();
+ expect(textField).toHaveAttribute('placeholder', 'yyyy-MM-dd HH:mm');
+ });
+
+ it('should open the Popover when the TextField is clicked', async () => {
+ renderWithTheme();
+ const textField = screen.getByRole('textbox', {
+ name: 'Select Date and Time',
+ });
+ await userEvent.click(textField);
+ expect(screen.getByRole('dialog')).toBeVisible(); // Verifying the Popover is open
+ });
+
+ it('should call onCancel when the Cancel button is clicked', async () => {
+ renderWithTheme();
+ await userEvent.click(
+ screen.getByRole('textbox', { name: 'Select Date and Time' })
+ );
+ const cancelButton = screen.getByRole('button', { name: /Cancel/i });
+ await userEvent.click(cancelButton);
+ expect(defaultProps.onCancel).toHaveBeenCalled();
+ });
+
+ it('should call onApply when the Apply button is clicked', async () => {
+ renderWithTheme();
+ await userEvent.click(
+ screen.getByRole('textbox', { name: 'Select Date and Time' })
+ );
+ const applyButton = screen.getByRole('button', { name: /Apply/i });
+ await userEvent.click(applyButton);
+ expect(defaultProps.onApply).toHaveBeenCalled();
+ expect(defaultProps.onChange).toHaveBeenCalledWith(expect.any(DateTime)); // Ensuring onChange was called with a DateTime object
+ });
+
+ it('should handle date changes correctly', async () => {
+ renderWithTheme();
+ await userEvent.click(
+ screen.getByRole('textbox', { name: 'Select Date and Time' })
+ );
+
+ // Simulate selecting a date (e.g., 15th of the month)
+ const dateButton = screen.getByRole('gridcell', { name: '15' });
+ await userEvent.click(dateButton);
+
+ // Check that the displayed value has been updated correctly (this assumes the date format)
+ expect(defaultProps.onChange).toHaveBeenCalled();
+ });
+
+ it.skip('should handle time changes correctly', async () => {
+ renderWithTheme();
+ await userEvent.click(
+ screen.getByRole('textbox', { name: 'Select Date and Time' })
+ );
+
+ // Simulate changing time in the TimePicker (e.g., setting time to 10:45)
+ const timeInput = screen.getByLabelText(/Select Time/i);
+ await userEvent.type(timeInput, '10:45');
+ expect(defaultProps.onChange).toHaveBeenCalled();
+ });
+
+ it('should handle timezone changes correctly', async () => {
+ const timezoneChangeMock = vi.fn(); // Create a mock function
+
+ const updatedProps = {
+ ...defaultProps,
+ timeZoneSelectProps: { onChange: timezoneChangeMock, value: 'UTC' },
+ };
+
+ renderWithTheme();
+
+ await userEvent.click(
+ screen.getByRole('textbox', { name: 'Select Date and Time' })
+ );
+
+ // Simulate selecting a timezone from the TimeZoneSelect
+ const timezoneInput = screen.getByPlaceholderText(/Choose a Timezone/i);
+ await userEvent.click(timezoneInput);
+
+ // Select a timezone from the dropdown options
+ await userEvent.click(
+ screen.getByRole('option', { name: '(GMT -11:00) Niue Time' })
+ );
+
+ // Click the Apply button to trigger the change
+ await userEvent.click(screen.getByRole('button', { name: 'Apply' }));
+
+ // Verify that the onChange function was called with the expected value
+ expect(timezoneChangeMock).toHaveBeenCalledWith('Pacific/Niue');
+ });
+
+ it('should display the error text when provided', () => {
+ renderWithTheme(
+
+ );
+ expect(screen.getByText(/Invalid date-time/i)).toBeVisible();
+ });
+
+ it('should format the date-time correctly when a custom format is provided', () => {
+ renderWithTheme(
+
+ );
+ const textField = screen.getByRole('textbox', {
+ name: 'Select Date and Time',
+ });
+
+ expect(textField).toHaveValue('25/10/2024 15:30');
+ });
+});
diff --git a/packages/manager/src/components/DatePicker/DateTimePicker.tsx b/packages/manager/src/components/DatePicker/DateTimePicker.tsx
new file mode 100644
index 00000000000..a77c4eae60d
--- /dev/null
+++ b/packages/manager/src/components/DatePicker/DateTimePicker.tsx
@@ -0,0 +1,199 @@
+import { Grid, Popover } from '@mui/material';
+import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon';
+import { DateCalendar } from '@mui/x-date-pickers/DateCalendar';
+import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
+import { TimePicker } from '@mui/x-date-pickers/TimePicker';
+import React, { useState } from 'react';
+
+import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel';
+import { Box } from 'src/components/Box';
+import { Divider } from 'src/components/Divider';
+import { TextField } from 'src/components/TextField';
+
+import { TimeZoneSelect } from './TimeZoneSelect';
+
+import type { DateCalendarProps } from '@mui/x-date-pickers/DateCalendar';
+import type { DateTime } from 'luxon';
+
+export interface DateTimePickerProps {
+ /** Additional props for the DateCalendar */
+ dateCalendarProps?: Partial>;
+ /** Error text for the date picker filed */
+ errorText?: string;
+ /** Format for displaying the date-time */
+ format?: string;
+ /** Label for the input field */
+ label?: string;
+ /** Callback when the "Apply" button is clicked */
+ onApply?: () => void;
+ /** Callback when the "Cancel" button is clicked */
+ onCancel?: () => void;
+ /** Callback when date-time changes */
+ onChange?: (dateTime: DateTime | null) => void;
+ /** Placeholder text for the input field */
+ placeholder?: string;
+ timeSelectProps?: {
+ label?: string;
+ onChange?: (time: string) => void;
+ value?: null | string;
+ };
+ /** Props for customizing the TimeZoneSelect component */
+ timeZoneSelectProps?: {
+ label?: string;
+ onChange?: (timezone: string) => void;
+ value?: null | string;
+ };
+ /** Initial or controlled dateTime value */
+ value?: DateTime | null;
+}
+
+export const DateTimePicker = ({
+ dateCalendarProps = {},
+ errorText = '',
+ format = 'yyyy-MM-dd HH:mm',
+ label = 'Select Date and Time',
+ onApply,
+ onCancel,
+ onChange,
+ placeholder = 'yyyy-MM-dd HH:mm',
+ timeSelectProps,
+ timeZoneSelectProps,
+ value = null,
+}: DateTimePickerProps) => {
+ const [anchorEl, setAnchorEl] = useState(null);
+ const [dateTime, setDateTime] = useState(value);
+ const [timezone, setTimezone] = useState(
+ timeZoneSelectProps?.value || null
+ );
+
+ const handleOpen = (event: React.MouseEvent) => {
+ setAnchorEl(event.currentTarget);
+ };
+
+ const handleClose = () => {
+ setAnchorEl(null);
+ if (onCancel) {
+ onCancel();
+ }
+ };
+
+ const handleApply = () => {
+ setAnchorEl(null);
+ if (onChange) {
+ onChange(dateTime);
+ }
+ if (onApply) {
+ onApply();
+ }
+ };
+
+ const handleDateChange = (newDate: DateTime | null) => {
+ setDateTime((prev) =>
+ newDate
+ ? newDate.set({
+ hour: prev?.hour || 0,
+ minute: prev?.minute || 0,
+ })
+ : null
+ );
+ };
+
+ const handleTimeChange = (newTime: DateTime | null) => {
+ if (newTime) {
+ setDateTime((prev) => {
+ if (prev) {
+ // Ensure hour and minute are valid numbers
+ const newHour = newTime.hour;
+ const newMinute = newTime.minute;
+
+ if (typeof newHour === 'number' && typeof newMinute === 'number') {
+ return prev.set({ hour: newHour, minute: newMinute });
+ }
+ }
+ // Return the current `prev` value if newTime is invalid
+ return prev;
+ });
+ }
+ };
+
+ const handleTimezoneChange = (newTimezone: string) => {
+ setTimezone(newTimezone);
+ setDateTime((prev) => (prev ? prev.setZone(newTimezone) : null));
+ if (timeZoneSelectProps?.onChange) {
+ timeZoneSelectProps.onChange(newTimezone);
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ({
+ marginBottom: theme.spacing(1),
+ marginRight: theme.spacing(2),
+ })}
+ />
+
+
+
+ );
+};
From 2578b837a0656cee6829eb95d2d2718643a21e11 Mon Sep 17 00:00:00 2001
From: cpathipa <119517080+cpathipa@users.noreply.github.com>
Date: Thu, 31 Oct 2024 10:01:06 -0500
Subject: [PATCH 08/35] Reusable TimeZone Select Component
---
.../components/DatePicker/TimeZoneSelect.tsx | 63 +++++++++++++++++++
1 file changed, 63 insertions(+)
create mode 100644 packages/manager/src/components/DatePicker/TimeZoneSelect.tsx
diff --git a/packages/manager/src/components/DatePicker/TimeZoneSelect.tsx b/packages/manager/src/components/DatePicker/TimeZoneSelect.tsx
new file mode 100644
index 00000000000..3ec726b6ef6
--- /dev/null
+++ b/packages/manager/src/components/DatePicker/TimeZoneSelect.tsx
@@ -0,0 +1,63 @@
+import { DateTime } from 'luxon';
+import React from 'react';
+
+import { timezones } from 'src/assets/timezones/timezones';
+import { Autocomplete } from 'src/components/Autocomplete/Autocomplete';
+
+type Timezone = typeof timezones[number];
+
+interface TimeZoneSelectProps {
+ disabled?: boolean;
+ errorText?: string;
+ label?: string;
+ onChange: (timezone: string) => void;
+ value: null | string;
+}
+
+const getOptionLabel = ({ label, offset }: Timezone) => {
+ const minutes = (Math.abs(offset) % 60).toLocaleString(undefined, {
+ minimumIntegerDigits: 2,
+ useGrouping: false,
+ });
+ const hours = Math.floor(Math.abs(offset) / 60);
+ const isPositive = Math.abs(offset) === offset ? '+' : '-';
+
+ return `(GMT ${isPositive}${hours}:${minutes}) ${label}`;
+};
+
+const getTimezoneOptions = () => {
+ return timezones
+ .map((tz) => {
+ const offset = DateTime.now().setZone(tz.name).offset;
+ const label = getOptionLabel({ ...tz, offset });
+ return { label, offset, value: tz.name };
+ })
+ .sort((a, b) => a.offset - b.offset);
+};
+
+const timezoneOptions = getTimezoneOptions();
+
+export const TimeZoneSelect = ({
+ disabled = false,
+ errorText,
+ label = 'Timezone',
+ onChange,
+ value,
+}: TimeZoneSelectProps) => {
+ return (
+ option.value === value) ?? undefined
+ }
+ autoHighlight
+ disableClearable
+ disabled={disabled}
+ errorText={errorText}
+ fullWidth
+ label={label}
+ onChange={(e, option) => onChange(option?.value || '')}
+ options={timezoneOptions}
+ placeholder="Choose a Timezone"
+ />
+ );
+};
From 43810e75d75ec4417799ce779e8620de0ce16f5d Mon Sep 17 00:00:00 2001
From: cpathipa <119517080+cpathipa@users.noreply.github.com>
Date: Thu, 31 Oct 2024 10:18:17 -0500
Subject: [PATCH 09/35] Create custom DateTimeRangePicker component
---
.../DateTimeRangePicker.stories.tsx | 77 ++++++++++++
.../DatePicker/DateTimeRangePicker.test.tsx | 112 ++++++++++++++++++
.../DatePicker/DateTimeRangePicker.tsx | 89 ++++++++++++++
3 files changed, 278 insertions(+)
create mode 100644 packages/manager/src/components/DatePicker/DateTimeRangePicker.stories.tsx
create mode 100644 packages/manager/src/components/DatePicker/DateTimeRangePicker.test.tsx
create mode 100644 packages/manager/src/components/DatePicker/DateTimeRangePicker.tsx
diff --git a/packages/manager/src/components/DatePicker/DateTimeRangePicker.stories.tsx b/packages/manager/src/components/DatePicker/DateTimeRangePicker.stories.tsx
new file mode 100644
index 00000000000..70ad6b2a39d
--- /dev/null
+++ b/packages/manager/src/components/DatePicker/DateTimeRangePicker.stories.tsx
@@ -0,0 +1,77 @@
+import { action } from '@storybook/addon-actions';
+import { DateTime } from 'luxon';
+import * as React from 'react';
+
+import { DateTimeRangePicker } from './DateTimeRangePicker';
+
+import type { Meta, StoryObj } from '@storybook/react';
+
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: {
+ endLabel: 'End Date and Time',
+ format: 'yyyy-MM-dd HH:mm',
+ onChange: action('DateTime range changed'),
+ startLabel: 'Start Date and Time',
+ },
+ render: (args) => ,
+};
+
+export const WithInitialValues: Story = {
+ render: () => {
+ const ComponentWithState: React.FC = () => {
+ const [start, setStart] = React.useState(
+ DateTime.now().minus({ days: 1 })
+ );
+ const [end, setEnd] = React.useState(DateTime.now());
+
+ const handleDateChange = (
+ newStart: DateTime | null,
+ newEnd: DateTime | null
+ ) => {
+ setStart(newStart);
+ setEnd(newEnd);
+ action('DateTime range changed')(newStart?.toISO(), newEnd?.toISO());
+ };
+
+ return (
+
+ );
+ };
+
+ return ;
+ },
+};
+
+const meta: Meta = {
+ argTypes: {
+ endLabel: {
+ control: 'text',
+ description: 'Label for the end date picker',
+ },
+ format: {
+ control: 'text',
+ description: 'Format for displaying the date-time',
+ },
+ onChange: {
+ action: 'DateTime range changed',
+ description: 'Callback when the date-time range changes',
+ },
+ startLabel: {
+ control: 'text',
+ description: 'Label for the start date picker',
+ },
+ },
+ component: DateTimeRangePicker,
+ title: 'Components/DateTimeRangePicker',
+};
+
+export default meta;
diff --git a/packages/manager/src/components/DatePicker/DateTimeRangePicker.test.tsx b/packages/manager/src/components/DatePicker/DateTimeRangePicker.test.tsx
new file mode 100644
index 00000000000..843678ac4b9
--- /dev/null
+++ b/packages/manager/src/components/DatePicker/DateTimeRangePicker.test.tsx
@@ -0,0 +1,112 @@
+import { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { DateTime } from 'luxon';
+import * as React from 'react';
+
+import { renderWithTheme } from 'src/utilities/testHelpers';
+
+import { DateTimeRangePicker } from './DateTimeRangePicker';
+
+describe('DateTimeRangePicker Component', () => {
+ const onChangeMock = vi.fn();
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('should render start and end DateTimePickers with correct labels', () => {
+ renderWithTheme();
+
+ expect(screen.getByLabelText('Start Date')).toBeVisible();
+ expect(screen.getByLabelText('End Date and Time')).toBeVisible();
+ });
+
+ it('should call onChange when start date is changed', async () => {
+ renderWithTheme();
+
+ // Open start date picker
+ await userEvent.click(screen.getByLabelText('Start Date'));
+
+ await userEvent.click(screen.getByRole('gridcell', { name: '10' }));
+ await userEvent.click(screen.getByRole('button', { name: 'Apply' }));
+
+ // Check if the onChange function is called with the expected DateTime value
+ expect(onChangeMock).toHaveBeenCalledWith(
+ expect.objectContaining({ day: 10 }),
+ null
+ );
+ });
+
+ it.skip('should call onChange when end date is changed', async () => {
+ renderWithTheme(
+
+ );
+
+ // Open the end date picker
+ await userEvent.click(
+ screen.getByRole('textbox', { name: 'End Date and Time' })
+ );
+
+ // Confirm the dialog is visible and wait for it
+ expect(screen.getByRole('dialog')).toBeVisible();
+
+ // Simulate selecting a date (e.g., 15th of the month)
+ const element = screen.getByRole('gridcell', { name: '2' });
+
+ await userEvent.click(element);
+ // await userEvent.click(screen.getByRole('gridcell', { name: '1' }));
+
+ // Click the 'Apply' button and wait for onChange to be called
+ await userEvent.click(screen.getByRole('button', { name: 'Apply' }));
+
+ // Verify onChange is called with the appropriate arguments
+ expect(onChangeMock).toHaveBeenCalledWith(null, expect.any(DateTime));
+ });
+
+ it.skip('should show error when end date-time is before start date-time', async () => {
+ renderWithTheme();
+
+ // Set start date-time to the 15th
+ const startDateField = screen.getByLabelText('Start Date');
+ await userEvent.click(startDateField);
+ await userEvent.click(screen.getByRole('gridcell', { name: '15' }));
+ await userEvent.click(screen.getByRole('button', { name: 'Apply' }));
+
+ // Set end date-time to the 10th (before start date-time)
+ const endDateField = screen.getByLabelText('End Date and Time');
+ await userEvent.click(endDateField);
+ await userEvent.click(screen.getByRole('gridcell', { name: '10' }));
+ await userEvent.click(screen.getByRole('button', { name: 'Apply' }));
+
+ expect(screen.getAllByText('Invalid dae and Time')).toHaveLength(2);
+ });
+
+ it.skip('should clear the error when a valid end date-time is selected', async () => {
+ renderWithTheme();
+
+ // Set start date-time to the 10th
+ const startDateField = screen.getByLabelText('Start Date');
+ await userEvent.click(startDateField);
+ await userEvent.click(screen.getByRole('gridcell', { name: '10' }));
+ await userEvent.click(screen.getByRole('button', { name: 'Apply' }));
+
+ // Set an invalid end date-time (e.g., 5th)
+ const endDateField = screen.getByLabelText('End Date and Time');
+ await userEvent.click(endDateField);
+ await userEvent.click(screen.getByRole('gridcell', { name: '5' }));
+ await userEvent.click(screen.getByRole('button', { name: 'Apply' }));
+
+ expect(screen.getAllByText('Invalid dae and Time')).toHaveLength(2);
+
+ // Select a valid end date-time (e.g., 15th)
+ await userEvent.click(endDateField);
+ await userEvent.click(screen.getByRole('gridcell', { name: '15' }));
+ await userEvent.click(screen.getByRole('button', { name: 'Apply' }));
+
+ expect(screen.queryByText('Invalid dae and Time')).not.toBeInTheDocument();
+ });
+});
diff --git a/packages/manager/src/components/DatePicker/DateTimeRangePicker.tsx b/packages/manager/src/components/DatePicker/DateTimeRangePicker.tsx
new file mode 100644
index 00000000000..95d9e12a45e
--- /dev/null
+++ b/packages/manager/src/components/DatePicker/DateTimeRangePicker.tsx
@@ -0,0 +1,89 @@
+import { DateTime } from 'luxon';
+import React, { useState } from 'react';
+
+import { Box } from 'src/components/Box';
+
+import { DateTimePicker } from './DateTimePicker';
+
+interface DateTimeRangePickerProps {
+ /** Initial or controlled value for the end date-time */
+ endDateTimeValue?: DateTime | null;
+ /** Custom labels for the start and end date/time fields */
+ endLabel?: string;
+ /** Format for displaying the date-time */
+ format?: string;
+ /** Callback when the date-time range changes */
+ onChange?: (start: DateTime | null, end: DateTime | null) => void;
+ /** Initial or controlled value for the start date-time */
+ startDateTimeValue?: DateTime | null;
+ /** Custom labels for the start and end date/time fields */
+ startLabel?: string;
+}
+
+export const DateTimeRangePicker = ({
+ endDateTimeValue = null,
+ endLabel = 'End Date and Time',
+ format = 'yyyy-MM-dd HH:mm',
+ onChange,
+ startDateTimeValue = null,
+ startLabel = 'Start Date',
+}: DateTimeRangePickerProps) => {
+ const [startDateTime, setStartDateTime] = useState(
+ startDateTimeValue
+ );
+ const [endDateTime, setEndDateTime] = useState(
+ endDateTimeValue
+ );
+ const [error, setError] = useState();
+
+ const handleStartDateTimeChange = (newStart: DateTime | null) => {
+ setStartDateTime(newStart);
+
+ // Reset error if the selection is valid
+ if (endDateTime && newStart && endDateTime >= newStart) {
+ setError(undefined);
+ }
+
+ if (onChange) {
+ onChange(newStart, endDateTime);
+ }
+ };
+
+ const handleEndDateTimeChange = (newEnd: DateTime | null) => {
+ // Check if the end date is before the start date
+ if (startDateTime && newEnd && newEnd < startDateTime) {
+ setError('Invalid dae and Time');
+ } else {
+ setEndDateTime(newEnd);
+ setError(undefined); // Clear the error if the selection is valid
+ }
+
+ if (onChange) {
+ onChange(startDateTime, newEnd);
+ }
+ };
+
+ return (
+
+
+
+
+ );
+};
From 3b6f981363089f989663e16e3e4cfd97b420cc18 Mon Sep 17 00:00:00 2001
From: cpathipa <119517080+cpathipa@users.noreply.github.com>
Date: Tue, 5 Nov 2024 19:45:30 -0600
Subject: [PATCH 10/35] Storybook for DateTimePicker
---
.../DatePicker/DateTimePicker.stories.tsx | 93 +++++++++++++++++++
.../components/DatePicker/DateTimePicker.tsx | 2 +-
.../DatePicker/DateTimeRangePicker.tsx | 3 +-
3 files changed, 95 insertions(+), 3 deletions(-)
create mode 100644 packages/manager/src/components/DatePicker/DateTimePicker.stories.tsx
diff --git a/packages/manager/src/components/DatePicker/DateTimePicker.stories.tsx b/packages/manager/src/components/DatePicker/DateTimePicker.stories.tsx
new file mode 100644
index 00000000000..32d65dfa0b7
--- /dev/null
+++ b/packages/manager/src/components/DatePicker/DateTimePicker.stories.tsx
@@ -0,0 +1,93 @@
+import { action } from '@storybook/addon-actions';
+import { DateTime } from 'luxon';
+import * as React from 'react';
+
+import { DateTimePicker } from './DateTimePicker';
+
+import type { Meta, StoryObj } from '@storybook/react';
+
+type Story = StoryObj;
+
+export const ControlledExample: Story = {
+ render: () => {
+ const ControlledDateTimePicker = () => {
+ const [
+ selectedDateTime,
+ setSelectedDateTime,
+ ] = React.useState(DateTime.now());
+
+ const handleChange = (newDateTime: DateTime | null) => {
+ setSelectedDateTime(newDateTime);
+ action('Controlled dateTime change')(newDateTime?.toISO());
+ };
+
+ return (
+
+ );
+ };
+
+ return ;
+ },
+};
+
+export const DefaultExample: Story = {
+ args: {
+ label: 'Default Date-Time Picker',
+ onApply: action('Apply clicked'),
+ onCancel: action('Cancel clicked'),
+ onChange: action('Date-Time selected'),
+ placeholder: 'yyyy-MM-dd HH:mm',
+ },
+};
+
+export const WithErrorText: Story = {
+ args: {
+ errorText: 'This field is required',
+ label: 'Date-Time Picker with Error',
+ onApply: action('Apply clicked with error'),
+ onCancel: action('Cancel clicked with error'),
+ onChange: action('Date-Time selected with error'),
+ placeholder: 'yyyy-MM-dd HH:mm',
+ },
+};
+
+const meta: Meta = {
+ argTypes: {
+ errorText: {
+ control: { type: 'text' },
+ },
+ format: {
+ control: { type: 'text' },
+ },
+ label: {
+ control: { type: 'text' },
+ },
+ onApply: { action: 'applyClicked' },
+ onCancel: { action: 'cancelClicked' },
+ onChange: { action: 'dateTimeChanged' },
+ placeholder: {
+ control: { type: 'text' },
+ },
+ },
+ args: {
+ format: 'yyyy-MM-dd HH:mm',
+ label: 'Date-Time Picker',
+ placeholder: 'Select a date and time',
+ },
+ component: DateTimePicker,
+ title: 'Components/DateTimePicker',
+};
+
+export default meta;
diff --git a/packages/manager/src/components/DatePicker/DateTimePicker.tsx b/packages/manager/src/components/DatePicker/DateTimePicker.tsx
index a77c4eae60d..2fac07dfaf3 100644
--- a/packages/manager/src/components/DatePicker/DateTimePicker.tsx
+++ b/packages/manager/src/components/DatePicker/DateTimePicker.tsx
@@ -1,3 +1,4 @@
+import { Box } from '@linode/ui';
import { Grid, Popover } from '@mui/material';
import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon';
import { DateCalendar } from '@mui/x-date-pickers/DateCalendar';
@@ -6,7 +7,6 @@ import { TimePicker } from '@mui/x-date-pickers/TimePicker';
import React, { useState } from 'react';
import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel';
-import { Box } from 'src/components/Box';
import { Divider } from 'src/components/Divider';
import { TextField } from 'src/components/TextField';
diff --git a/packages/manager/src/components/DatePicker/DateTimeRangePicker.tsx b/packages/manager/src/components/DatePicker/DateTimeRangePicker.tsx
index 95d9e12a45e..f7a658178b3 100644
--- a/packages/manager/src/components/DatePicker/DateTimeRangePicker.tsx
+++ b/packages/manager/src/components/DatePicker/DateTimeRangePicker.tsx
@@ -1,8 +1,7 @@
+import { Box } from '@linode/ui';
import { DateTime } from 'luxon';
import React, { useState } from 'react';
-import { Box } from 'src/components/Box';
-
import { DateTimePicker } from './DateTimePicker';
interface DateTimeRangePickerProps {
From 9b10623bdcb63b88c3c9c00ca56e9bfff503d492 Mon Sep 17 00:00:00 2001
From: cpathipa <119517080+cpathipa@users.noreply.github.com>
Date: Thu, 7 Nov 2024 12:20:29 -0600
Subject: [PATCH 11/35] Fix tests and remove console warnings
---
.../components/DatePicker/DateTimePicker.tsx | 2 +-
.../DatePicker/DateTimeRangePicker.test.tsx | 67 ++-----------------
2 files changed, 8 insertions(+), 61 deletions(-)
diff --git a/packages/manager/src/components/DatePicker/DateTimePicker.tsx b/packages/manager/src/components/DatePicker/DateTimePicker.tsx
index 2fac07dfaf3..1d7e05b8237 100644
--- a/packages/manager/src/components/DatePicker/DateTimePicker.tsx
+++ b/packages/manager/src/components/DatePicker/DateTimePicker.tsx
@@ -133,7 +133,7 @@ export const DateTimePicker = ({
? `${dateTime.toFormat(format)}${
timezone ? ` (${timezone})` : ''
}`
- : null
+ : ''
}
InputProps={{ readOnly: true }}
errorText={errorText}
diff --git a/packages/manager/src/components/DatePicker/DateTimeRangePicker.test.tsx b/packages/manager/src/components/DatePicker/DateTimeRangePicker.test.tsx
index 843678ac4b9..f3421b684c3 100644
--- a/packages/manager/src/components/DatePicker/DateTimeRangePicker.test.tsx
+++ b/packages/manager/src/components/DatePicker/DateTimeRangePicker.test.tsx
@@ -1,6 +1,5 @@
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
-import { DateTime } from 'luxon';
import * as React from 'react';
import { renderWithTheme } from 'src/utilities/testHelpers';
@@ -37,37 +36,7 @@ describe('DateTimeRangePicker Component', () => {
);
});
- it.skip('should call onChange when end date is changed', async () => {
- renderWithTheme(
-
- );
-
- // Open the end date picker
- await userEvent.click(
- screen.getByRole('textbox', { name: 'End Date and Time' })
- );
-
- // Confirm the dialog is visible and wait for it
- expect(screen.getByRole('dialog')).toBeVisible();
-
- // Simulate selecting a date (e.g., 15th of the month)
- const element = screen.getByRole('gridcell', { name: '2' });
-
- await userEvent.click(element);
- // await userEvent.click(screen.getByRole('gridcell', { name: '1' }));
-
- // Click the 'Apply' button and wait for onChange to be called
- await userEvent.click(screen.getByRole('button', { name: 'Apply' }));
-
- // Verify onChange is called with the appropriate arguments
- expect(onChangeMock).toHaveBeenCalledWith(null, expect.any(DateTime));
- });
-
- it.skip('should show error when end date-time is before start date-time', async () => {
+ it('should show error when end date-time is before start date-time', async () => {
renderWithTheme();
// Set start date-time to the 15th
@@ -76,37 +45,15 @@ describe('DateTimeRangePicker Component', () => {
await userEvent.click(screen.getByRole('gridcell', { name: '15' }));
await userEvent.click(screen.getByRole('button', { name: 'Apply' }));
- // Set end date-time to the 10th (before start date-time)
- const endDateField = screen.getByLabelText('End Date and Time');
- await userEvent.click(endDateField);
- await userEvent.click(screen.getByRole('gridcell', { name: '10' }));
- await userEvent.click(screen.getByRole('button', { name: 'Apply' }));
-
- expect(screen.getAllByText('Invalid dae and Time')).toHaveLength(2);
- });
-
- it.skip('should clear the error when a valid end date-time is selected', async () => {
- renderWithTheme();
-
- // Set start date-time to the 10th
- const startDateField = screen.getByLabelText('Start Date');
- await userEvent.click(startDateField);
- await userEvent.click(screen.getByRole('gridcell', { name: '10' }));
- await userEvent.click(screen.getByRole('button', { name: 'Apply' }));
-
- // Set an invalid end date-time (e.g., 5th)
+ // Open the end date picker
const endDateField = screen.getByLabelText('End Date and Time');
await userEvent.click(endDateField);
- await userEvent.click(screen.getByRole('gridcell', { name: '5' }));
- await userEvent.click(screen.getByRole('button', { name: 'Apply' }));
- expect(screen.getAllByText('Invalid dae and Time')).toHaveLength(2);
-
- // Select a valid end date-time (e.g., 15th)
- await userEvent.click(endDateField);
- await userEvent.click(screen.getByRole('gridcell', { name: '15' }));
- await userEvent.click(screen.getByRole('button', { name: 'Apply' }));
+ // Check if the date before the start date is disabled via a class or attribute
+ const invalidDate = screen.getByRole('gridcell', { name: '10' });
+ expect(invalidDate).toHaveClass('Mui-disabled'); // or check for the specific attribute used
- expect(screen.queryByText('Invalid dae and Time')).not.toBeInTheDocument();
+ // Confirm error message is not shown since the click was blocked
+ expect(screen.queryByText('Invalid date and Time')).not.toBeInTheDocument();
});
});
From 11d878004ebca1ebea35a703bb4d6a96797e3a4d Mon Sep 17 00:00:00 2001
From: cpathipa <119517080+cpathipa@users.noreply.github.com>
Date: Mon, 18 Nov 2024 09:38:27 -0600
Subject: [PATCH 12/35] changeset
---
packages/manager/.changeset/pr-11151-added-1731944151381.md | 5 +++++
.../manager/src/components/DatePicker/DateTimePicker.tsx | 2 +-
.../features/GlobalNotifications/CreditCardExpiredBanner.tsx | 2 +-
3 files changed, 7 insertions(+), 2 deletions(-)
create mode 100644 packages/manager/.changeset/pr-11151-added-1731944151381.md
diff --git a/packages/manager/.changeset/pr-11151-added-1731944151381.md b/packages/manager/.changeset/pr-11151-added-1731944151381.md
new file mode 100644
index 00000000000..1a64466eea4
--- /dev/null
+++ b/packages/manager/.changeset/pr-11151-added-1731944151381.md
@@ -0,0 +1,5 @@
+---
+"@linode/manager": Added
+---
+
+New DatePicker Component ([#11151](https://github.com/linode/manager/pull/11151))
diff --git a/packages/manager/src/components/DatePicker/DateTimePicker.tsx b/packages/manager/src/components/DatePicker/DateTimePicker.tsx
index 1d7e05b8237..2fca86ab17f 100644
--- a/packages/manager/src/components/DatePicker/DateTimePicker.tsx
+++ b/packages/manager/src/components/DatePicker/DateTimePicker.tsx
@@ -1,3 +1,4 @@
+import { Divider } from '@linode/ui';
import { Box } from '@linode/ui';
import { Grid, Popover } from '@mui/material';
import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon';
@@ -7,7 +8,6 @@ import { TimePicker } from '@mui/x-date-pickers/TimePicker';
import React, { useState } from 'react';
import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel';
-import { Divider } from 'src/components/Divider';
import { TextField } from 'src/components/TextField';
import { TimeZoneSelect } from './TimeZoneSelect';
diff --git a/packages/manager/src/features/GlobalNotifications/CreditCardExpiredBanner.tsx b/packages/manager/src/features/GlobalNotifications/CreditCardExpiredBanner.tsx
index dbbe22436e8..c386feb9b4c 100644
--- a/packages/manager/src/features/GlobalNotifications/CreditCardExpiredBanner.tsx
+++ b/packages/manager/src/features/GlobalNotifications/CreditCardExpiredBanner.tsx
@@ -1,8 +1,8 @@
+import { Button } from '@linode/ui';
import { Typography } from '@mui/material';
import * as React from 'react';
import { useHistory } from 'react-router-dom';
-import { Button } from 'src/components/Button/Button';
import { DismissibleBanner } from 'src/components/DismissibleBanner/DismissibleBanner';
import { useAccount } from 'src/queries/account/account';
import { isCreditCardExpired } from 'src/utilities/creditCard';
From 7aec8c958694af09bdf801c1d97f2d75bfad4127 Mon Sep 17 00:00:00 2001
From: cpathipa <119517080+cpathipa@users.noreply.github.com>
Date: Wed, 20 Nov 2024 05:17:53 -0600
Subject: [PATCH 13/35] Update
packages/manager/src/components/DatePicker/DateTimeRangePicker.tsx
Co-authored-by: Connie Liu <139280159+coliu-akamai@users.noreply.github.com>
---
.../manager/src/components/DatePicker/DateTimeRangePicker.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/manager/src/components/DatePicker/DateTimeRangePicker.tsx b/packages/manager/src/components/DatePicker/DateTimeRangePicker.tsx
index f7a658178b3..ed57af7c37c 100644
--- a/packages/manager/src/components/DatePicker/DateTimeRangePicker.tsx
+++ b/packages/manager/src/components/DatePicker/DateTimeRangePicker.tsx
@@ -51,7 +51,7 @@ export const DateTimeRangePicker = ({
const handleEndDateTimeChange = (newEnd: DateTime | null) => {
// Check if the end date is before the start date
if (startDateTime && newEnd && newEnd < startDateTime) {
- setError('Invalid dae and Time');
+ setError('Invalid date and time');
} else {
setEndDateTime(newEnd);
setError(undefined); // Clear the error if the selection is valid
From 0c379a2f692ba5256aef483a5152845b08d467b1 Mon Sep 17 00:00:00 2001
From: cpathipa <119517080+cpathipa@users.noreply.github.com>
Date: Thu, 21 Nov 2024 13:02:31 -0600
Subject: [PATCH 14/35] Adjust styles for DatePicker
---
.../src/components/DatePicker/DatePicker.tsx | 31 +++++++++++++++----
.../DatePicker/DateTimePicker.test.tsx | 12 -------
2 files changed, 25 insertions(+), 18 deletions(-)
diff --git a/packages/manager/src/components/DatePicker/DatePicker.tsx b/packages/manager/src/components/DatePicker/DatePicker.tsx
index 006bcb426d9..20573d41f7f 100644
--- a/packages/manager/src/components/DatePicker/DatePicker.tsx
+++ b/packages/manager/src/components/DatePicker/DatePicker.tsx
@@ -1,3 +1,4 @@
+import { useTheme } from '@mui/material/styles';
import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon';
import { DatePicker as MuiDatePicker } from '@mui/x-date-pickers/DatePicker';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
@@ -39,6 +40,8 @@ export const DatePicker = ({
value = null,
...props
}: DatePickerProps) => {
+ const theme = useTheme();
+
const onChangeHandler = (newDate: DateTime | null) => {
if (onChange) {
onChange(newDate);
@@ -50,23 +53,39 @@ export const DatePicker = ({
diff --git a/packages/manager/src/components/DatePicker/DateTimePicker.test.tsx b/packages/manager/src/components/DatePicker/DateTimePicker.test.tsx
index bc19b643ad3..ca9bc882340 100644
--- a/packages/manager/src/components/DatePicker/DateTimePicker.test.tsx
+++ b/packages/manager/src/components/DatePicker/DateTimePicker.test.tsx
@@ -72,18 +72,6 @@ describe('DateTimePicker Component', () => {
expect(defaultProps.onChange).toHaveBeenCalled();
});
- it.skip('should handle time changes correctly', async () => {
- renderWithTheme();
- await userEvent.click(
- screen.getByRole('textbox', { name: 'Select Date and Time' })
- );
-
- // Simulate changing time in the TimePicker (e.g., setting time to 10:45)
- const timeInput = screen.getByLabelText(/Select Time/i);
- await userEvent.type(timeInput, '10:45');
- expect(defaultProps.onChange).toHaveBeenCalled();
- });
-
it('should handle timezone changes correctly', async () => {
const timezoneChangeMock = vi.fn(); // Create a mock function
From 194b0d2a4526ec4b42edf3fefd54e3b5195365a8 Mon Sep 17 00:00:00 2001
From: cpathipa <119517080+cpathipa@users.noreply.github.com>
Date: Fri, 22 Nov 2024 06:51:32 -0600
Subject: [PATCH 15/35] Adjust styles for DateTimePicker
---
.../components/DatePicker/DateTimePicker.tsx | 46 +++++++++++++++++--
.../components/DatePicker/TimeZoneSelect.tsx | 5 +-
2 files changed, 45 insertions(+), 6 deletions(-)
diff --git a/packages/manager/src/components/DatePicker/DateTimePicker.tsx b/packages/manager/src/components/DatePicker/DateTimePicker.tsx
index 2fca86ab17f..2f01eabb20a 100644
--- a/packages/manager/src/components/DatePicker/DateTimePicker.tsx
+++ b/packages/manager/src/components/DatePicker/DateTimePicker.tsx
@@ -1,5 +1,5 @@
-import { Divider } from '@linode/ui';
import { Box } from '@linode/ui';
+import { Divider } from '@linode/ui';
import { Grid, Popover } from '@mui/material';
import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon';
import { DateCalendar } from '@mui/x-date-pickers/DateCalendar';
@@ -14,6 +14,7 @@ import { TimeZoneSelect } from './TimeZoneSelect';
import type { DateCalendarProps } from '@mui/x-date-pickers/DateCalendar';
import type { DateTime } from 'luxon';
+import type { TextFieldProps } from 'src/components/TextField';
export interface DateTimePickerProps {
/** Additional props for the DateCalendar */
@@ -66,6 +67,11 @@ export const DateTimePicker = ({
timeZoneSelectProps?.value || null
);
+ const textFieldProps: TextFieldProps = {
+ label: timeSelectProps?.label ?? 'Select Time',
+ noMarginTop: true,
+ };
+
const handleOpen = (event: React.MouseEvent) => {
setAnchorEl(event.currentTarget);
};
@@ -151,24 +157,56 @@ export const DateTimePicker = ({
>
({
+ '& .MuiDayCalendar-weekContainer, & .MuiDayCalendar-header': {
+ justifyContent: 'space-between',
+ },
+ '& .MuiDayCalendar-weekDayLabel': {
+ fontSize: '0.875rem',
+ },
+ '& .MuiPickersCalendarHeader-label': {
+ fontFamily: theme.font.bold,
+ },
+ '& .MuiPickersCalendarHeader-root': {
+ borderBottom: `1px solid ${theme.borderColors.divider}`,
+ fontSize: '0.875rem',
+ paddingBottom: theme.spacing(1),
+ },
+ '& .MuiPickersDay-root': {
+ fontSize: '0.875rem',
+ margin: `${theme.spacing(0.5)}px`,
+ },
+ borderRadius: `${theme.spacing(2)}`,
+ borderWidth: '0px',
+ })}
onChange={handleDateChange}
value={dateTime}
{...dateCalendarProps}
/>
-
+
-
+
diff --git a/packages/manager/src/components/DatePicker/TimeZoneSelect.tsx b/packages/manager/src/components/DatePicker/TimeZoneSelect.tsx
index 3ec726b6ef6..19312098007 100644
--- a/packages/manager/src/components/DatePicker/TimeZoneSelect.tsx
+++ b/packages/manager/src/components/DatePicker/TimeZoneSelect.tsx
@@ -10,6 +10,7 @@ interface TimeZoneSelectProps {
disabled?: boolean;
errorText?: string;
label?: string;
+ noMarginTop?: boolean;
onChange: (timezone: string) => void;
value: null | string;
}
@@ -41,6 +42,7 @@ export const TimeZoneSelect = ({
disabled = false,
errorText,
label = 'Timezone',
+ noMarginTop = false,
onChange,
value,
}: TimeZoneSelectProps) => {
@@ -50,11 +52,10 @@ export const TimeZoneSelect = ({
timezoneOptions.find((option) => option.value === value) ?? undefined
}
autoHighlight
- disableClearable
disabled={disabled}
errorText={errorText}
- fullWidth
label={label}
+ noMarginTop={noMarginTop}
onChange={(e, option) => onChange(option?.value || '')}
options={timezoneOptions}
placeholder="Choose a Timezone"
From b2d591f58823c508e76ff21e6d0cf7dd88190916 Mon Sep 17 00:00:00 2001
From: cpathipa <119517080+cpathipa@users.noreply.github.com>
Date: Mon, 2 Dec 2024 11:21:40 -0600
Subject: [PATCH 16/35] update imports
---
packages/manager/src/components/DatePicker/DatePicker.tsx | 5 ++---
.../manager/src/components/DatePicker/DateTimePicker.tsx | 4 ++--
2 files changed, 4 insertions(+), 5 deletions(-)
diff --git a/packages/manager/src/components/DatePicker/DatePicker.tsx b/packages/manager/src/components/DatePicker/DatePicker.tsx
index 20573d41f7f..a3a5a722e82 100644
--- a/packages/manager/src/components/DatePicker/DatePicker.tsx
+++ b/packages/manager/src/components/DatePicker/DatePicker.tsx
@@ -1,14 +1,13 @@
+import { TextField } from '@linode/ui';
import { useTheme } from '@mui/material/styles';
import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon';
import { DatePicker as MuiDatePicker } from '@mui/x-date-pickers/DatePicker';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import React from 'react';
-import { TextField } from 'src/components/TextField';
-
+import type { TextFieldProps } from '@linode/ui';
import type { DatePickerProps as MuiDatePickerProps } from '@mui/x-date-pickers/DatePicker';
import type { DateTime } from 'luxon';
-import type { TextFieldProps } from 'src/components/TextField';
export interface DatePickerProps
extends Omit, 'onChange' | 'value'> {
diff --git a/packages/manager/src/components/DatePicker/DateTimePicker.tsx b/packages/manager/src/components/DatePicker/DateTimePicker.tsx
index 2f01eabb20a..bc37175a636 100644
--- a/packages/manager/src/components/DatePicker/DateTimePicker.tsx
+++ b/packages/manager/src/components/DatePicker/DateTimePicker.tsx
@@ -1,3 +1,4 @@
+import { TextField } from '@linode/ui';
import { Box } from '@linode/ui';
import { Divider } from '@linode/ui';
import { Grid, Popover } from '@mui/material';
@@ -8,13 +9,12 @@ import { TimePicker } from '@mui/x-date-pickers/TimePicker';
import React, { useState } from 'react';
import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel';
-import { TextField } from 'src/components/TextField';
import { TimeZoneSelect } from './TimeZoneSelect';
+import type { TextFieldProps } from '@linode/ui';
import type { DateCalendarProps } from '@mui/x-date-pickers/DateCalendar';
import type { DateTime } from 'luxon';
-import type { TextFieldProps } from 'src/components/TextField';
export interface DateTimePickerProps {
/** Additional props for the DateCalendar */
From efcfe5ce5a926a9b12fbc7c5eb6c3379122e3366 Mon Sep 17 00:00:00 2001
From: cpathipa <119517080+cpathipa@users.noreply.github.com>
Date: Mon, 2 Dec 2024 13:35:44 -0600
Subject: [PATCH 17/35] Render time and timezone conditionally in
DateTimePicker component
---
.../DatePicker/DateTimePicker.test.tsx | 17 ++++++
.../components/DatePicker/DateTimePicker.tsx | 55 +++++++++++--------
2 files changed, 50 insertions(+), 22 deletions(-)
diff --git a/packages/manager/src/components/DatePicker/DateTimePicker.test.tsx b/packages/manager/src/components/DatePicker/DateTimePicker.test.tsx
index ca9bc882340..12f1795a747 100644
--- a/packages/manager/src/components/DatePicker/DateTimePicker.test.tsx
+++ b/packages/manager/src/components/DatePicker/DateTimePicker.test.tsx
@@ -123,4 +123,21 @@ describe('DateTimePicker Component', () => {
expect(textField).toHaveValue('25/10/2024 15:30');
});
+ it('should not render the time selector when showTime is false', async () => {
+ renderWithTheme();
+ await userEvent.click(
+ screen.getByRole('textbox', { name: 'Select Date and Time' })
+ );
+ const timePicker = screen.queryByLabelText(/Select Time/i); // Label from timeSelectProps
+ expect(timePicker).not.toBeInTheDocument();
+ });
+
+ it('should not render the timezone selector when showTimeZone is false', async () => {
+ renderWithTheme();
+ await userEvent.click(
+ screen.getByRole('textbox', { name: 'Select Date and Time' })
+ );
+ const timeZoneSelect = screen.queryByLabelText(/Timezone/i); // Label from timeZoneSelectProps
+ expect(timeZoneSelect).not.toBeInTheDocument();
+ });
});
diff --git a/packages/manager/src/components/DatePicker/DateTimePicker.tsx b/packages/manager/src/components/DatePicker/DateTimePicker.tsx
index bc37175a636..c6ac1834166 100644
--- a/packages/manager/src/components/DatePicker/DateTimePicker.tsx
+++ b/packages/manager/src/components/DatePicker/DateTimePicker.tsx
@@ -33,6 +33,11 @@ export interface DateTimePickerProps {
onChange?: (dateTime: DateTime | null) => void;
/** Placeholder text for the input field */
placeholder?: string;
+ /** Whether to show the time selector */
+ showTime?: boolean;
+ /** Whether to show the timezone selector */
+ showTimeZone?: boolean;
+ /** Props for customizing the TimePicker component */
timeSelectProps?: {
label?: string;
onChange?: (time: string) => void;
@@ -57,6 +62,8 @@ export const DateTimePicker = ({
onCancel,
onChange,
placeholder = 'yyyy-MM-dd HH:mm',
+ showTime = true,
+ showTimeZone = true,
timeSelectProps,
timeZoneSelectProps,
value = null,
@@ -188,29 +195,33 @@ export const DateTimePicker = ({
spacing={2}
sx={{ display: 'flex', justifyContent: 'space-between' }}
>
-
-
+
-
-
-
-
+ textField: textFieldProps,
+ }}
+ onChange={handleTimeChange}
+ slots={{ textField: TextField }}
+ value={dateTime}
+ />
+
+ )}
+ {showTimeZone && (
+
+
+
+ )}
From 2ca7adf92d8b9c40d9c7b0d751e8dbea45b3832d Mon Sep 17 00:00:00 2001
From: cpathipa <119517080+cpathipa@users.noreply.github.com>
Date: Tue, 3 Dec 2024 10:46:16 -0600
Subject: [PATCH 18/35] Move DatePicker to UI package
---
.../src/components/DatePicker/DatePicker.stories.tsx | 0
.../src/components/DatePicker/DatePicker.test.tsx | 0
.../{manager => ui}/src/components/DatePicker/DatePicker.tsx | 0
.../src/components/DatePicker/DateTimePicker.stories.tsx | 0
.../src/components/DatePicker/DateTimePicker.test.tsx | 0
.../{manager => ui}/src/components/DatePicker/DateTimePicker.tsx | 0
.../src/components/DatePicker/DateTimeRangePicker.stories.tsx | 0
.../src/components/DatePicker/DateTimeRangePicker.test.tsx | 0
.../src/components/DatePicker/DateTimeRangePicker.tsx | 0
.../{manager => ui}/src/components/DatePicker/TimeZoneSelect.tsx | 0
packages/ui/src/components/DatePicker/index.ts | 1 +
packages/ui/src/components/index.ts | 1 +
12 files changed, 2 insertions(+)
rename packages/{manager => ui}/src/components/DatePicker/DatePicker.stories.tsx (100%)
rename packages/{manager => ui}/src/components/DatePicker/DatePicker.test.tsx (100%)
rename packages/{manager => ui}/src/components/DatePicker/DatePicker.tsx (100%)
rename packages/{manager => ui}/src/components/DatePicker/DateTimePicker.stories.tsx (100%)
rename packages/{manager => ui}/src/components/DatePicker/DateTimePicker.test.tsx (100%)
rename packages/{manager => ui}/src/components/DatePicker/DateTimePicker.tsx (100%)
rename packages/{manager => ui}/src/components/DatePicker/DateTimeRangePicker.stories.tsx (100%)
rename packages/{manager => ui}/src/components/DatePicker/DateTimeRangePicker.test.tsx (100%)
rename packages/{manager => ui}/src/components/DatePicker/DateTimeRangePicker.tsx (100%)
rename packages/{manager => ui}/src/components/DatePicker/TimeZoneSelect.tsx (100%)
create mode 100644 packages/ui/src/components/DatePicker/index.ts
diff --git a/packages/manager/src/components/DatePicker/DatePicker.stories.tsx b/packages/ui/src/components/DatePicker/DatePicker.stories.tsx
similarity index 100%
rename from packages/manager/src/components/DatePicker/DatePicker.stories.tsx
rename to packages/ui/src/components/DatePicker/DatePicker.stories.tsx
diff --git a/packages/manager/src/components/DatePicker/DatePicker.test.tsx b/packages/ui/src/components/DatePicker/DatePicker.test.tsx
similarity index 100%
rename from packages/manager/src/components/DatePicker/DatePicker.test.tsx
rename to packages/ui/src/components/DatePicker/DatePicker.test.tsx
diff --git a/packages/manager/src/components/DatePicker/DatePicker.tsx b/packages/ui/src/components/DatePicker/DatePicker.tsx
similarity index 100%
rename from packages/manager/src/components/DatePicker/DatePicker.tsx
rename to packages/ui/src/components/DatePicker/DatePicker.tsx
diff --git a/packages/manager/src/components/DatePicker/DateTimePicker.stories.tsx b/packages/ui/src/components/DatePicker/DateTimePicker.stories.tsx
similarity index 100%
rename from packages/manager/src/components/DatePicker/DateTimePicker.stories.tsx
rename to packages/ui/src/components/DatePicker/DateTimePicker.stories.tsx
diff --git a/packages/manager/src/components/DatePicker/DateTimePicker.test.tsx b/packages/ui/src/components/DatePicker/DateTimePicker.test.tsx
similarity index 100%
rename from packages/manager/src/components/DatePicker/DateTimePicker.test.tsx
rename to packages/ui/src/components/DatePicker/DateTimePicker.test.tsx
diff --git a/packages/manager/src/components/DatePicker/DateTimePicker.tsx b/packages/ui/src/components/DatePicker/DateTimePicker.tsx
similarity index 100%
rename from packages/manager/src/components/DatePicker/DateTimePicker.tsx
rename to packages/ui/src/components/DatePicker/DateTimePicker.tsx
diff --git a/packages/manager/src/components/DatePicker/DateTimeRangePicker.stories.tsx b/packages/ui/src/components/DatePicker/DateTimeRangePicker.stories.tsx
similarity index 100%
rename from packages/manager/src/components/DatePicker/DateTimeRangePicker.stories.tsx
rename to packages/ui/src/components/DatePicker/DateTimeRangePicker.stories.tsx
diff --git a/packages/manager/src/components/DatePicker/DateTimeRangePicker.test.tsx b/packages/ui/src/components/DatePicker/DateTimeRangePicker.test.tsx
similarity index 100%
rename from packages/manager/src/components/DatePicker/DateTimeRangePicker.test.tsx
rename to packages/ui/src/components/DatePicker/DateTimeRangePicker.test.tsx
diff --git a/packages/manager/src/components/DatePicker/DateTimeRangePicker.tsx b/packages/ui/src/components/DatePicker/DateTimeRangePicker.tsx
similarity index 100%
rename from packages/manager/src/components/DatePicker/DateTimeRangePicker.tsx
rename to packages/ui/src/components/DatePicker/DateTimeRangePicker.tsx
diff --git a/packages/manager/src/components/DatePicker/TimeZoneSelect.tsx b/packages/ui/src/components/DatePicker/TimeZoneSelect.tsx
similarity index 100%
rename from packages/manager/src/components/DatePicker/TimeZoneSelect.tsx
rename to packages/ui/src/components/DatePicker/TimeZoneSelect.tsx
diff --git a/packages/ui/src/components/DatePicker/index.ts b/packages/ui/src/components/DatePicker/index.ts
new file mode 100644
index 00000000000..a48b62e4dfc
--- /dev/null
+++ b/packages/ui/src/components/DatePicker/index.ts
@@ -0,0 +1 @@
+export * from './DatePicker';
diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts
index db690f6940e..f64e7b4105a 100644
--- a/packages/ui/src/components/index.ts
+++ b/packages/ui/src/components/index.ts
@@ -26,3 +26,4 @@ export * from './Tooltip';
export * from './TooltipIcon';
export * from './Typography';
export * from './VisibilityTooltip';
+export * from './DatePicker';
From 31dc03ea9c5625f56cd0496721c84d8d10969a37 Mon Sep 17 00:00:00 2001
From: cpathipa <119517080+cpathipa@users.noreply.github.com>
Date: Tue, 3 Dec 2024 14:43:53 -0600
Subject: [PATCH 19/35] Add DatePicker dependencies
---
packages/ui/package.json | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 342fef3d1ba..3a85d11a12a 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -20,7 +20,9 @@
"@emotion/styled": "^11.11.0",
"@mui/material": "^5.14.7",
"react": "^18.2.0",
- "react-dom": "^18.2.0"
+ "react-dom": "^18.2.0",
+ "@mui/x-date-pickers": "^7.12.0",
+ "luxon": "3.4.4"
},
"scripts": {
"start": "tsc -w --preserveWatchOutput",
@@ -57,4 +59,4 @@
"prettier": "~2.2.1",
"vite-plugin-svgr": "^3.2.0"
}
-}
+}
\ No newline at end of file
From cd875be5b91f4337892e2af4091731fb38f80f77 Mon Sep 17 00:00:00 2001
From: cpathipa <119517080+cpathipa@users.noreply.github.com>
Date: Tue, 3 Dec 2024 18:15:21 -0600
Subject: [PATCH 20/35] Code cleanup
---
.../components/DatePicker/DateTimePicker.tsx | 75 ++++++++++++-------
1 file changed, 47 insertions(+), 28 deletions(-)
diff --git a/packages/ui/src/components/DatePicker/DateTimePicker.tsx b/packages/ui/src/components/DatePicker/DateTimePicker.tsx
index c6ac1834166..4d65dc6f1b9 100644
--- a/packages/ui/src/components/DatePicker/DateTimePicker.tsx
+++ b/packages/ui/src/components/DatePicker/DateTimePicker.tsx
@@ -1,25 +1,26 @@
import { TextField } from '@linode/ui';
-import { Box } from '@linode/ui';
import { Divider } from '@linode/ui';
+import { Box } from '@linode/ui';
import { Grid, Popover } from '@mui/material';
import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon';
import { DateCalendar } from '@mui/x-date-pickers/DateCalendar';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { TimePicker } from '@mui/x-date-pickers/TimePicker';
-import React, { useState } from 'react';
+import React, { useEffect, useState } from 'react';
import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel';
import { TimeZoneSelect } from './TimeZoneSelect';
import type { TextFieldProps } from '@linode/ui';
+import type { Theme } from '@mui/material/styles';
import type { DateCalendarProps } from '@mui/x-date-pickers/DateCalendar';
import type { DateTime } from 'luxon';
export interface DateTimePickerProps {
/** Additional props for the DateCalendar */
dateCalendarProps?: Partial>;
- /** Error text for the date picker filed */
+ /** Error text for the date picker field */
errorText?: string;
/** Format for displaying the date-time */
format?: string;
@@ -64,21 +65,35 @@ export const DateTimePicker = ({
placeholder = 'yyyy-MM-dd HH:mm',
showTime = true,
showTimeZone = true,
- timeSelectProps,
- timeZoneSelectProps,
+ timeSelectProps = {},
+ timeZoneSelectProps = {},
value = null,
}: DateTimePickerProps) => {
const [anchorEl, setAnchorEl] = useState(null);
- const [dateTime, setDateTime] = useState(value);
- const [timezone, setTimezone] = useState(
- timeZoneSelectProps?.value || null
+ const [selectedDateTime, setSelectedDateTime] = useState(
+ value
+ );
+ const [selectedTimeZone, setSelectedTimeZone] = useState(
+ timeZoneSelectProps.value || null
);
- const textFieldProps: TextFieldProps = {
+ const TimePickerFieldProps: TextFieldProps = {
label: timeSelectProps?.label ?? 'Select Time',
noMarginTop: true,
};
+ useEffect(() => {
+ if (value) {
+ setSelectedDateTime(value);
+ }
+ }, [value]);
+
+ useEffect(() => {
+ if (timeZoneSelectProps.value) {
+ setSelectedTimeZone(timeZoneSelectProps.value);
+ }
+ }, [timeZoneSelectProps.value]);
+
const handleOpen = (event: React.MouseEvent) => {
setAnchorEl(event.currentTarget);
};
@@ -93,7 +108,7 @@ export const DateTimePicker = ({
const handleApply = () => {
setAnchorEl(null);
if (onChange) {
- onChange(dateTime);
+ onChange(selectedDateTime);
}
if (onApply) {
onApply();
@@ -101,7 +116,7 @@ export const DateTimePicker = ({
};
const handleDateChange = (newDate: DateTime | null) => {
- setDateTime((prev) =>
+ setSelectedDateTime((prev) =>
newDate
? newDate.set({
hour: prev?.hour || 0,
@@ -113,7 +128,7 @@ export const DateTimePicker = ({
const handleTimeChange = (newTime: DateTime | null) => {
if (newTime) {
- setDateTime((prev) => {
+ setSelectedDateTime((prev) => {
if (prev) {
// Ensure hour and minute are valid numbers
const newHour = newTime.hour;
@@ -127,13 +142,15 @@ export const DateTimePicker = ({
return prev;
});
}
+ if (timeSelectProps.onChange && newTime) {
+ timeSelectProps.onChange(newTime.toISOTime());
+ }
};
- const handleTimezoneChange = (newTimezone: string) => {
- setTimezone(newTimezone);
- setDateTime((prev) => (prev ? prev.setZone(newTimezone) : null));
- if (timeZoneSelectProps?.onChange) {
- timeZoneSelectProps.onChange(newTimezone);
+ const handleTimeZoneChange = (newTimeZone: string) => {
+ setSelectedTimeZone(newTimeZone);
+ if (timeZoneSelectProps.onChange) {
+ timeZoneSelectProps.onChange(newTimeZone);
}
};
@@ -142,9 +159,11 @@ export const DateTimePicker = ({
({
+ sx={(theme: Theme) => ({
'& .MuiDayCalendar-weekContainer, & .MuiDayCalendar-header': {
justifyContent: 'space-between',
},
@@ -187,7 +206,7 @@ export const DateTimePicker = ({
borderWidth: '0px',
})}
onChange={handleDateChange}
- value={dateTime}
+ value={selectedDateTime}
{...dateCalendarProps}
/>
)}
@@ -217,8 +236,8 @@ export const DateTimePicker = ({
)}
@@ -236,7 +255,7 @@ export const DateTimePicker = ({
label: 'Cancel',
onClick: handleClose,
}}
- sx={(theme) => ({
+ sx={(theme: Theme) => ({
marginBottom: theme.spacing(1),
marginRight: theme.spacing(2),
})}
From 5f35d7082e4aa0b8fbbf60f8a1625b45073f13de Mon Sep 17 00:00:00 2001
From: cpathipa <119517080+cpathipa@users.noreply.github.com>
Date: Tue, 3 Dec 2024 19:25:50 -0600
Subject: [PATCH 21/35] PR feedback
---
.../components/DatePicker/DateTimePicker.tsx | 4 +-
.../DatePicker/DateTimeRangePicker.tsx | 51 ++++++++++++++-----
2 files changed, 40 insertions(+), 15 deletions(-)
diff --git a/packages/ui/src/components/DatePicker/DateTimePicker.tsx b/packages/ui/src/components/DatePicker/DateTimePicker.tsx
index 4d65dc6f1b9..ebe97d07071 100644
--- a/packages/ui/src/components/DatePicker/DateTimePicker.tsx
+++ b/packages/ui/src/components/DatePicker/DateTimePicker.tsx
@@ -161,9 +161,7 @@ export const DateTimePicker = ({
value={
selectedDateTime
? `${selectedDateTime.toFormat(format)}${
- showTimeZone && selectedTimeZone
- ? ` (${selectedTimeZone})`
- : ''
+ selectedTimeZone ? ` (${selectedTimeZone})` : ''
}`
: ''
}
diff --git a/packages/ui/src/components/DatePicker/DateTimeRangePicker.tsx b/packages/ui/src/components/DatePicker/DateTimeRangePicker.tsx
index ed57af7c37c..49b155b9e39 100644
--- a/packages/ui/src/components/DatePicker/DateTimeRangePicker.tsx
+++ b/packages/ui/src/components/DatePicker/DateTimeRangePicker.tsx
@@ -12,11 +12,19 @@ interface DateTimeRangePickerProps {
/** Format for displaying the date-time */
format?: string;
/** Callback when the date-time range changes */
- onChange?: (start: DateTime | null, end: DateTime | null) => void;
+ onChange?: (
+ start: DateTime | null,
+ end: DateTime | null,
+ startTimeZone?: null | string
+ ) => void;
+ /** Whether to show the timezone field for the end date picker */
+ showEndTimeZone?: boolean;
/** Initial or controlled value for the start date-time */
startDateTimeValue?: DateTime | null;
/** Custom labels for the start and end date/time fields */
startLabel?: string;
+ /** Initial or controlled value for the start timezone */
+ startTimeZoneValue?: null | string;
}
export const DateTimeRangePicker = ({
@@ -25,7 +33,8 @@ export const DateTimeRangePicker = ({
format = 'yyyy-MM-dd HH:mm',
onChange,
startDateTimeValue = null,
- startLabel = 'Start Date',
+ startLabel = 'Start Date and Time',
+ startTimeZoneValue = null,
}: DateTimeRangePickerProps) => {
const [startDateTime, setStartDateTime] = useState(
startDateTimeValue
@@ -33,54 +42,72 @@ export const DateTimeRangePicker = ({
const [endDateTime, setEndDateTime] = useState(
endDateTimeValue
);
+ const [startTimeZone, setStartTimeZone] = useState(
+ startTimeZoneValue
+ );
const [error, setError] = useState();
const handleStartDateTimeChange = (newStart: DateTime | null) => {
setStartDateTime(newStart);
- // Reset error if the selection is valid
if (endDateTime && newStart && endDateTime >= newStart) {
- setError(undefined);
+ setError(undefined); // Clear error if valid
}
if (onChange) {
- onChange(newStart, endDateTime);
+ onChange(newStart, endDateTime, startTimeZone);
}
};
const handleEndDateTimeChange = (newEnd: DateTime | null) => {
- // Check if the end date is before the start date
if (startDateTime && newEnd && newEnd < startDateTime) {
- setError('Invalid date and time');
+ setError('End date/time must be after the start date/time.');
} else {
setEndDateTime(newEnd);
- setError(undefined); // Clear the error if the selection is valid
+ setError(undefined);
+ }
+
+ if (onChange) {
+ onChange(startDateTime, newEnd, startTimeZone);
}
+ };
+
+ const handleStartTimeZoneChange = (newTimeZone: null | string) => {
+ setStartTimeZone(newTimeZone);
if (onChange) {
- onChange(startDateTime, newEnd);
+ onChange(startDateTime, endDateTime, newTimeZone);
}
};
return (
+ {/* Start DateTime Picker */}
+
+ {/* End DateTime Picker */}
From 208b2a07a2b35361a7aa14ed7b5101ccd29d9a03 Mon Sep 17 00:00:00 2001
From: cpathipa <119517080+cpathipa@users.noreply.github.com>
Date: Tue, 3 Dec 2024 19:27:51 -0600
Subject: [PATCH 22/35] code cleanup
---
packages/ui/src/components/DatePicker/DateTimeRangePicker.tsx | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/packages/ui/src/components/DatePicker/DateTimeRangePicker.tsx b/packages/ui/src/components/DatePicker/DateTimeRangePicker.tsx
index 49b155b9e39..f4cb77b175a 100644
--- a/packages/ui/src/components/DatePicker/DateTimeRangePicker.tsx
+++ b/packages/ui/src/components/DatePicker/DateTimeRangePicker.tsx
@@ -32,6 +32,7 @@ export const DateTimeRangePicker = ({
endLabel = 'End Date and Time',
format = 'yyyy-MM-dd HH:mm',
onChange,
+ showEndTimeZone = false,
startDateTimeValue = null,
startLabel = 'Start Date and Time',
startTimeZoneValue = null,
@@ -106,7 +107,7 @@ export const DateTimeRangePicker = ({
format={format}
label={endLabel}
onChange={handleEndDateTimeChange}
- showTimeZone={false}
+ showTimeZone={showEndTimeZone}
timeSelectProps={{ label: 'End Time' }}
value={endDateTime}
/>
From d1c3fa1528d70ebef0508b4888a297965b9768c8 Mon Sep 17 00:00:00 2001
From: cpathipa <119517080+cpathipa@users.noreply.github.com>
Date: Wed, 4 Dec 2024 09:24:03 -0600
Subject: [PATCH 23/35] Move DatePicker back to src/components
---
.../DatePicker/DatePicker.stories.tsx | 43 +++
.../components/DatePicker/DatePicker.test.tsx | 80 ++++++
.../src/components/DatePicker/DatePicker.tsx | 92 ++++++
.../DatePicker/DateTimePicker.stories.tsx | 93 ++++++
.../DatePicker/DateTimePicker.test.tsx | 143 ++++++++++
.../components/DatePicker/DateTimePicker.tsx | 265 ++++++++++++++++++
.../DateTimeRangePicker.stories.tsx | 77 +++++
.../DatePicker/DateTimeRangePicker.test.tsx | 59 ++++
.../DatePicker/DateTimeRangePicker.tsx | 116 ++++++++
.../components/DatePicker/TimeZoneSelect.tsx | 64 +++++
packages/ui/src/components/index.ts | 1 -
11 files changed, 1032 insertions(+), 1 deletion(-)
create mode 100644 packages/manager/src/components/DatePicker/DatePicker.stories.tsx
create mode 100644 packages/manager/src/components/DatePicker/DatePicker.test.tsx
create mode 100644 packages/manager/src/components/DatePicker/DatePicker.tsx
create mode 100644 packages/manager/src/components/DatePicker/DateTimePicker.stories.tsx
create mode 100644 packages/manager/src/components/DatePicker/DateTimePicker.test.tsx
create mode 100644 packages/manager/src/components/DatePicker/DateTimePicker.tsx
create mode 100644 packages/manager/src/components/DatePicker/DateTimeRangePicker.stories.tsx
create mode 100644 packages/manager/src/components/DatePicker/DateTimeRangePicker.test.tsx
create mode 100644 packages/manager/src/components/DatePicker/DateTimeRangePicker.tsx
create mode 100644 packages/manager/src/components/DatePicker/TimeZoneSelect.tsx
diff --git a/packages/manager/src/components/DatePicker/DatePicker.stories.tsx b/packages/manager/src/components/DatePicker/DatePicker.stories.tsx
new file mode 100644
index 00000000000..db3925b03eb
--- /dev/null
+++ b/packages/manager/src/components/DatePicker/DatePicker.stories.tsx
@@ -0,0 +1,43 @@
+import { action } from '@storybook/addon-actions';
+import { DateTime } from 'luxon';
+import * as React from 'react';
+
+import { DatePicker } from './DatePicker';
+
+import type { Meta, StoryObj } from '@storybook/react';
+
+type Story = StoryObj;
+
+export const ControlledExample: Story = {
+ render: () => {
+ const ControlledDatePicker = () => {
+ const [selectedDate, setSelectedDate] = React.useState(
+ DateTime.now()
+ );
+
+ const handleChange = (newDate: DateTime | null) => {
+ setSelectedDate(newDate);
+ action('Controlled date change')(newDate?.toISO());
+ };
+
+ return (
+
+ );
+ };
+
+ return ;
+ },
+};
+
+const meta: Meta = {
+ component: DatePicker,
+ title: 'Components/DatePicker',
+};
+
+export default meta;
diff --git a/packages/manager/src/components/DatePicker/DatePicker.test.tsx b/packages/manager/src/components/DatePicker/DatePicker.test.tsx
new file mode 100644
index 00000000000..e051d160ec5
--- /dev/null
+++ b/packages/manager/src/components/DatePicker/DatePicker.test.tsx
@@ -0,0 +1,80 @@
+import { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { DateTime } from 'luxon';
+import * as React from 'react';
+
+import { renderWithTheme } from 'src/utilities/testHelpers';
+
+import { DatePicker } from './DatePicker';
+
+import type { DatePickerProps } from './DatePicker';
+
+const props: DatePickerProps = {
+ onChange: vi.fn(),
+ placeholder: 'Pick a date',
+ textFieldProps: { errorText: 'Invalid date', label: 'Select a date' },
+ value: null,
+};
+
+describe('DatePicker', () => {
+ it('should render the DatePicker component', () => {
+ renderWithTheme();
+ const DatePickerField = screen.getByRole('textbox', {
+ name: 'Select a date',
+ });
+
+ expect(DatePickerField).toBeVisible();
+ });
+
+ it('should handle value changes', async () => {
+ renderWithTheme();
+
+ const calendarButton = screen.getByRole('button', { name: 'Choose date' });
+
+ // Click the calendar button to open the date picker
+ await userEvent.click(calendarButton);
+
+ // Find a date button to click (e.g., the 15th of the month)
+ const dateToSelect = screen.getByRole('gridcell', { name: '15' });
+ await userEvent.click(dateToSelect);
+
+ // Check if onChange was called after selecting a date
+ expect(props.onChange).toHaveBeenCalled();
+ });
+
+ it('should display the error text when provided', () => {
+ renderWithTheme();
+ const errorMessage = screen.getByText('Invalid date');
+ expect(errorMessage).toBeVisible();
+ });
+
+ it('should display the helper text when provided', () => {
+ renderWithTheme();
+ const helperText = screen.getByText('Choose a valid date');
+ expect(helperText).toBeVisible();
+ });
+
+ it('should use the default format when no format is specified', () => {
+ renderWithTheme(
+
+ );
+ const datePickerField = screen.getByRole('textbox', {
+ name: 'Select a date',
+ });
+ expect(datePickerField).toHaveValue('2024-10-25');
+ });
+
+ it('should handle the custom format correctly', () => {
+ renderWithTheme(
+
+ );
+ const datePickerField = screen.getByRole('textbox', {
+ name: 'Select a date',
+ });
+ expect(datePickerField).toHaveValue('25/10/2024');
+ });
+});
diff --git a/packages/manager/src/components/DatePicker/DatePicker.tsx b/packages/manager/src/components/DatePicker/DatePicker.tsx
new file mode 100644
index 00000000000..a3a5a722e82
--- /dev/null
+++ b/packages/manager/src/components/DatePicker/DatePicker.tsx
@@ -0,0 +1,92 @@
+import { TextField } from '@linode/ui';
+import { useTheme } from '@mui/material/styles';
+import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon';
+import { DatePicker as MuiDatePicker } from '@mui/x-date-pickers/DatePicker';
+import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
+import React from 'react';
+
+import type { TextFieldProps } from '@linode/ui';
+import type { DatePickerProps as MuiDatePickerProps } from '@mui/x-date-pickers/DatePicker';
+import type { DateTime } from 'luxon';
+
+export interface DatePickerProps
+ extends Omit, 'onChange' | 'value'> {
+ /** Error text to display below the input */
+ errorText?: string;
+ /** Format of the date when rendered in the input field. */
+ format?: string;
+ /** Helper text to display below the input */
+ helperText?: string;
+ /** Label to display for the date picker input */
+ label?: string;
+ /** Callback function fired when the value changes */
+ onChange?: (newDate: DateTime | null) => void;
+ /** Placeholder text for the date picker input */
+ placeholder?: string;
+ /** Additional props to pass to the underlying TextField component */
+ textFieldProps?: Omit;
+ /** The currently selected date */
+ value?: DateTime | null;
+}
+
+export const DatePicker = ({
+ format = 'yyyy-MM-dd',
+ helperText = '',
+ label = 'Select a date',
+ onChange,
+ placeholder = 'Pick a date',
+ textFieldProps,
+ value = null,
+ ...props
+}: DatePickerProps) => {
+ const theme = useTheme();
+
+ const onChangeHandler = (newDate: DateTime | null) => {
+ if (onChange) {
+ onChange(newDate);
+ }
+ };
+
+ return (
+
+
+
+ );
+};
diff --git a/packages/manager/src/components/DatePicker/DateTimePicker.stories.tsx b/packages/manager/src/components/DatePicker/DateTimePicker.stories.tsx
new file mode 100644
index 00000000000..32d65dfa0b7
--- /dev/null
+++ b/packages/manager/src/components/DatePicker/DateTimePicker.stories.tsx
@@ -0,0 +1,93 @@
+import { action } from '@storybook/addon-actions';
+import { DateTime } from 'luxon';
+import * as React from 'react';
+
+import { DateTimePicker } from './DateTimePicker';
+
+import type { Meta, StoryObj } from '@storybook/react';
+
+type Story = StoryObj;
+
+export const ControlledExample: Story = {
+ render: () => {
+ const ControlledDateTimePicker = () => {
+ const [
+ selectedDateTime,
+ setSelectedDateTime,
+ ] = React.useState(DateTime.now());
+
+ const handleChange = (newDateTime: DateTime | null) => {
+ setSelectedDateTime(newDateTime);
+ action('Controlled dateTime change')(newDateTime?.toISO());
+ };
+
+ return (
+
+ );
+ };
+
+ return ;
+ },
+};
+
+export const DefaultExample: Story = {
+ args: {
+ label: 'Default Date-Time Picker',
+ onApply: action('Apply clicked'),
+ onCancel: action('Cancel clicked'),
+ onChange: action('Date-Time selected'),
+ placeholder: 'yyyy-MM-dd HH:mm',
+ },
+};
+
+export const WithErrorText: Story = {
+ args: {
+ errorText: 'This field is required',
+ label: 'Date-Time Picker with Error',
+ onApply: action('Apply clicked with error'),
+ onCancel: action('Cancel clicked with error'),
+ onChange: action('Date-Time selected with error'),
+ placeholder: 'yyyy-MM-dd HH:mm',
+ },
+};
+
+const meta: Meta = {
+ argTypes: {
+ errorText: {
+ control: { type: 'text' },
+ },
+ format: {
+ control: { type: 'text' },
+ },
+ label: {
+ control: { type: 'text' },
+ },
+ onApply: { action: 'applyClicked' },
+ onCancel: { action: 'cancelClicked' },
+ onChange: { action: 'dateTimeChanged' },
+ placeholder: {
+ control: { type: 'text' },
+ },
+ },
+ args: {
+ format: 'yyyy-MM-dd HH:mm',
+ label: 'Date-Time Picker',
+ placeholder: 'Select a date and time',
+ },
+ component: DateTimePicker,
+ title: 'Components/DateTimePicker',
+};
+
+export default meta;
diff --git a/packages/manager/src/components/DatePicker/DateTimePicker.test.tsx b/packages/manager/src/components/DatePicker/DateTimePicker.test.tsx
new file mode 100644
index 00000000000..12f1795a747
--- /dev/null
+++ b/packages/manager/src/components/DatePicker/DateTimePicker.test.tsx
@@ -0,0 +1,143 @@
+import { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { DateTime } from 'luxon';
+import * as React from 'react';
+
+import { renderWithTheme } from 'src/utilities/testHelpers';
+
+import { DateTimePicker } from './DateTimePicker';
+
+import type { DateTimePickerProps } from './DateTimePicker';
+
+const defaultProps: DateTimePickerProps = {
+ label: 'Select Date and Time',
+ onApply: vi.fn(),
+ onCancel: vi.fn(),
+ onChange: vi.fn(),
+ placeholder: 'yyyy-MM-dd HH:mm',
+ value: DateTime.fromISO('2024-10-25T15:30:00'),
+};
+
+describe('DateTimePicker Component', () => {
+ it('should render the DateTimePicker component with the correct label and placeholder', () => {
+ renderWithTheme();
+ const textField = screen.getByRole('textbox', {
+ name: 'Select Date and Time',
+ });
+ expect(textField).toBeVisible();
+ expect(textField).toHaveAttribute('placeholder', 'yyyy-MM-dd HH:mm');
+ });
+
+ it('should open the Popover when the TextField is clicked', async () => {
+ renderWithTheme();
+ const textField = screen.getByRole('textbox', {
+ name: 'Select Date and Time',
+ });
+ await userEvent.click(textField);
+ expect(screen.getByRole('dialog')).toBeVisible(); // Verifying the Popover is open
+ });
+
+ it('should call onCancel when the Cancel button is clicked', async () => {
+ renderWithTheme();
+ await userEvent.click(
+ screen.getByRole('textbox', { name: 'Select Date and Time' })
+ );
+ const cancelButton = screen.getByRole('button', { name: /Cancel/i });
+ await userEvent.click(cancelButton);
+ expect(defaultProps.onCancel).toHaveBeenCalled();
+ });
+
+ it('should call onApply when the Apply button is clicked', async () => {
+ renderWithTheme();
+ await userEvent.click(
+ screen.getByRole('textbox', { name: 'Select Date and Time' })
+ );
+ const applyButton = screen.getByRole('button', { name: /Apply/i });
+ await userEvent.click(applyButton);
+ expect(defaultProps.onApply).toHaveBeenCalled();
+ expect(defaultProps.onChange).toHaveBeenCalledWith(expect.any(DateTime)); // Ensuring onChange was called with a DateTime object
+ });
+
+ it('should handle date changes correctly', async () => {
+ renderWithTheme();
+ await userEvent.click(
+ screen.getByRole('textbox', { name: 'Select Date and Time' })
+ );
+
+ // Simulate selecting a date (e.g., 15th of the month)
+ const dateButton = screen.getByRole('gridcell', { name: '15' });
+ await userEvent.click(dateButton);
+
+ // Check that the displayed value has been updated correctly (this assumes the date format)
+ expect(defaultProps.onChange).toHaveBeenCalled();
+ });
+
+ it('should handle timezone changes correctly', async () => {
+ const timezoneChangeMock = vi.fn(); // Create a mock function
+
+ const updatedProps = {
+ ...defaultProps,
+ timeZoneSelectProps: { onChange: timezoneChangeMock, value: 'UTC' },
+ };
+
+ renderWithTheme();
+
+ await userEvent.click(
+ screen.getByRole('textbox', { name: 'Select Date and Time' })
+ );
+
+ // Simulate selecting a timezone from the TimeZoneSelect
+ const timezoneInput = screen.getByPlaceholderText(/Choose a Timezone/i);
+ await userEvent.click(timezoneInput);
+
+ // Select a timezone from the dropdown options
+ await userEvent.click(
+ screen.getByRole('option', { name: '(GMT -11:00) Niue Time' })
+ );
+
+ // Click the Apply button to trigger the change
+ await userEvent.click(screen.getByRole('button', { name: 'Apply' }));
+
+ // Verify that the onChange function was called with the expected value
+ expect(timezoneChangeMock).toHaveBeenCalledWith('Pacific/Niue');
+ });
+
+ it('should display the error text when provided', () => {
+ renderWithTheme(
+
+ );
+ expect(screen.getByText(/Invalid date-time/i)).toBeVisible();
+ });
+
+ it('should format the date-time correctly when a custom format is provided', () => {
+ renderWithTheme(
+
+ );
+ const textField = screen.getByRole('textbox', {
+ name: 'Select Date and Time',
+ });
+
+ expect(textField).toHaveValue('25/10/2024 15:30');
+ });
+ it('should not render the time selector when showTime is false', async () => {
+ renderWithTheme();
+ await userEvent.click(
+ screen.getByRole('textbox', { name: 'Select Date and Time' })
+ );
+ const timePicker = screen.queryByLabelText(/Select Time/i); // Label from timeSelectProps
+ expect(timePicker).not.toBeInTheDocument();
+ });
+
+ it('should not render the timezone selector when showTimeZone is false', async () => {
+ renderWithTheme();
+ await userEvent.click(
+ screen.getByRole('textbox', { name: 'Select Date and Time' })
+ );
+ const timeZoneSelect = screen.queryByLabelText(/Timezone/i); // Label from timeZoneSelectProps
+ expect(timeZoneSelect).not.toBeInTheDocument();
+ });
+});
diff --git a/packages/manager/src/components/DatePicker/DateTimePicker.tsx b/packages/manager/src/components/DatePicker/DateTimePicker.tsx
new file mode 100644
index 00000000000..027b25822d2
--- /dev/null
+++ b/packages/manager/src/components/DatePicker/DateTimePicker.tsx
@@ -0,0 +1,265 @@
+import { TextField } from '@linode/ui';
+import { Divider } from '@linode/ui';
+import { Box } from '@linode/ui';
+import { Grid, Popover } from '@mui/material';
+import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon';
+import { DateCalendar } from '@mui/x-date-pickers/DateCalendar';
+import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
+import { TimePicker } from '@mui/x-date-pickers/TimePicker';
+import React, { useEffect, useState } from 'react';
+
+import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel';
+
+import { TimeZoneSelect } from './TimeZoneSelect';
+
+import type { TextFieldProps } from '@linode/ui';
+import type { Theme } from '@mui/material/styles';
+import type { DateCalendarProps } from '@mui/x-date-pickers/DateCalendar';
+import type { DateTime } from 'luxon';
+
+export interface DateTimePickerProps {
+ /** Additional props for the DateCalendar */
+ dateCalendarProps?: Partial>;
+ /** Error text for the date picker field */
+ errorText?: string;
+ /** Format for displaying the date-time */
+ format?: string;
+ /** Label for the input field */
+ label?: string;
+ /** Callback when the "Apply" button is clicked */
+ onApply?: () => void;
+ /** Callback when the "Cancel" button is clicked */
+ onCancel?: () => void;
+ /** Callback when date-time changes */
+ onChange?: (dateTime: DateTime | null) => void;
+ /** Placeholder text for the input field */
+ placeholder?: string;
+ /** Whether to show the time selector */
+ showTime?: boolean;
+ /** Whether to show the timezone selector */
+ showTimeZone?: boolean;
+ /** Props for customizing the TimePicker component */
+ timeSelectProps?: {
+ label?: string;
+ onChange?: (time: null | string) => void;
+ value?: null | string;
+ };
+ /** Props for customizing the TimeZoneSelect component */
+ timeZoneSelectProps?: {
+ label?: string;
+ onChange?: (timezone: string) => void;
+ value?: null | string;
+ };
+ /** Initial or controlled dateTime value */
+ value?: DateTime | null;
+}
+
+export const DateTimePicker = ({
+ dateCalendarProps = {},
+ errorText = '',
+ format = 'yyyy-MM-dd HH:mm',
+ label = 'Select Date and Time',
+ onApply,
+ onCancel,
+ onChange,
+ placeholder = 'yyyy-MM-dd HH:mm',
+ showTime = true,
+ showTimeZone = true,
+ timeSelectProps = {},
+ timeZoneSelectProps = {},
+ value = null,
+}: DateTimePickerProps) => {
+ const [anchorEl, setAnchorEl] = useState(null);
+ const [selectedDateTime, setSelectedDateTime] = useState(
+ value
+ );
+ const [selectedTimeZone, setSelectedTimeZone] = useState(
+ timeZoneSelectProps.value || null
+ );
+
+ const TimePickerFieldProps: TextFieldProps = {
+ label: timeSelectProps?.label ?? 'Select Time',
+ noMarginTop: true,
+ };
+
+ useEffect(() => {
+ if (value) {
+ setSelectedDateTime(value);
+ }
+ }, [value]);
+
+ useEffect(() => {
+ if (timeZoneSelectProps.value) {
+ setSelectedTimeZone(timeZoneSelectProps.value);
+ }
+ }, [timeZoneSelectProps.value]);
+
+ const handleOpen = (event: React.MouseEvent) => {
+ setAnchorEl(event.currentTarget);
+ };
+
+ const handleClose = () => {
+ setAnchorEl(null);
+ if (onCancel) {
+ onCancel();
+ }
+ };
+
+ const handleApply = () => {
+ setAnchorEl(null);
+ if (onChange) {
+ onChange(selectedDateTime);
+ }
+ if (onApply) {
+ onApply();
+ }
+ };
+
+ const handleDateChange = (newDate: DateTime | null) => {
+ setSelectedDateTime((prev) =>
+ newDate
+ ? newDate.set({
+ hour: prev?.hour || 0,
+ minute: prev?.minute || 0,
+ })
+ : null
+ );
+ };
+
+ const handleTimeChange = (newTime: DateTime | null) => {
+ if (newTime) {
+ setSelectedDateTime((prev) => {
+ if (prev) {
+ // Ensure hour and minute are valid numbers
+ const newHour = newTime.hour;
+ const newMinute = newTime.minute;
+
+ if (typeof newHour === 'number' && typeof newMinute === 'number') {
+ return prev.set({ hour: newHour, minute: newMinute });
+ }
+ }
+ // Return the current `prev` value if newTime is invalid
+ return prev;
+ });
+ }
+ if (timeSelectProps.onChange && newTime) {
+ timeSelectProps.onChange(newTime.toISOTime());
+ }
+ };
+
+ const handleTimeZoneChange = (newTimeZone: string) => {
+ setSelectedTimeZone(newTimeZone);
+ if (timeZoneSelectProps.onChange) {
+ timeZoneSelectProps.onChange(newTimeZone);
+ }
+ };
+
+ return (
+
+
+
+
+
+
+ ({
+ '& .MuiDayCalendar-weekContainer, & .MuiDayCalendar-header': {
+ justifyContent: 'space-between',
+ },
+ '& .MuiDayCalendar-weekDayLabel': {
+ fontSize: '0.875rem',
+ },
+ '& .MuiPickersCalendarHeader-label': {
+ fontFamily: theme.font.bold,
+ },
+ '& .MuiPickersCalendarHeader-root': {
+ borderBottom: `1px solid ${theme.borderColors.divider}`,
+ fontSize: '0.875rem',
+ paddingBottom: theme.spacing(1),
+ },
+ '& .MuiPickersDay-root': {
+ fontSize: '0.875rem',
+ margin: `${theme.spacing(0.5)}px`,
+ },
+ borderRadius: `${theme.spacing(2)}`,
+ borderWidth: '0px',
+ })}
+ onChange={handleDateChange}
+ value={selectedDateTime}
+ {...dateCalendarProps}
+ />
+
+ {showTime && (
+
+
+
+ )}
+ {showTimeZone && (
+
+
+
+ )}
+
+
+
+
+ ({
+ marginBottom: theme.spacing(1),
+ marginRight: theme.spacing(2),
+ })}
+ />
+
+
+
+ );
+};
diff --git a/packages/manager/src/components/DatePicker/DateTimeRangePicker.stories.tsx b/packages/manager/src/components/DatePicker/DateTimeRangePicker.stories.tsx
new file mode 100644
index 00000000000..70ad6b2a39d
--- /dev/null
+++ b/packages/manager/src/components/DatePicker/DateTimeRangePicker.stories.tsx
@@ -0,0 +1,77 @@
+import { action } from '@storybook/addon-actions';
+import { DateTime } from 'luxon';
+import * as React from 'react';
+
+import { DateTimeRangePicker } from './DateTimeRangePicker';
+
+import type { Meta, StoryObj } from '@storybook/react';
+
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: {
+ endLabel: 'End Date and Time',
+ format: 'yyyy-MM-dd HH:mm',
+ onChange: action('DateTime range changed'),
+ startLabel: 'Start Date and Time',
+ },
+ render: (args) => ,
+};
+
+export const WithInitialValues: Story = {
+ render: () => {
+ const ComponentWithState: React.FC = () => {
+ const [start, setStart] = React.useState(
+ DateTime.now().minus({ days: 1 })
+ );
+ const [end, setEnd] = React.useState(DateTime.now());
+
+ const handleDateChange = (
+ newStart: DateTime | null,
+ newEnd: DateTime | null
+ ) => {
+ setStart(newStart);
+ setEnd(newEnd);
+ action('DateTime range changed')(newStart?.toISO(), newEnd?.toISO());
+ };
+
+ return (
+
+ );
+ };
+
+ return ;
+ },
+};
+
+const meta: Meta = {
+ argTypes: {
+ endLabel: {
+ control: 'text',
+ description: 'Label for the end date picker',
+ },
+ format: {
+ control: 'text',
+ description: 'Format for displaying the date-time',
+ },
+ onChange: {
+ action: 'DateTime range changed',
+ description: 'Callback when the date-time range changes',
+ },
+ startLabel: {
+ control: 'text',
+ description: 'Label for the start date picker',
+ },
+ },
+ component: DateTimeRangePicker,
+ title: 'Components/DateTimeRangePicker',
+};
+
+export default meta;
diff --git a/packages/manager/src/components/DatePicker/DateTimeRangePicker.test.tsx b/packages/manager/src/components/DatePicker/DateTimeRangePicker.test.tsx
new file mode 100644
index 00000000000..f3421b684c3
--- /dev/null
+++ b/packages/manager/src/components/DatePicker/DateTimeRangePicker.test.tsx
@@ -0,0 +1,59 @@
+import { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import * as React from 'react';
+
+import { renderWithTheme } from 'src/utilities/testHelpers';
+
+import { DateTimeRangePicker } from './DateTimeRangePicker';
+
+describe('DateTimeRangePicker Component', () => {
+ const onChangeMock = vi.fn();
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('should render start and end DateTimePickers with correct labels', () => {
+ renderWithTheme();
+
+ expect(screen.getByLabelText('Start Date')).toBeVisible();
+ expect(screen.getByLabelText('End Date and Time')).toBeVisible();
+ });
+
+ it('should call onChange when start date is changed', async () => {
+ renderWithTheme();
+
+ // Open start date picker
+ await userEvent.click(screen.getByLabelText('Start Date'));
+
+ await userEvent.click(screen.getByRole('gridcell', { name: '10' }));
+ await userEvent.click(screen.getByRole('button', { name: 'Apply' }));
+
+ // Check if the onChange function is called with the expected DateTime value
+ expect(onChangeMock).toHaveBeenCalledWith(
+ expect.objectContaining({ day: 10 }),
+ null
+ );
+ });
+
+ it('should show error when end date-time is before start date-time', async () => {
+ renderWithTheme();
+
+ // Set start date-time to the 15th
+ const startDateField = screen.getByLabelText('Start Date');
+ await userEvent.click(startDateField);
+ await userEvent.click(screen.getByRole('gridcell', { name: '15' }));
+ await userEvent.click(screen.getByRole('button', { name: 'Apply' }));
+
+ // Open the end date picker
+ const endDateField = screen.getByLabelText('End Date and Time');
+ await userEvent.click(endDateField);
+
+ // Check if the date before the start date is disabled via a class or attribute
+ const invalidDate = screen.getByRole('gridcell', { name: '10' });
+ expect(invalidDate).toHaveClass('Mui-disabled'); // or check for the specific attribute used
+
+ // Confirm error message is not shown since the click was blocked
+ expect(screen.queryByText('Invalid date and Time')).not.toBeInTheDocument();
+ });
+});
diff --git a/packages/manager/src/components/DatePicker/DateTimeRangePicker.tsx b/packages/manager/src/components/DatePicker/DateTimeRangePicker.tsx
new file mode 100644
index 00000000000..f4cb77b175a
--- /dev/null
+++ b/packages/manager/src/components/DatePicker/DateTimeRangePicker.tsx
@@ -0,0 +1,116 @@
+import { Box } from '@linode/ui';
+import { DateTime } from 'luxon';
+import React, { useState } from 'react';
+
+import { DateTimePicker } from './DateTimePicker';
+
+interface DateTimeRangePickerProps {
+ /** Initial or controlled value for the end date-time */
+ endDateTimeValue?: DateTime | null;
+ /** Custom labels for the start and end date/time fields */
+ endLabel?: string;
+ /** Format for displaying the date-time */
+ format?: string;
+ /** Callback when the date-time range changes */
+ onChange?: (
+ start: DateTime | null,
+ end: DateTime | null,
+ startTimeZone?: null | string
+ ) => void;
+ /** Whether to show the timezone field for the end date picker */
+ showEndTimeZone?: boolean;
+ /** Initial or controlled value for the start date-time */
+ startDateTimeValue?: DateTime | null;
+ /** Custom labels for the start and end date/time fields */
+ startLabel?: string;
+ /** Initial or controlled value for the start timezone */
+ startTimeZoneValue?: null | string;
+}
+
+export const DateTimeRangePicker = ({
+ endDateTimeValue = null,
+ endLabel = 'End Date and Time',
+ format = 'yyyy-MM-dd HH:mm',
+ onChange,
+ showEndTimeZone = false,
+ startDateTimeValue = null,
+ startLabel = 'Start Date and Time',
+ startTimeZoneValue = null,
+}: DateTimeRangePickerProps) => {
+ const [startDateTime, setStartDateTime] = useState(
+ startDateTimeValue
+ );
+ const [endDateTime, setEndDateTime] = useState(
+ endDateTimeValue
+ );
+ const [startTimeZone, setStartTimeZone] = useState(
+ startTimeZoneValue
+ );
+ const [error, setError] = useState();
+
+ const handleStartDateTimeChange = (newStart: DateTime | null) => {
+ setStartDateTime(newStart);
+
+ if (endDateTime && newStart && endDateTime >= newStart) {
+ setError(undefined); // Clear error if valid
+ }
+
+ if (onChange) {
+ onChange(newStart, endDateTime, startTimeZone);
+ }
+ };
+
+ const handleEndDateTimeChange = (newEnd: DateTime | null) => {
+ if (startDateTime && newEnd && newEnd < startDateTime) {
+ setError('End date/time must be after the start date/time.');
+ } else {
+ setEndDateTime(newEnd);
+ setError(undefined);
+ }
+
+ if (onChange) {
+ onChange(startDateTime, newEnd, startTimeZone);
+ }
+ };
+
+ const handleStartTimeZoneChange = (newTimeZone: null | string) => {
+ setStartTimeZone(newTimeZone);
+
+ if (onChange) {
+ onChange(startDateTime, endDateTime, newTimeZone);
+ }
+ };
+
+ return (
+
+ {/* Start DateTime Picker */}
+
+
+ {/* End DateTime Picker */}
+
+
+ );
+};
diff --git a/packages/manager/src/components/DatePicker/TimeZoneSelect.tsx b/packages/manager/src/components/DatePicker/TimeZoneSelect.tsx
new file mode 100644
index 00000000000..19312098007
--- /dev/null
+++ b/packages/manager/src/components/DatePicker/TimeZoneSelect.tsx
@@ -0,0 +1,64 @@
+import { DateTime } from 'luxon';
+import React from 'react';
+
+import { timezones } from 'src/assets/timezones/timezones';
+import { Autocomplete } from 'src/components/Autocomplete/Autocomplete';
+
+type Timezone = typeof timezones[number];
+
+interface TimeZoneSelectProps {
+ disabled?: boolean;
+ errorText?: string;
+ label?: string;
+ noMarginTop?: boolean;
+ onChange: (timezone: string) => void;
+ value: null | string;
+}
+
+const getOptionLabel = ({ label, offset }: Timezone) => {
+ const minutes = (Math.abs(offset) % 60).toLocaleString(undefined, {
+ minimumIntegerDigits: 2,
+ useGrouping: false,
+ });
+ const hours = Math.floor(Math.abs(offset) / 60);
+ const isPositive = Math.abs(offset) === offset ? '+' : '-';
+
+ return `(GMT ${isPositive}${hours}:${minutes}) ${label}`;
+};
+
+const getTimezoneOptions = () => {
+ return timezones
+ .map((tz) => {
+ const offset = DateTime.now().setZone(tz.name).offset;
+ const label = getOptionLabel({ ...tz, offset });
+ return { label, offset, value: tz.name };
+ })
+ .sort((a, b) => a.offset - b.offset);
+};
+
+const timezoneOptions = getTimezoneOptions();
+
+export const TimeZoneSelect = ({
+ disabled = false,
+ errorText,
+ label = 'Timezone',
+ noMarginTop = false,
+ onChange,
+ value,
+}: TimeZoneSelectProps) => {
+ return (
+ option.value === value) ?? undefined
+ }
+ autoHighlight
+ disabled={disabled}
+ errorText={errorText}
+ label={label}
+ noMarginTop={noMarginTop}
+ onChange={(e, option) => onChange(option?.value || '')}
+ options={timezoneOptions}
+ placeholder="Choose a Timezone"
+ />
+ );
+};
diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts
index f64e7b4105a..db690f6940e 100644
--- a/packages/ui/src/components/index.ts
+++ b/packages/ui/src/components/index.ts
@@ -26,4 +26,3 @@ export * from './Tooltip';
export * from './TooltipIcon';
export * from './Typography';
export * from './VisibilityTooltip';
-export * from './DatePicker';
From 6186d267968389e09591eb4e497248b07391288c Mon Sep 17 00:00:00 2001
From: cpathipa <119517080+cpathipa@users.noreply.github.com>
Date: Wed, 4 Dec 2024 10:11:33 -0600
Subject: [PATCH 24/35] Reverting changes
---
packages/ui/package.json | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 3a85d11a12a..a0926371058 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -20,9 +20,7 @@
"@emotion/styled": "^11.11.0",
"@mui/material": "^5.14.7",
"react": "^18.2.0",
- "react-dom": "^18.2.0",
- "@mui/x-date-pickers": "^7.12.0",
- "luxon": "3.4.4"
+ "react-dom": "^18.2.0"
},
"scripts": {
"start": "tsc -w --preserveWatchOutput",
From 2cd83660d85c758088aa3b1e2c8c86f9d19a7045 Mon Sep 17 00:00:00 2001
From: cpathipa <119517080+cpathipa@users.noreply.github.com>
Date: Wed, 4 Dec 2024 10:30:39 -0600
Subject: [PATCH 25/35] Code cleanup
---
.../DatePicker/DatePicker.stories.tsx | 43 ---
.../components/DatePicker/DatePicker.test.tsx | 80 ------
.../src/components/DatePicker/DatePicker.tsx | 92 ------
.../DatePicker/DateTimePicker.stories.tsx | 93 ------
.../DatePicker/DateTimePicker.test.tsx | 143 ----------
.../components/DatePicker/DateTimePicker.tsx | 265 ------------------
.../DateTimeRangePicker.stories.tsx | 77 -----
.../DatePicker/DateTimeRangePicker.test.tsx | 59 ----
.../DatePicker/DateTimeRangePicker.tsx | 116 --------
.../components/DatePicker/TimeZoneSelect.tsx | 64 -----
.../ui/src/components/DatePicker/index.ts | 1 -
11 files changed, 1033 deletions(-)
delete mode 100644 packages/ui/src/components/DatePicker/DatePicker.stories.tsx
delete mode 100644 packages/ui/src/components/DatePicker/DatePicker.test.tsx
delete mode 100644 packages/ui/src/components/DatePicker/DatePicker.tsx
delete mode 100644 packages/ui/src/components/DatePicker/DateTimePicker.stories.tsx
delete mode 100644 packages/ui/src/components/DatePicker/DateTimePicker.test.tsx
delete mode 100644 packages/ui/src/components/DatePicker/DateTimePicker.tsx
delete mode 100644 packages/ui/src/components/DatePicker/DateTimeRangePicker.stories.tsx
delete mode 100644 packages/ui/src/components/DatePicker/DateTimeRangePicker.test.tsx
delete mode 100644 packages/ui/src/components/DatePicker/DateTimeRangePicker.tsx
delete mode 100644 packages/ui/src/components/DatePicker/TimeZoneSelect.tsx
delete mode 100644 packages/ui/src/components/DatePicker/index.ts
diff --git a/packages/ui/src/components/DatePicker/DatePicker.stories.tsx b/packages/ui/src/components/DatePicker/DatePicker.stories.tsx
deleted file mode 100644
index db3925b03eb..00000000000
--- a/packages/ui/src/components/DatePicker/DatePicker.stories.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import { action } from '@storybook/addon-actions';
-import { DateTime } from 'luxon';
-import * as React from 'react';
-
-import { DatePicker } from './DatePicker';
-
-import type { Meta, StoryObj } from '@storybook/react';
-
-type Story = StoryObj;
-
-export const ControlledExample: Story = {
- render: () => {
- const ControlledDatePicker = () => {
- const [selectedDate, setSelectedDate] = React.useState(
- DateTime.now()
- );
-
- const handleChange = (newDate: DateTime | null) => {
- setSelectedDate(newDate);
- action('Controlled date change')(newDate?.toISO());
- };
-
- return (
-
- );
- };
-
- return ;
- },
-};
-
-const meta: Meta = {
- component: DatePicker,
- title: 'Components/DatePicker',
-};
-
-export default meta;
diff --git a/packages/ui/src/components/DatePicker/DatePicker.test.tsx b/packages/ui/src/components/DatePicker/DatePicker.test.tsx
deleted file mode 100644
index e051d160ec5..00000000000
--- a/packages/ui/src/components/DatePicker/DatePicker.test.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-import { screen } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import { DateTime } from 'luxon';
-import * as React from 'react';
-
-import { renderWithTheme } from 'src/utilities/testHelpers';
-
-import { DatePicker } from './DatePicker';
-
-import type { DatePickerProps } from './DatePicker';
-
-const props: DatePickerProps = {
- onChange: vi.fn(),
- placeholder: 'Pick a date',
- textFieldProps: { errorText: 'Invalid date', label: 'Select a date' },
- value: null,
-};
-
-describe('DatePicker', () => {
- it('should render the DatePicker component', () => {
- renderWithTheme();
- const DatePickerField = screen.getByRole('textbox', {
- name: 'Select a date',
- });
-
- expect(DatePickerField).toBeVisible();
- });
-
- it('should handle value changes', async () => {
- renderWithTheme();
-
- const calendarButton = screen.getByRole('button', { name: 'Choose date' });
-
- // Click the calendar button to open the date picker
- await userEvent.click(calendarButton);
-
- // Find a date button to click (e.g., the 15th of the month)
- const dateToSelect = screen.getByRole('gridcell', { name: '15' });
- await userEvent.click(dateToSelect);
-
- // Check if onChange was called after selecting a date
- expect(props.onChange).toHaveBeenCalled();
- });
-
- it('should display the error text when provided', () => {
- renderWithTheme();
- const errorMessage = screen.getByText('Invalid date');
- expect(errorMessage).toBeVisible();
- });
-
- it('should display the helper text when provided', () => {
- renderWithTheme();
- const helperText = screen.getByText('Choose a valid date');
- expect(helperText).toBeVisible();
- });
-
- it('should use the default format when no format is specified', () => {
- renderWithTheme(
-
- );
- const datePickerField = screen.getByRole('textbox', {
- name: 'Select a date',
- });
- expect(datePickerField).toHaveValue('2024-10-25');
- });
-
- it('should handle the custom format correctly', () => {
- renderWithTheme(
-
- );
- const datePickerField = screen.getByRole('textbox', {
- name: 'Select a date',
- });
- expect(datePickerField).toHaveValue('25/10/2024');
- });
-});
diff --git a/packages/ui/src/components/DatePicker/DatePicker.tsx b/packages/ui/src/components/DatePicker/DatePicker.tsx
deleted file mode 100644
index a3a5a722e82..00000000000
--- a/packages/ui/src/components/DatePicker/DatePicker.tsx
+++ /dev/null
@@ -1,92 +0,0 @@
-import { TextField } from '@linode/ui';
-import { useTheme } from '@mui/material/styles';
-import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon';
-import { DatePicker as MuiDatePicker } from '@mui/x-date-pickers/DatePicker';
-import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
-import React from 'react';
-
-import type { TextFieldProps } from '@linode/ui';
-import type { DatePickerProps as MuiDatePickerProps } from '@mui/x-date-pickers/DatePicker';
-import type { DateTime } from 'luxon';
-
-export interface DatePickerProps
- extends Omit, 'onChange' | 'value'> {
- /** Error text to display below the input */
- errorText?: string;
- /** Format of the date when rendered in the input field. */
- format?: string;
- /** Helper text to display below the input */
- helperText?: string;
- /** Label to display for the date picker input */
- label?: string;
- /** Callback function fired when the value changes */
- onChange?: (newDate: DateTime | null) => void;
- /** Placeholder text for the date picker input */
- placeholder?: string;
- /** Additional props to pass to the underlying TextField component */
- textFieldProps?: Omit;
- /** The currently selected date */
- value?: DateTime | null;
-}
-
-export const DatePicker = ({
- format = 'yyyy-MM-dd',
- helperText = '',
- label = 'Select a date',
- onChange,
- placeholder = 'Pick a date',
- textFieldProps,
- value = null,
- ...props
-}: DatePickerProps) => {
- const theme = useTheme();
-
- const onChangeHandler = (newDate: DateTime | null) => {
- if (onChange) {
- onChange(newDate);
- }
- };
-
- return (
-
-
-
- );
-};
diff --git a/packages/ui/src/components/DatePicker/DateTimePicker.stories.tsx b/packages/ui/src/components/DatePicker/DateTimePicker.stories.tsx
deleted file mode 100644
index 32d65dfa0b7..00000000000
--- a/packages/ui/src/components/DatePicker/DateTimePicker.stories.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-import { action } from '@storybook/addon-actions';
-import { DateTime } from 'luxon';
-import * as React from 'react';
-
-import { DateTimePicker } from './DateTimePicker';
-
-import type { Meta, StoryObj } from '@storybook/react';
-
-type Story = StoryObj;
-
-export const ControlledExample: Story = {
- render: () => {
- const ControlledDateTimePicker = () => {
- const [
- selectedDateTime,
- setSelectedDateTime,
- ] = React.useState(DateTime.now());
-
- const handleChange = (newDateTime: DateTime | null) => {
- setSelectedDateTime(newDateTime);
- action('Controlled dateTime change')(newDateTime?.toISO());
- };
-
- return (
-
- );
- };
-
- return ;
- },
-};
-
-export const DefaultExample: Story = {
- args: {
- label: 'Default Date-Time Picker',
- onApply: action('Apply clicked'),
- onCancel: action('Cancel clicked'),
- onChange: action('Date-Time selected'),
- placeholder: 'yyyy-MM-dd HH:mm',
- },
-};
-
-export const WithErrorText: Story = {
- args: {
- errorText: 'This field is required',
- label: 'Date-Time Picker with Error',
- onApply: action('Apply clicked with error'),
- onCancel: action('Cancel clicked with error'),
- onChange: action('Date-Time selected with error'),
- placeholder: 'yyyy-MM-dd HH:mm',
- },
-};
-
-const meta: Meta = {
- argTypes: {
- errorText: {
- control: { type: 'text' },
- },
- format: {
- control: { type: 'text' },
- },
- label: {
- control: { type: 'text' },
- },
- onApply: { action: 'applyClicked' },
- onCancel: { action: 'cancelClicked' },
- onChange: { action: 'dateTimeChanged' },
- placeholder: {
- control: { type: 'text' },
- },
- },
- args: {
- format: 'yyyy-MM-dd HH:mm',
- label: 'Date-Time Picker',
- placeholder: 'Select a date and time',
- },
- component: DateTimePicker,
- title: 'Components/DateTimePicker',
-};
-
-export default meta;
diff --git a/packages/ui/src/components/DatePicker/DateTimePicker.test.tsx b/packages/ui/src/components/DatePicker/DateTimePicker.test.tsx
deleted file mode 100644
index 12f1795a747..00000000000
--- a/packages/ui/src/components/DatePicker/DateTimePicker.test.tsx
+++ /dev/null
@@ -1,143 +0,0 @@
-import { screen } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import { DateTime } from 'luxon';
-import * as React from 'react';
-
-import { renderWithTheme } from 'src/utilities/testHelpers';
-
-import { DateTimePicker } from './DateTimePicker';
-
-import type { DateTimePickerProps } from './DateTimePicker';
-
-const defaultProps: DateTimePickerProps = {
- label: 'Select Date and Time',
- onApply: vi.fn(),
- onCancel: vi.fn(),
- onChange: vi.fn(),
- placeholder: 'yyyy-MM-dd HH:mm',
- value: DateTime.fromISO('2024-10-25T15:30:00'),
-};
-
-describe('DateTimePicker Component', () => {
- it('should render the DateTimePicker component with the correct label and placeholder', () => {
- renderWithTheme();
- const textField = screen.getByRole('textbox', {
- name: 'Select Date and Time',
- });
- expect(textField).toBeVisible();
- expect(textField).toHaveAttribute('placeholder', 'yyyy-MM-dd HH:mm');
- });
-
- it('should open the Popover when the TextField is clicked', async () => {
- renderWithTheme();
- const textField = screen.getByRole('textbox', {
- name: 'Select Date and Time',
- });
- await userEvent.click(textField);
- expect(screen.getByRole('dialog')).toBeVisible(); // Verifying the Popover is open
- });
-
- it('should call onCancel when the Cancel button is clicked', async () => {
- renderWithTheme();
- await userEvent.click(
- screen.getByRole('textbox', { name: 'Select Date and Time' })
- );
- const cancelButton = screen.getByRole('button', { name: /Cancel/i });
- await userEvent.click(cancelButton);
- expect(defaultProps.onCancel).toHaveBeenCalled();
- });
-
- it('should call onApply when the Apply button is clicked', async () => {
- renderWithTheme();
- await userEvent.click(
- screen.getByRole('textbox', { name: 'Select Date and Time' })
- );
- const applyButton = screen.getByRole('button', { name: /Apply/i });
- await userEvent.click(applyButton);
- expect(defaultProps.onApply).toHaveBeenCalled();
- expect(defaultProps.onChange).toHaveBeenCalledWith(expect.any(DateTime)); // Ensuring onChange was called with a DateTime object
- });
-
- it('should handle date changes correctly', async () => {
- renderWithTheme();
- await userEvent.click(
- screen.getByRole('textbox', { name: 'Select Date and Time' })
- );
-
- // Simulate selecting a date (e.g., 15th of the month)
- const dateButton = screen.getByRole('gridcell', { name: '15' });
- await userEvent.click(dateButton);
-
- // Check that the displayed value has been updated correctly (this assumes the date format)
- expect(defaultProps.onChange).toHaveBeenCalled();
- });
-
- it('should handle timezone changes correctly', async () => {
- const timezoneChangeMock = vi.fn(); // Create a mock function
-
- const updatedProps = {
- ...defaultProps,
- timeZoneSelectProps: { onChange: timezoneChangeMock, value: 'UTC' },
- };
-
- renderWithTheme();
-
- await userEvent.click(
- screen.getByRole('textbox', { name: 'Select Date and Time' })
- );
-
- // Simulate selecting a timezone from the TimeZoneSelect
- const timezoneInput = screen.getByPlaceholderText(/Choose a Timezone/i);
- await userEvent.click(timezoneInput);
-
- // Select a timezone from the dropdown options
- await userEvent.click(
- screen.getByRole('option', { name: '(GMT -11:00) Niue Time' })
- );
-
- // Click the Apply button to trigger the change
- await userEvent.click(screen.getByRole('button', { name: 'Apply' }));
-
- // Verify that the onChange function was called with the expected value
- expect(timezoneChangeMock).toHaveBeenCalledWith('Pacific/Niue');
- });
-
- it('should display the error text when provided', () => {
- renderWithTheme(
-
- );
- expect(screen.getByText(/Invalid date-time/i)).toBeVisible();
- });
-
- it('should format the date-time correctly when a custom format is provided', () => {
- renderWithTheme(
-
- );
- const textField = screen.getByRole('textbox', {
- name: 'Select Date and Time',
- });
-
- expect(textField).toHaveValue('25/10/2024 15:30');
- });
- it('should not render the time selector when showTime is false', async () => {
- renderWithTheme();
- await userEvent.click(
- screen.getByRole('textbox', { name: 'Select Date and Time' })
- );
- const timePicker = screen.queryByLabelText(/Select Time/i); // Label from timeSelectProps
- expect(timePicker).not.toBeInTheDocument();
- });
-
- it('should not render the timezone selector when showTimeZone is false', async () => {
- renderWithTheme();
- await userEvent.click(
- screen.getByRole('textbox', { name: 'Select Date and Time' })
- );
- const timeZoneSelect = screen.queryByLabelText(/Timezone/i); // Label from timeZoneSelectProps
- expect(timeZoneSelect).not.toBeInTheDocument();
- });
-});
diff --git a/packages/ui/src/components/DatePicker/DateTimePicker.tsx b/packages/ui/src/components/DatePicker/DateTimePicker.tsx
deleted file mode 100644
index ebe97d07071..00000000000
--- a/packages/ui/src/components/DatePicker/DateTimePicker.tsx
+++ /dev/null
@@ -1,265 +0,0 @@
-import { TextField } from '@linode/ui';
-import { Divider } from '@linode/ui';
-import { Box } from '@linode/ui';
-import { Grid, Popover } from '@mui/material';
-import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon';
-import { DateCalendar } from '@mui/x-date-pickers/DateCalendar';
-import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
-import { TimePicker } from '@mui/x-date-pickers/TimePicker';
-import React, { useEffect, useState } from 'react';
-
-import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel';
-
-import { TimeZoneSelect } from './TimeZoneSelect';
-
-import type { TextFieldProps } from '@linode/ui';
-import type { Theme } from '@mui/material/styles';
-import type { DateCalendarProps } from '@mui/x-date-pickers/DateCalendar';
-import type { DateTime } from 'luxon';
-
-export interface DateTimePickerProps {
- /** Additional props for the DateCalendar */
- dateCalendarProps?: Partial>;
- /** Error text for the date picker field */
- errorText?: string;
- /** Format for displaying the date-time */
- format?: string;
- /** Label for the input field */
- label?: string;
- /** Callback when the "Apply" button is clicked */
- onApply?: () => void;
- /** Callback when the "Cancel" button is clicked */
- onCancel?: () => void;
- /** Callback when date-time changes */
- onChange?: (dateTime: DateTime | null) => void;
- /** Placeholder text for the input field */
- placeholder?: string;
- /** Whether to show the time selector */
- showTime?: boolean;
- /** Whether to show the timezone selector */
- showTimeZone?: boolean;
- /** Props for customizing the TimePicker component */
- timeSelectProps?: {
- label?: string;
- onChange?: (time: string) => void;
- value?: null | string;
- };
- /** Props for customizing the TimeZoneSelect component */
- timeZoneSelectProps?: {
- label?: string;
- onChange?: (timezone: string) => void;
- value?: null | string;
- };
- /** Initial or controlled dateTime value */
- value?: DateTime | null;
-}
-
-export const DateTimePicker = ({
- dateCalendarProps = {},
- errorText = '',
- format = 'yyyy-MM-dd HH:mm',
- label = 'Select Date and Time',
- onApply,
- onCancel,
- onChange,
- placeholder = 'yyyy-MM-dd HH:mm',
- showTime = true,
- showTimeZone = true,
- timeSelectProps = {},
- timeZoneSelectProps = {},
- value = null,
-}: DateTimePickerProps) => {
- const [anchorEl, setAnchorEl] = useState(null);
- const [selectedDateTime, setSelectedDateTime] = useState(
- value
- );
- const [selectedTimeZone, setSelectedTimeZone] = useState(
- timeZoneSelectProps.value || null
- );
-
- const TimePickerFieldProps: TextFieldProps = {
- label: timeSelectProps?.label ?? 'Select Time',
- noMarginTop: true,
- };
-
- useEffect(() => {
- if (value) {
- setSelectedDateTime(value);
- }
- }, [value]);
-
- useEffect(() => {
- if (timeZoneSelectProps.value) {
- setSelectedTimeZone(timeZoneSelectProps.value);
- }
- }, [timeZoneSelectProps.value]);
-
- const handleOpen = (event: React.MouseEvent) => {
- setAnchorEl(event.currentTarget);
- };
-
- const handleClose = () => {
- setAnchorEl(null);
- if (onCancel) {
- onCancel();
- }
- };
-
- const handleApply = () => {
- setAnchorEl(null);
- if (onChange) {
- onChange(selectedDateTime);
- }
- if (onApply) {
- onApply();
- }
- };
-
- const handleDateChange = (newDate: DateTime | null) => {
- setSelectedDateTime((prev) =>
- newDate
- ? newDate.set({
- hour: prev?.hour || 0,
- minute: prev?.minute || 0,
- })
- : null
- );
- };
-
- const handleTimeChange = (newTime: DateTime | null) => {
- if (newTime) {
- setSelectedDateTime((prev) => {
- if (prev) {
- // Ensure hour and minute are valid numbers
- const newHour = newTime.hour;
- const newMinute = newTime.minute;
-
- if (typeof newHour === 'number' && typeof newMinute === 'number') {
- return prev.set({ hour: newHour, minute: newMinute });
- }
- }
- // Return the current `prev` value if newTime is invalid
- return prev;
- });
- }
- if (timeSelectProps.onChange && newTime) {
- timeSelectProps.onChange(newTime.toISOTime());
- }
- };
-
- const handleTimeZoneChange = (newTimeZone: string) => {
- setSelectedTimeZone(newTimeZone);
- if (timeZoneSelectProps.onChange) {
- timeZoneSelectProps.onChange(newTimeZone);
- }
- };
-
- return (
-
-
-
-
-
-
- ({
- '& .MuiDayCalendar-weekContainer, & .MuiDayCalendar-header': {
- justifyContent: 'space-between',
- },
- '& .MuiDayCalendar-weekDayLabel': {
- fontSize: '0.875rem',
- },
- '& .MuiPickersCalendarHeader-label': {
- fontFamily: theme.font.bold,
- },
- '& .MuiPickersCalendarHeader-root': {
- borderBottom: `1px solid ${theme.borderColors.divider}`,
- fontSize: '0.875rem',
- paddingBottom: theme.spacing(1),
- },
- '& .MuiPickersDay-root': {
- fontSize: '0.875rem',
- margin: `${theme.spacing(0.5)}px`,
- },
- borderRadius: `${theme.spacing(2)}`,
- borderWidth: '0px',
- })}
- onChange={handleDateChange}
- value={selectedDateTime}
- {...dateCalendarProps}
- />
-
- {showTime && (
-
-
-
- )}
- {showTimeZone && (
-
-
-
- )}
-
-
-
-
- ({
- marginBottom: theme.spacing(1),
- marginRight: theme.spacing(2),
- })}
- />
-
-
-
- );
-};
diff --git a/packages/ui/src/components/DatePicker/DateTimeRangePicker.stories.tsx b/packages/ui/src/components/DatePicker/DateTimeRangePicker.stories.tsx
deleted file mode 100644
index 70ad6b2a39d..00000000000
--- a/packages/ui/src/components/DatePicker/DateTimeRangePicker.stories.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-import { action } from '@storybook/addon-actions';
-import { DateTime } from 'luxon';
-import * as React from 'react';
-
-import { DateTimeRangePicker } from './DateTimeRangePicker';
-
-import type { Meta, StoryObj } from '@storybook/react';
-
-type Story = StoryObj;
-
-export const Default: Story = {
- args: {
- endLabel: 'End Date and Time',
- format: 'yyyy-MM-dd HH:mm',
- onChange: action('DateTime range changed'),
- startLabel: 'Start Date and Time',
- },
- render: (args) => ,
-};
-
-export const WithInitialValues: Story = {
- render: () => {
- const ComponentWithState: React.FC = () => {
- const [start, setStart] = React.useState(
- DateTime.now().minus({ days: 1 })
- );
- const [end, setEnd] = React.useState(DateTime.now());
-
- const handleDateChange = (
- newStart: DateTime | null,
- newEnd: DateTime | null
- ) => {
- setStart(newStart);
- setEnd(newEnd);
- action('DateTime range changed')(newStart?.toISO(), newEnd?.toISO());
- };
-
- return (
-
- );
- };
-
- return ;
- },
-};
-
-const meta: Meta = {
- argTypes: {
- endLabel: {
- control: 'text',
- description: 'Label for the end date picker',
- },
- format: {
- control: 'text',
- description: 'Format for displaying the date-time',
- },
- onChange: {
- action: 'DateTime range changed',
- description: 'Callback when the date-time range changes',
- },
- startLabel: {
- control: 'text',
- description: 'Label for the start date picker',
- },
- },
- component: DateTimeRangePicker,
- title: 'Components/DateTimeRangePicker',
-};
-
-export default meta;
diff --git a/packages/ui/src/components/DatePicker/DateTimeRangePicker.test.tsx b/packages/ui/src/components/DatePicker/DateTimeRangePicker.test.tsx
deleted file mode 100644
index f3421b684c3..00000000000
--- a/packages/ui/src/components/DatePicker/DateTimeRangePicker.test.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-import { screen } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import * as React from 'react';
-
-import { renderWithTheme } from 'src/utilities/testHelpers';
-
-import { DateTimeRangePicker } from './DateTimeRangePicker';
-
-describe('DateTimeRangePicker Component', () => {
- const onChangeMock = vi.fn();
-
- beforeEach(() => {
- vi.clearAllMocks();
- });
-
- it('should render start and end DateTimePickers with correct labels', () => {
- renderWithTheme();
-
- expect(screen.getByLabelText('Start Date')).toBeVisible();
- expect(screen.getByLabelText('End Date and Time')).toBeVisible();
- });
-
- it('should call onChange when start date is changed', async () => {
- renderWithTheme();
-
- // Open start date picker
- await userEvent.click(screen.getByLabelText('Start Date'));
-
- await userEvent.click(screen.getByRole('gridcell', { name: '10' }));
- await userEvent.click(screen.getByRole('button', { name: 'Apply' }));
-
- // Check if the onChange function is called with the expected DateTime value
- expect(onChangeMock).toHaveBeenCalledWith(
- expect.objectContaining({ day: 10 }),
- null
- );
- });
-
- it('should show error when end date-time is before start date-time', async () => {
- renderWithTheme();
-
- // Set start date-time to the 15th
- const startDateField = screen.getByLabelText('Start Date');
- await userEvent.click(startDateField);
- await userEvent.click(screen.getByRole('gridcell', { name: '15' }));
- await userEvent.click(screen.getByRole('button', { name: 'Apply' }));
-
- // Open the end date picker
- const endDateField = screen.getByLabelText('End Date and Time');
- await userEvent.click(endDateField);
-
- // Check if the date before the start date is disabled via a class or attribute
- const invalidDate = screen.getByRole('gridcell', { name: '10' });
- expect(invalidDate).toHaveClass('Mui-disabled'); // or check for the specific attribute used
-
- // Confirm error message is not shown since the click was blocked
- expect(screen.queryByText('Invalid date and Time')).not.toBeInTheDocument();
- });
-});
diff --git a/packages/ui/src/components/DatePicker/DateTimeRangePicker.tsx b/packages/ui/src/components/DatePicker/DateTimeRangePicker.tsx
deleted file mode 100644
index f4cb77b175a..00000000000
--- a/packages/ui/src/components/DatePicker/DateTimeRangePicker.tsx
+++ /dev/null
@@ -1,116 +0,0 @@
-import { Box } from '@linode/ui';
-import { DateTime } from 'luxon';
-import React, { useState } from 'react';
-
-import { DateTimePicker } from './DateTimePicker';
-
-interface DateTimeRangePickerProps {
- /** Initial or controlled value for the end date-time */
- endDateTimeValue?: DateTime | null;
- /** Custom labels for the start and end date/time fields */
- endLabel?: string;
- /** Format for displaying the date-time */
- format?: string;
- /** Callback when the date-time range changes */
- onChange?: (
- start: DateTime | null,
- end: DateTime | null,
- startTimeZone?: null | string
- ) => void;
- /** Whether to show the timezone field for the end date picker */
- showEndTimeZone?: boolean;
- /** Initial or controlled value for the start date-time */
- startDateTimeValue?: DateTime | null;
- /** Custom labels for the start and end date/time fields */
- startLabel?: string;
- /** Initial or controlled value for the start timezone */
- startTimeZoneValue?: null | string;
-}
-
-export const DateTimeRangePicker = ({
- endDateTimeValue = null,
- endLabel = 'End Date and Time',
- format = 'yyyy-MM-dd HH:mm',
- onChange,
- showEndTimeZone = false,
- startDateTimeValue = null,
- startLabel = 'Start Date and Time',
- startTimeZoneValue = null,
-}: DateTimeRangePickerProps) => {
- const [startDateTime, setStartDateTime] = useState(
- startDateTimeValue
- );
- const [endDateTime, setEndDateTime] = useState(
- endDateTimeValue
- );
- const [startTimeZone, setStartTimeZone] = useState(
- startTimeZoneValue
- );
- const [error, setError] = useState();
-
- const handleStartDateTimeChange = (newStart: DateTime | null) => {
- setStartDateTime(newStart);
-
- if (endDateTime && newStart && endDateTime >= newStart) {
- setError(undefined); // Clear error if valid
- }
-
- if (onChange) {
- onChange(newStart, endDateTime, startTimeZone);
- }
- };
-
- const handleEndDateTimeChange = (newEnd: DateTime | null) => {
- if (startDateTime && newEnd && newEnd < startDateTime) {
- setError('End date/time must be after the start date/time.');
- } else {
- setEndDateTime(newEnd);
- setError(undefined);
- }
-
- if (onChange) {
- onChange(startDateTime, newEnd, startTimeZone);
- }
- };
-
- const handleStartTimeZoneChange = (newTimeZone: null | string) => {
- setStartTimeZone(newTimeZone);
-
- if (onChange) {
- onChange(startDateTime, endDateTime, newTimeZone);
- }
- };
-
- return (
-
- {/* Start DateTime Picker */}
-
-
- {/* End DateTime Picker */}
-
-
- );
-};
diff --git a/packages/ui/src/components/DatePicker/TimeZoneSelect.tsx b/packages/ui/src/components/DatePicker/TimeZoneSelect.tsx
deleted file mode 100644
index 19312098007..00000000000
--- a/packages/ui/src/components/DatePicker/TimeZoneSelect.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-import { DateTime } from 'luxon';
-import React from 'react';
-
-import { timezones } from 'src/assets/timezones/timezones';
-import { Autocomplete } from 'src/components/Autocomplete/Autocomplete';
-
-type Timezone = typeof timezones[number];
-
-interface TimeZoneSelectProps {
- disabled?: boolean;
- errorText?: string;
- label?: string;
- noMarginTop?: boolean;
- onChange: (timezone: string) => void;
- value: null | string;
-}
-
-const getOptionLabel = ({ label, offset }: Timezone) => {
- const minutes = (Math.abs(offset) % 60).toLocaleString(undefined, {
- minimumIntegerDigits: 2,
- useGrouping: false,
- });
- const hours = Math.floor(Math.abs(offset) / 60);
- const isPositive = Math.abs(offset) === offset ? '+' : '-';
-
- return `(GMT ${isPositive}${hours}:${minutes}) ${label}`;
-};
-
-const getTimezoneOptions = () => {
- return timezones
- .map((tz) => {
- const offset = DateTime.now().setZone(tz.name).offset;
- const label = getOptionLabel({ ...tz, offset });
- return { label, offset, value: tz.name };
- })
- .sort((a, b) => a.offset - b.offset);
-};
-
-const timezoneOptions = getTimezoneOptions();
-
-export const TimeZoneSelect = ({
- disabled = false,
- errorText,
- label = 'Timezone',
- noMarginTop = false,
- onChange,
- value,
-}: TimeZoneSelectProps) => {
- return (
- option.value === value) ?? undefined
- }
- autoHighlight
- disabled={disabled}
- errorText={errorText}
- label={label}
- noMarginTop={noMarginTop}
- onChange={(e, option) => onChange(option?.value || '')}
- options={timezoneOptions}
- placeholder="Choose a Timezone"
- />
- );
-};
diff --git a/packages/ui/src/components/DatePicker/index.ts b/packages/ui/src/components/DatePicker/index.ts
deleted file mode 100644
index a48b62e4dfc..00000000000
--- a/packages/ui/src/components/DatePicker/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from './DatePicker';
From b8f2d739124874ee61e827beb3a7fa7c5f6347d9 Mon Sep 17 00:00:00 2001
From: cpathipa <119517080+cpathipa@users.noreply.github.com>
Date: Wed, 4 Dec 2024 11:51:16 -0600
Subject: [PATCH 26/35] Adjust broken tests
---
.../src/components/DatePicker/DateTimeRangePicker.test.tsx | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/packages/manager/src/components/DatePicker/DateTimeRangePicker.test.tsx b/packages/manager/src/components/DatePicker/DateTimeRangePicker.test.tsx
index f3421b684c3..c1303d84437 100644
--- a/packages/manager/src/components/DatePicker/DateTimeRangePicker.test.tsx
+++ b/packages/manager/src/components/DatePicker/DateTimeRangePicker.test.tsx
@@ -16,7 +16,7 @@ describe('DateTimeRangePicker Component', () => {
it('should render start and end DateTimePickers with correct labels', () => {
renderWithTheme();
- expect(screen.getByLabelText('Start Date')).toBeVisible();
+ expect(screen.getByLabelText('Start Date and Time')).toBeVisible();
expect(screen.getByLabelText('End Date and Time')).toBeVisible();
});
@@ -24,7 +24,7 @@ describe('DateTimeRangePicker Component', () => {
renderWithTheme();
// Open start date picker
- await userEvent.click(screen.getByLabelText('Start Date'));
+ await userEvent.click(screen.getByLabelText('Start Date and Time'));
await userEvent.click(screen.getByRole('gridcell', { name: '10' }));
await userEvent.click(screen.getByRole('button', { name: 'Apply' }));
@@ -32,6 +32,7 @@ describe('DateTimeRangePicker Component', () => {
// Check if the onChange function is called with the expected DateTime value
expect(onChangeMock).toHaveBeenCalledWith(
expect.objectContaining({ day: 10 }),
+ null,
null
);
});
@@ -40,7 +41,7 @@ describe('DateTimeRangePicker Component', () => {
renderWithTheme();
// Set start date-time to the 15th
- const startDateField = screen.getByLabelText('Start Date');
+ const startDateField = screen.getByLabelText('Start Date and Time');
await userEvent.click(startDateField);
await userEvent.click(screen.getByRole('gridcell', { name: '15' }));
await userEvent.click(screen.getByRole('button', { name: 'Apply' }));
From e52304339f8884bbbc4d3ea21ea2fd06554b0040 Mon Sep 17 00:00:00 2001
From: cpathipa <119517080+cpathipa@users.noreply.github.com>
Date: Wed, 4 Dec 2024 11:58:14 -0600
Subject: [PATCH 27/35] Update TimeZoneSelect.tsx
---
packages/manager/src/components/DatePicker/TimeZoneSelect.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/manager/src/components/DatePicker/TimeZoneSelect.tsx b/packages/manager/src/components/DatePicker/TimeZoneSelect.tsx
index 19312098007..f4bd68c97a3 100644
--- a/packages/manager/src/components/DatePicker/TimeZoneSelect.tsx
+++ b/packages/manager/src/components/DatePicker/TimeZoneSelect.tsx
@@ -1,8 +1,8 @@
+import { Autocomplete } from '@linode/ui';
import { DateTime } from 'luxon';
import React from 'react';
import { timezones } from 'src/assets/timezones/timezones';
-import { Autocomplete } from 'src/components/Autocomplete/Autocomplete';
type Timezone = typeof timezones[number];
From f0dd5c09520d39c3c8b5e802ef1e41a75e63305c Mon Sep 17 00:00:00 2001
From: cpathipa <119517080+cpathipa@users.noreply.github.com>
Date: Mon, 9 Dec 2024 11:50:57 -0600
Subject: [PATCH 28/35] Code cleanup
---
.../DatePicker/DatePicker.stories.tsx | 7 +-
.../DatePicker/DateTimePicker.stories.tsx | 5 +-
.../components/DatePicker/DateTimePicker.tsx | 77 +++++++------------
.../DatePicker/DateTimeRangePicker.tsx | 6 +-
4 files changed, 40 insertions(+), 55 deletions(-)
diff --git a/packages/manager/src/components/DatePicker/DatePicker.stories.tsx b/packages/manager/src/components/DatePicker/DatePicker.stories.tsx
index db3925b03eb..6e9d4408783 100644
--- a/packages/manager/src/components/DatePicker/DatePicker.stories.tsx
+++ b/packages/manager/src/components/DatePicker/DatePicker.stories.tsx
@@ -1,19 +1,17 @@
import { action } from '@storybook/addon-actions';
-import { DateTime } from 'luxon';
import * as React from 'react';
import { DatePicker } from './DatePicker';
import type { Meta, StoryObj } from '@storybook/react';
+import type { DateTime } from 'luxon';
type Story = StoryObj;
export const ControlledExample: Story = {
render: () => {
const ControlledDatePicker = () => {
- const [selectedDate, setSelectedDate] = React.useState(
- DateTime.now()
- );
+ const [selectedDate, setSelectedDate] = React.useState();
const handleChange = (newDate: DateTime | null) => {
setSelectedDate(newDate);
@@ -26,6 +24,7 @@ export const ControlledExample: Story = {
helperText="This is a controlled DatePicker"
label="Controlled Date Picker"
onChange={handleChange}
+ placeholder="yyyy-MM-dd"
value={selectedDate}
/>
);
diff --git a/packages/manager/src/components/DatePicker/DateTimePicker.stories.tsx b/packages/manager/src/components/DatePicker/DateTimePicker.stories.tsx
index 32d65dfa0b7..bb3f0c48233 100644
--- a/packages/manager/src/components/DatePicker/DateTimePicker.stories.tsx
+++ b/packages/manager/src/components/DatePicker/DateTimePicker.stories.tsx
@@ -1,10 +1,10 @@
import { action } from '@storybook/addon-actions';
-import { DateTime } from 'luxon';
import * as React from 'react';
import { DateTimePicker } from './DateTimePicker';
import type { Meta, StoryObj } from '@storybook/react';
+import type { DateTime } from 'luxon';
type Story = StoryObj;
@@ -14,7 +14,7 @@ export const ControlledExample: Story = {
const [
selectedDateTime,
setSelectedDateTime,
- ] = React.useState(DateTime.now());
+ ] = React.useState(null); // Start with null
const handleChange = (newDateTime: DateTime | null) => {
setSelectedDateTime(newDateTime);
@@ -32,6 +32,7 @@ export const ControlledExample: Story = {
onCancel={action('Cancel clicked')}
onChange={handleChange}
placeholder="yyyy-MM-dd HH:mm"
+ showTimeZone={false}
timeSelectProps={{ label: 'Select Time' }}
value={selectedDateTime}
/>
diff --git a/packages/manager/src/components/DatePicker/DateTimePicker.tsx b/packages/manager/src/components/DatePicker/DateTimePicker.tsx
index 027b25822d2..a67b5ccb155 100644
--- a/packages/manager/src/components/DatePicker/DateTimePicker.tsx
+++ b/packages/manager/src/components/DatePicker/DateTimePicker.tsx
@@ -82,39 +82,6 @@ export const DateTimePicker = ({
noMarginTop: true,
};
- useEffect(() => {
- if (value) {
- setSelectedDateTime(value);
- }
- }, [value]);
-
- useEffect(() => {
- if (timeZoneSelectProps.value) {
- setSelectedTimeZone(timeZoneSelectProps.value);
- }
- }, [timeZoneSelectProps.value]);
-
- const handleOpen = (event: React.MouseEvent) => {
- setAnchorEl(event.currentTarget);
- };
-
- const handleClose = () => {
- setAnchorEl(null);
- if (onCancel) {
- onCancel();
- }
- };
-
- const handleApply = () => {
- setAnchorEl(null);
- if (onChange) {
- onChange(selectedDateTime);
- }
- if (onApply) {
- onApply();
- }
- };
-
const handleDateChange = (newDate: DateTime | null) => {
setSelectedDateTime((prev) =>
newDate
@@ -130,7 +97,6 @@ export const DateTimePicker = ({
if (newTime) {
setSelectedDateTime((prev) => {
if (prev) {
- // Ensure hour and minute are valid numbers
const newHour = newTime.hour;
const newMinute = newTime.minute;
@@ -138,13 +104,9 @@ export const DateTimePicker = ({
return prev.set({ hour: newHour, minute: newMinute });
}
}
- // Return the current `prev` value if newTime is invalid
return prev;
});
}
- if (timeSelectProps.onChange && newTime) {
- timeSelectProps.onChange(newTime.toISOTime());
- }
};
const handleTimeZoneChange = (newTimeZone: string) => {
@@ -154,6 +116,29 @@ export const DateTimePicker = ({
}
};
+ const handleApply = () => {
+ setAnchorEl(null);
+ if (onChange) {
+ onChange(selectedDateTime);
+ }
+ if (onApply) {
+ onApply();
+ }
+ };
+
+ const handleClose = () => {
+ setAnchorEl(null);
+ if (onCancel) {
+ onCancel();
+ }
+ };
+
+ useEffect(() => {
+ if (timeZoneSelectProps.value) {
+ setSelectedTimeZone(timeZoneSelectProps.value);
+ }
+ }, [timeZoneSelectProps.value]);
+
return (
@@ -168,7 +153,7 @@ export const DateTimePicker = ({
InputProps={{ readOnly: true }}
errorText={errorText}
label={label}
- onClick={handleOpen}
+ onClick={(event) => setAnchorEl(event.currentTarget)}
placeholder={placeholder}
/>
@@ -181,6 +166,9 @@ export const DateTimePicker = ({
>
({
'& .MuiDayCalendar-weekContainer, & .MuiDayCalendar-header': {
justifyContent: 'space-between',
@@ -203,9 +191,6 @@ export const DateTimePicker = ({
borderRadius: `${theme.spacing(2)}`,
borderWidth: '0px',
})}
- onChange={handleDateChange}
- value={selectedDateTime}
- {...dateCalendarProps}
/>
)}
diff --git a/packages/manager/src/components/DatePicker/DateTimeRangePicker.tsx b/packages/manager/src/components/DatePicker/DateTimeRangePicker.tsx
index f4cb77b175a..0d973178715 100644
--- a/packages/manager/src/components/DatePicker/DateTimeRangePicker.tsx
+++ b/packages/manager/src/components/DatePicker/DateTimeRangePicker.tsx
@@ -17,8 +17,10 @@ interface DateTimeRangePickerProps {
end: DateTime | null,
startTimeZone?: null | string
) => void;
- /** Whether to show the timezone field for the end date picker */
+ /** Whether to show the end timezone field for the end date picker */
showEndTimeZone?: boolean;
+ /** Whether to show the start timezone field for the end date picker */
+ showStartTimeZone?: boolean;
/** Initial or controlled value for the start date-time */
startDateTimeValue?: DateTime | null;
/** Custom labels for the start and end date/time fields */
@@ -33,6 +35,7 @@ export const DateTimeRangePicker = ({
format = 'yyyy-MM-dd HH:mm',
onChange,
showEndTimeZone = false,
+ showStartTimeZone = false,
startDateTimeValue = null,
startLabel = 'Start Date and Time',
startTimeZoneValue = null,
@@ -94,6 +97,7 @@ export const DateTimeRangePicker = ({
format={format}
label={startLabel}
onChange={handleStartDateTimeChange}
+ showTimeZone={showStartTimeZone}
timeSelectProps={{ label: 'Start Time' }}
value={startDateTime}
/>
From 9abec60802f44ab47f9d5e972437a87548257639 Mon Sep 17 00:00:00 2001
From: cpathipa <119517080+cpathipa@users.noreply.github.com>
Date: Mon, 9 Dec 2024 14:16:07 -0600
Subject: [PATCH 29/35] Add validation for start date agains end date.
---
.../DatePicker/DateTimeRangePicker.test.tsx | 53 +++++++++++++++++++
.../DatePicker/DateTimeRangePicker.tsx | 42 ++++++++++-----
2 files changed, 83 insertions(+), 12 deletions(-)
diff --git a/packages/manager/src/components/DatePicker/DateTimeRangePicker.test.tsx b/packages/manager/src/components/DatePicker/DateTimeRangePicker.test.tsx
index c1303d84437..568e9559e06 100644
--- a/packages/manager/src/components/DatePicker/DateTimeRangePicker.test.tsx
+++ b/packages/manager/src/components/DatePicker/DateTimeRangePicker.test.tsx
@@ -57,4 +57,57 @@ describe('DateTimeRangePicker Component', () => {
// Confirm error message is not shown since the click was blocked
expect(screen.queryByText('Invalid date and Time')).not.toBeInTheDocument();
});
+
+ it('should show error when start date-time is after end date-time', async () => {
+ renderWithTheme(
+
+ );
+
+ // Set the end date-time to the 15th
+ const endDateField = screen.getByLabelText('End Date and Time');
+ await userEvent.click(endDateField);
+ await userEvent.click(screen.getByRole('gridcell', { name: '15' }));
+ await userEvent.click(screen.getByRole('button', { name: 'Apply' }));
+
+ // Set the start date-time to the 10th (which is earlier than the end date-time)
+ const startDateField = screen.getByLabelText('Start Date and Time');
+ await userEvent.click(startDateField);
+ await userEvent.click(screen.getByRole('gridcell', { name: '20' })); // Invalid date
+ await userEvent.click(screen.getByRole('button', { name: 'Apply' }));
+
+ // Confirm the error message is displayed
+ expect(
+ screen.getByText('Start date/time cannot be after the end date/time.')
+ ).toBeInTheDocument();
+ });
+
+ it('should display custom error messages when start date-time is after end date-time', async () => {
+ renderWithTheme(
+
+ );
+
+ // Set the end date-time to the 15th
+ const endDateField = screen.getByLabelText('End Date and Time');
+ await userEvent.click(endDateField);
+ await userEvent.click(screen.getByRole('gridcell', { name: '15' }));
+ await userEvent.click(screen.getByRole('button', { name: 'Apply' }));
+
+ // Set the start date-time to the 20th (which is after the end date-time)
+ const startDateField = screen.getByLabelText('Start Date and Time');
+ await userEvent.click(startDateField);
+ await userEvent.click(screen.getByRole('gridcell', { name: '20' })); // Invalid date
+ await userEvent.click(screen.getByRole('button', { name: 'Apply' }));
+
+ // Confirm the custom error message is displayed for the start date
+ expect(screen.getByText('Custom start date error')).toBeInTheDocument();
+ });
});
diff --git a/packages/manager/src/components/DatePicker/DateTimeRangePicker.tsx b/packages/manager/src/components/DatePicker/DateTimeRangePicker.tsx
index 0d973178715..d5fa4989b71 100644
--- a/packages/manager/src/components/DatePicker/DateTimeRangePicker.tsx
+++ b/packages/manager/src/components/DatePicker/DateTimeRangePicker.tsx
@@ -1,10 +1,13 @@
import { Box } from '@linode/ui';
-import { DateTime } from 'luxon';
import React, { useState } from 'react';
import { DateTimePicker } from './DateTimePicker';
+import type { DateTime } from 'luxon';
+
interface DateTimeRangePickerProps {
+ /** Custom error message for invalid end date */
+ endDateErrorMessage?: string;
/** Initial or controlled value for the end date-time */
endDateTimeValue?: DateTime | null;
/** Custom labels for the start and end date/time fields */
@@ -21,6 +24,8 @@ interface DateTimeRangePickerProps {
showEndTimeZone?: boolean;
/** Whether to show the start timezone field for the end date picker */
showStartTimeZone?: boolean;
+ /** Custom error message for invalid start date */
+ startDateErrorMessage?: string;
/** Initial or controlled value for the start date-time */
startDateTimeValue?: DateTime | null;
/** Custom labels for the start and end date/time fields */
@@ -30,12 +35,14 @@ interface DateTimeRangePickerProps {
}
export const DateTimeRangePicker = ({
+ endDateErrorMessage = 'End date/time cannot be before the start date/time.',
endDateTimeValue = null,
endLabel = 'End Date and Time',
format = 'yyyy-MM-dd HH:mm',
onChange,
showEndTimeZone = false,
showStartTimeZone = false,
+ startDateErrorMessage = 'Start date/time cannot be after the end date/time.',
startDateTimeValue = null,
startLabel = 'Start Date and Time',
startTimeZoneValue = null,
@@ -51,12 +58,27 @@ export const DateTimeRangePicker = ({
);
const [error, setError] = useState();
+ const validateDates = (
+ start: DateTime | null,
+ end: DateTime | null,
+ source: 'end' | 'start'
+ ) => {
+ if (start && end) {
+ if (source === 'start' && start > end) {
+ setError(startDateErrorMessage);
+ return;
+ }
+ if (source === 'end' && end < start) {
+ setError(endDateErrorMessage);
+ return;
+ }
+ }
+ setError(undefined); // Clear error if valid
+ };
+
const handleStartDateTimeChange = (newStart: DateTime | null) => {
setStartDateTime(newStart);
-
- if (endDateTime && newStart && endDateTime >= newStart) {
- setError(undefined); // Clear error if valid
- }
+ validateDates(newStart, endDateTime, 'start');
if (onChange) {
onChange(newStart, endDateTime, startTimeZone);
@@ -64,12 +86,8 @@ export const DateTimeRangePicker = ({
};
const handleEndDateTimeChange = (newEnd: DateTime | null) => {
- if (startDateTime && newEnd && newEnd < startDateTime) {
- setError('End date/time must be after the start date/time.');
- } else {
- setEndDateTime(newEnd);
- setError(undefined);
- }
+ setEndDateTime(newEnd);
+ validateDates(startDateTime, newEnd, 'end');
if (onChange) {
onChange(startDateTime, newEnd, startTimeZone);
@@ -107,7 +125,7 @@ export const DateTimeRangePicker = ({
timeZoneSelectProps={{
value: startTimeZone, // Automatically reflect the start timezone
}}
- dateCalendarProps={{ minDate: startDateTime ?? DateTime.now() }}
+ dateCalendarProps={{ minDate: startDateTime ?? undefined }}
format={format}
label={endLabel}
onChange={handleEndDateTimeChange}
From fa3e1861c5b2888ef496027503fdf6aaff1a8c09 Mon Sep 17 00:00:00 2001
From: cpathipa <119517080+cpathipa@users.noreply.github.com>
Date: Tue, 10 Dec 2024 08:42:00 -0600
Subject: [PATCH 30/35] Adjust styles for TimePicker component.
---
.../src/components/DatePicker/DatePicker.tsx | 1 +
.../components/DatePicker/DateTimePicker.tsx | 28 ++++++++++++++++++-
2 files changed, 28 insertions(+), 1 deletion(-)
diff --git a/packages/manager/src/components/DatePicker/DatePicker.tsx b/packages/manager/src/components/DatePicker/DatePicker.tsx
index a3a5a722e82..f8fc1965ac8 100644
--- a/packages/manager/src/components/DatePicker/DatePicker.tsx
+++ b/packages/manager/src/components/DatePicker/DatePicker.tsx
@@ -56,6 +56,7 @@ export const DatePicker = ({
value={value}
{...props}
slotProps={{
+ // TODO: Move styling customization to global theme styles.
popper: {
sx: {
'& .MuiDayCalendar-weekDayLabel': {
diff --git a/packages/manager/src/components/DatePicker/DateTimePicker.tsx b/packages/manager/src/components/DatePicker/DateTimePicker.tsx
index a67b5ccb155..0bbe09e5f21 100644
--- a/packages/manager/src/components/DatePicker/DateTimePicker.tsx
+++ b/packages/manager/src/components/DatePicker/DateTimePicker.tsx
@@ -169,6 +169,7 @@ export const DateTimePicker = ({
onChange={handleDateChange}
value={selectedDateTime || null}
{...dateCalendarProps}
+ // TODO: Move styling customization to global theme styles.
sx={(theme: Theme) => ({
'& .MuiDayCalendar-weekContainer, & .MuiDayCalendar-header': {
justifyContent: 'space-between',
@@ -201,7 +202,32 @@ export const DateTimePicker = ({
({
+ justifyContent: 'center',
+ marginBottom: theme.spacing(1 / 2),
+ marginTop: theme.spacing(1 / 2),
+ padding: 0,
+ }),
+ },
+ layout: {
+ sx: (theme: Theme) => ({
+ '& .MuiPickersLayout-contentWrapper': {
+ borderBottom: `1px solid ${theme.borderColors.divider}`,
+ },
+ border: `1px solid ${theme.borderColors.divider}`,
+ }),
+ },
+ openPickerButton: {
+ sx: { padding: 0 },
+ },
+ popper: {
+ sx: (theme: Theme) => ({
+ ul: {
+ borderColor: `${theme.borderColors.divider} !important`,
+ },
+ }),
+ },
textField: TimePickerFieldProps,
}}
onChange={handleTimeChange}
From 1ab71f926c03b8777a9768f2baf0caf14736134c Mon Sep 17 00:00:00 2001
From: cpathipa <119517080+cpathipa@users.noreply.github.com>
Date: Tue, 10 Dec 2024 23:23:09 -0600
Subject: [PATCH 31/35] PR feedback - @jaalah-akamai
---
.../src/components/DatePicker/DateTimePicker.tsx | 14 +++-----------
1 file changed, 3 insertions(+), 11 deletions(-)
diff --git a/packages/manager/src/components/DatePicker/DateTimePicker.tsx b/packages/manager/src/components/DatePicker/DateTimePicker.tsx
index 0bbe09e5f21..6b38ab243fc 100644
--- a/packages/manager/src/components/DatePicker/DateTimePicker.tsx
+++ b/packages/manager/src/components/DatePicker/DateTimePicker.tsx
@@ -95,17 +95,9 @@ export const DateTimePicker = ({
const handleTimeChange = (newTime: DateTime | null) => {
if (newTime) {
- setSelectedDateTime((prev) => {
- if (prev) {
- const newHour = newTime.hour;
- const newMinute = newTime.minute;
-
- if (typeof newHour === 'number' && typeof newMinute === 'number') {
- return prev.set({ hour: newHour, minute: newMinute });
- }
- }
- return prev;
- });
+ setSelectedDateTime((prev) =>
+ prev ? prev.set({ hour: newTime.hour, minute: newTime.minute }) : prev
+ );
}
};
From ee326f1281e36643d76b7d95c987de4748a02751 Mon Sep 17 00:00:00 2001
From: cpathipa <119517080+cpathipa@users.noreply.github.com>
Date: Wed, 11 Dec 2024 11:47:32 -0600
Subject: [PATCH 32/35] allow error messages from props.
---
.../components/DatePicker/DateTimePicker.tsx | 11 +++++--
.../DatePicker/DateTimeRangePicker.test.tsx | 22 +++++---------
.../DatePicker/DateTimeRangePicker.tsx | 30 +++++++++++++------
3 files changed, 36 insertions(+), 27 deletions(-)
diff --git a/packages/manager/src/components/DatePicker/DateTimePicker.tsx b/packages/manager/src/components/DatePicker/DateTimePicker.tsx
index 6b38ab243fc..d7b138e5d8e 100644
--- a/packages/manager/src/components/DatePicker/DateTimePicker.tsx
+++ b/packages/manager/src/components/DatePicker/DateTimePicker.tsx
@@ -1,6 +1,6 @@
-import { TextField } from '@linode/ui';
import { Divider } from '@linode/ui';
import { Box } from '@linode/ui';
+import { TextField } from '@linode/ui';
import { Grid, Popover } from '@mui/material';
import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon';
import { DateCalendar } from '@mui/x-date-pickers/DateCalendar';
@@ -13,7 +13,7 @@ import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel';
import { TimeZoneSelect } from './TimeZoneSelect';
import type { TextFieldProps } from '@linode/ui';
-import type { Theme } from '@mui/material/styles';
+import type { SxProps, Theme } from '@mui/material/styles';
import type { DateCalendarProps } from '@mui/x-date-pickers/DateCalendar';
import type { DateTime } from 'luxon';
@@ -38,6 +38,10 @@ export interface DateTimePickerProps {
showTime?: boolean;
/** Whether to show the timezone selector */
showTimeZone?: boolean;
+ /**
+ * Any additional styles to apply to the root element.
+ */
+ sx?: SxProps;
/** Props for customizing the TimePicker component */
timeSelectProps?: {
label?: string;
@@ -65,6 +69,7 @@ export const DateTimePicker = ({
placeholder = 'yyyy-MM-dd HH:mm',
showTime = true,
showTimeZone = true,
+ sx,
timeSelectProps = {},
timeZoneSelectProps = {},
value = null,
@@ -133,7 +138,7 @@ export const DateTimePicker = ({
return (
-
+
{
await userEvent.click(endDateField);
// Check if the date before the start date is disabled via a class or attribute
- const invalidDate = screen.getByRole('gridcell', { name: '10' });
- expect(invalidDate).toHaveClass('Mui-disabled'); // or check for the specific attribute used
+ await userEvent.click(screen.getByRole('gridcell', { name: '10' }));
+ await userEvent.click(screen.getByRole('button', { name: 'Apply' }));
// Confirm error message is not shown since the click was blocked
- expect(screen.queryByText('Invalid date and Time')).not.toBeInTheDocument();
+ expect(
+ screen.getByText('End date/time cannot be before the start date/time.')
+ ).toBeInTheDocument();
});
it('should show error when start date-time is after end date-time', async () => {
@@ -88,6 +90,7 @@ describe('DateTimeRangePicker Component', () => {
it('should display custom error messages when start date-time is after end date-time', async () => {
renderWithTheme(
{
/>
);
- // Set the end date-time to the 15th
- const endDateField = screen.getByLabelText('End Date and Time');
- await userEvent.click(endDateField);
- await userEvent.click(screen.getByRole('gridcell', { name: '15' }));
- await userEvent.click(screen.getByRole('button', { name: 'Apply' }));
-
- // Set the start date-time to the 20th (which is after the end date-time)
- const startDateField = screen.getByLabelText('Start Date and Time');
- await userEvent.click(startDateField);
- await userEvent.click(screen.getByRole('gridcell', { name: '20' })); // Invalid date
- await userEvent.click(screen.getByRole('button', { name: 'Apply' }));
-
// Confirm the custom error message is displayed for the start date
expect(screen.getByText('Custom start date error')).toBeInTheDocument();
+ expect(screen.getByText('Custom end date error')).toBeInTheDocument();
});
});
diff --git a/packages/manager/src/components/DatePicker/DateTimeRangePicker.tsx b/packages/manager/src/components/DatePicker/DateTimeRangePicker.tsx
index d5fa4989b71..a2a0408947e 100644
--- a/packages/manager/src/components/DatePicker/DateTimeRangePicker.tsx
+++ b/packages/manager/src/components/DatePicker/DateTimeRangePicker.tsx
@@ -3,6 +3,7 @@ import React, { useState } from 'react';
import { DateTimePicker } from './DateTimePicker';
+import type { SxProps, Theme } from '@mui/material/styles';
import type { DateTime } from 'luxon';
interface DateTimeRangePickerProps {
@@ -32,20 +33,25 @@ interface DateTimeRangePickerProps {
startLabel?: string;
/** Initial or controlled value for the start timezone */
startTimeZoneValue?: null | string;
+ /**
+ * Any additional styles to apply to the root element.
+ */
+ sx?: SxProps;
}
export const DateTimeRangePicker = ({
- endDateErrorMessage = 'End date/time cannot be before the start date/time.',
+ endDateErrorMessage,
endDateTimeValue = null,
endLabel = 'End Date and Time',
format = 'yyyy-MM-dd HH:mm',
onChange,
showEndTimeZone = false,
showStartTimeZone = false,
- startDateErrorMessage = 'Start date/time cannot be after the end date/time.',
+ startDateErrorMessage,
startDateTimeValue = null,
startLabel = 'Start Date and Time',
startTimeZoneValue = null,
+ sx,
}: DateTimeRangePickerProps) => {
const [startDateTime, setStartDateTime] = useState(
startDateTimeValue
@@ -56,7 +62,9 @@ export const DateTimeRangePicker = ({
const [startTimeZone, setStartTimeZone] = useState(
startTimeZoneValue
);
- const [error, setError] = useState();
+
+ const [startDateError, setStartDateError] = useState();
+ const [endDateError, setEndDateError] = useState();
const validateDates = (
start: DateTime | null,
@@ -65,15 +73,19 @@ export const DateTimeRangePicker = ({
) => {
if (start && end) {
if (source === 'start' && start > end) {
- setError(startDateErrorMessage);
+ setStartDateError('Start date/time cannot be after the end date/time.');
+ setEndDateError(undefined);
return;
}
if (source === 'end' && end < start) {
- setError(endDateErrorMessage);
+ setEndDateError('End date/time cannot be before the start date/time.');
+ setStartDateError(undefined);
return;
}
}
- setError(undefined); // Clear error if valid
+ // Reset validation errors if valid
+ setStartDateError(undefined);
+ setEndDateError(undefined);
};
const handleStartDateTimeChange = (newStart: DateTime | null) => {
@@ -103,7 +115,7 @@ export const DateTimeRangePicker = ({
};
return (
-
+
{/* Start DateTime Picker */}
Date: Wed, 11 Dec 2024 16:08:17 -0600
Subject: [PATCH 33/35] Update storybook components with args
---
.../DatePicker/DatePicker.stories.tsx | 100 ++++++++++++++++--
.../DatePicker/DateTimePicker.stories.tsx | 81 +++++++++++---
.../DateTimeRangePicker.stories.tsx | 100 ++++++++++++------
3 files changed, 227 insertions(+), 54 deletions(-)
diff --git a/packages/manager/src/components/DatePicker/DatePicker.stories.tsx b/packages/manager/src/components/DatePicker/DatePicker.stories.tsx
index 6e9d4408783..6b3891e9889 100644
--- a/packages/manager/src/components/DatePicker/DatePicker.stories.tsx
+++ b/packages/manager/src/components/DatePicker/DatePicker.stories.tsx
@@ -8,8 +8,63 @@ import type { DateTime } from 'luxon';
type Story = StoryObj;
+export const Default: Story = {
+ argTypes: {
+ errorText: {
+ control: 'text',
+ description: 'Error text to display below the input',
+ },
+ format: {
+ control: 'text',
+ description: 'Format of the date when rendered in the input field',
+ },
+ helperText: {
+ control: 'text',
+ description: 'Helper text to display below the input',
+ },
+ label: {
+ control: 'text',
+ description: 'Label to display for the date picker input',
+ },
+ onChange: {
+ action: 'date-changed',
+ description: 'Callback function fired when the value changes',
+ },
+ placeholder: {
+ control: 'text',
+ description: 'Placeholder text for the date picker input',
+ },
+ textFieldProps: {
+ control: 'object',
+ description:
+ 'Additional props to pass to the underlying TextField component',
+ },
+ value: {
+ control: 'date',
+ description: 'The currently selected date',
+ },
+ },
+ args: {
+ errorText: '',
+ format: 'yyyy-MM-dd',
+ label: 'Select a Date',
+ onChange: action('date-changed'),
+ placeholder: 'yyyy-MM-dd',
+ textFieldProps: { label: 'Select a Date' },
+ value: null,
+ },
+};
+
export const ControlledExample: Story = {
- render: () => {
+ args: {
+ errorText: '',
+ format: 'yyyy-MM-dd',
+ helperText: 'This is a controlled DatePicker',
+ label: 'Controlled Date Picker',
+ placeholder: 'yyyy-MM-dd',
+ value: null,
+ },
+ render: (args) => {
const ControlledDatePicker = () => {
const [selectedDate, setSelectedDate] = React.useState();
@@ -19,14 +74,7 @@ export const ControlledExample: Story = {
};
return (
-
+
);
};
@@ -35,6 +83,40 @@ export const ControlledExample: Story = {
};
const meta: Meta = {
+ argTypes: {
+ errorText: {
+ control: 'text',
+ },
+ format: {
+ control: 'text',
+ },
+ helperText: {
+ control: 'text',
+ },
+ label: {
+ control: 'text',
+ },
+ onChange: {
+ action: 'date-changed',
+ },
+ placeholder: {
+ control: 'text',
+ },
+ textFieldProps: {
+ control: 'object',
+ },
+ value: {
+ control: 'date',
+ },
+ },
+ args: {
+ errorText: '',
+ format: 'yyyy-MM-dd',
+ helperText: '',
+ label: 'Select a Date',
+ placeholder: 'yyyy-MM-dd',
+ value: null,
+ },
component: DatePicker,
title: 'Components/DatePicker',
};
diff --git a/packages/manager/src/components/DatePicker/DateTimePicker.stories.tsx b/packages/manager/src/components/DatePicker/DateTimePicker.stories.tsx
index bb3f0c48233..9af57ff82fe 100644
--- a/packages/manager/src/components/DatePicker/DateTimePicker.stories.tsx
+++ b/packages/manager/src/components/DatePicker/DateTimePicker.stories.tsx
@@ -9,12 +9,27 @@ import type { DateTime } from 'luxon';
type Story = StoryObj;
export const ControlledExample: Story = {
- render: () => {
+ args: {
+ label: 'Controlled Date-Time Picker',
+ onApply: action('Apply clicked'),
+ onCancel: action('Cancel clicked'),
+ placeholder: 'yyyy-MM-dd HH:mm',
+ showTime: true,
+ showTimeZone: true,
+ timeSelectProps: {
+ label: 'Select Time',
+ },
+ timeZoneSelectProps: {
+ label: 'Timezone',
+ onChange: action('Timezone changed'),
+ },
+ },
+ render: (args) => {
const ControlledDateTimePicker = () => {
const [
selectedDateTime,
setSelectedDateTime,
- ] = React.useState(null); // Start with null
+ ] = React.useState(args.value || null);
const handleChange = (newDateTime: DateTime | null) => {
setSelectedDateTime(newDateTime);
@@ -23,17 +38,8 @@ export const ControlledExample: Story = {
return (
);
@@ -50,6 +56,8 @@ export const DefaultExample: Story = {
onCancel: action('Cancel clicked'),
onChange: action('Date-Time selected'),
placeholder: 'yyyy-MM-dd HH:mm',
+ showTime: true,
+ showTimeZone: true,
},
};
@@ -61,25 +69,68 @@ export const WithErrorText: Story = {
onCancel: action('Cancel clicked with error'),
onChange: action('Date-Time selected with error'),
placeholder: 'yyyy-MM-dd HH:mm',
+ showTime: true,
+ showTimeZone: true,
},
};
const meta: Meta = {
argTypes: {
+ dateCalendarProps: {
+ control: { type: 'object' },
+ description: 'Additional props for the DateCalendar component.',
+ },
errorText: {
control: { type: 'text' },
+ description: 'Error text for the date picker field.',
},
format: {
control: { type: 'text' },
+ description: 'Format for displaying the date-time.',
},
label: {
control: { type: 'text' },
+ description: 'Label for the input field.',
+ },
+ onApply: {
+ action: 'applyClicked',
+ description: 'Callback when the "Apply" button is clicked.',
+ },
+ onCancel: {
+ action: 'cancelClicked',
+ description: 'Callback when the "Cancel" button is clicked.',
+ },
+ onChange: {
+ action: 'dateTimeChanged',
+ description: 'Callback when the date-time changes.',
},
- onApply: { action: 'applyClicked' },
- onCancel: { action: 'cancelClicked' },
- onChange: { action: 'dateTimeChanged' },
placeholder: {
control: { type: 'text' },
+ description: 'Placeholder text for the input field.',
+ },
+ showTime: {
+ control: { type: 'boolean' },
+ description: 'Whether to show the time selector.',
+ },
+ showTimeZone: {
+ control: { type: 'boolean' },
+ description: 'Whether to show the timezone selector.',
+ },
+ sx: {
+ control: { type: 'object' },
+ description: 'Styles to apply to the root element.',
+ },
+ timeSelectProps: {
+ control: { type: 'object' },
+ description: 'Props for customizing the TimePicker component.',
+ },
+ timeZoneSelectProps: {
+ control: { type: 'object' },
+ description: 'Props for customizing the TimeZoneSelect component.',
+ },
+ value: {
+ control: { type: 'date' },
+ description: 'Initial or controlled dateTime value.',
},
},
args: {
diff --git a/packages/manager/src/components/DatePicker/DateTimeRangePicker.stories.tsx b/packages/manager/src/components/DatePicker/DateTimeRangePicker.stories.tsx
index 70ad6b2a39d..23c1fe11f3d 100644
--- a/packages/manager/src/components/DatePicker/DateTimeRangePicker.stories.tsx
+++ b/packages/manager/src/components/DatePicker/DateTimeRangePicker.stories.tsx
@@ -10,52 +10,61 @@ type Story = StoryObj;
export const Default: Story = {
args: {
+ endDateErrorMessage: '',
+ endDateTimeValue: null,
endLabel: 'End Date and Time',
format: 'yyyy-MM-dd HH:mm',
onChange: action('DateTime range changed'),
+ showEndTimeZone: true,
+ showStartTimeZone: true,
+ startDateErrorMessage: '',
+ startDateTimeValue: null,
startLabel: 'Start Date and Time',
+ startTimeZoneValue: null,
},
render: (args) => ,
};
export const WithInitialValues: Story = {
- render: () => {
- const ComponentWithState: React.FC = () => {
- const [start, setStart] = React.useState(
- DateTime.now().minus({ days: 1 })
- );
- const [end, setEnd] = React.useState(DateTime.now());
-
- const handleDateChange = (
- newStart: DateTime | null,
- newEnd: DateTime | null
- ) => {
- setStart(newStart);
- setEnd(newEnd);
- action('DateTime range changed')(newStart?.toISO(), newEnd?.toISO());
- };
-
- return (
-
- );
- };
+ args: {
+ endDateTimeValue: DateTime.now(),
+ endLabel: 'End Date and Time',
+ format: 'yyyy-MM-dd HH:mm',
+ onChange: action('DateTime range changed'),
+ showEndTimeZone: true,
+ showStartTimeZone: true,
+ startDateTimeValue: DateTime.now().minus({ days: 1 }),
+ startLabel: 'Start Date and Time',
+ startTimeZoneValue: 'America/New_York',
+ },
+};
- return ;
+export const WithCustomErrors: Story = {
+ args: {
+ endDateErrorMessage: 'End date must be after the start date.',
+ endDateTimeValue: DateTime.now().minus({ days: 1 }),
+ endLabel: 'Custom End Label',
+ format: 'yyyy-MM-dd HH:mm',
+ onChange: action('DateTime range changed'),
+ startDateErrorMessage: 'Start date must be before the end date.',
+ startDateTimeValue: DateTime.now().minus({ days: 2 }),
+ startLabel: 'Custom Start Label',
},
};
const meta: Meta = {
argTypes: {
+ endDateErrorMessage: {
+ control: 'text',
+ description: 'Custom error message for invalid end date',
+ },
+ endDateTimeValue: {
+ control: 'date',
+ description: 'Initial or controlled value for the end date-time',
+ },
endLabel: {
control: 'text',
- description: 'Label for the end date picker',
+ description: 'Custom label for the end date-time picker',
},
format: {
control: 'text',
@@ -65,11 +74,42 @@ const meta: Meta = {
action: 'DateTime range changed',
description: 'Callback when the date-time range changes',
},
+ showEndTimeZone: {
+ control: 'boolean',
+ description:
+ 'Whether to show the timezone selector for the end date picker',
+ },
+ showStartTimeZone: {
+ control: 'boolean',
+ description:
+ 'Whether to show the timezone selector for the start date picker',
+ },
+ startDateErrorMessage: {
+ control: 'text',
+ description: 'Custom error message for invalid start date',
+ },
+ startDateTimeValue: {
+ control: 'date',
+ description: 'Initial or controlled value for the start date-time',
+ },
startLabel: {
control: 'text',
- description: 'Label for the start date picker',
+ description: 'Custom label for the start date-time picker',
+ },
+ startTimeZoneValue: {
+ control: 'text',
+ description: 'Initial or controlled value for the start timezone',
+ },
+ sx: {
+ control: 'object',
+ description: 'Styles to apply to the root element',
},
},
+ args: {
+ endLabel: 'End Date and Time',
+ format: 'yyyy-MM-dd HH:mm',
+ startLabel: 'Start Date and Time',
+ },
component: DateTimeRangePicker,
title: 'Components/DateTimeRangePicker',
};
From dc5291f4f1bcd3f11ebe7efbf1288e39ac675acf Mon Sep 17 00:00:00 2001
From: cpathipa <119517080+cpathipa@users.noreply.github.com>
Date: Thu, 12 Dec 2024 11:29:22 -0600
Subject: [PATCH 34/35] Update props
---
.../src/components/DatePicker/DatePicker.tsx | 6 ++----
.../src/components/DatePicker/DateTimePicker.tsx | 7 +++----
.../components/DatePicker/DateTimeRangePicker.tsx | 14 ++++----------
3 files changed, 9 insertions(+), 18 deletions(-)
diff --git a/packages/manager/src/components/DatePicker/DatePicker.tsx b/packages/manager/src/components/DatePicker/DatePicker.tsx
index f8fc1965ac8..25fb95ff048 100644
--- a/packages/manager/src/components/DatePicker/DatePicker.tsx
+++ b/packages/manager/src/components/DatePicker/DatePicker.tsx
@@ -20,7 +20,7 @@ export interface DatePickerProps
/** Label to display for the date picker input */
label?: string;
/** Callback function fired when the value changes */
- onChange?: (newDate: DateTime | null) => void;
+ onChange: (newDate: DateTime | null) => void;
/** Placeholder text for the date picker input */
placeholder?: string;
/** Additional props to pass to the underlying TextField component */
@@ -42,9 +42,7 @@ export const DatePicker = ({
const theme = useTheme();
const onChangeHandler = (newDate: DateTime | null) => {
- if (onChange) {
- onChange(newDate);
- }
+ onChange(newDate);
};
return (
diff --git a/packages/manager/src/components/DatePicker/DateTimePicker.tsx b/packages/manager/src/components/DatePicker/DateTimePicker.tsx
index d7b138e5d8e..b503ba37674 100644
--- a/packages/manager/src/components/DatePicker/DateTimePicker.tsx
+++ b/packages/manager/src/components/DatePicker/DateTimePicker.tsx
@@ -31,7 +31,7 @@ export interface DateTimePickerProps {
/** Callback when the "Cancel" button is clicked */
onCancel?: () => void;
/** Callback when date-time changes */
- onChange?: (dateTime: DateTime | null) => void;
+ onChange: (dateTime: DateTime | null) => void;
/** Placeholder text for the input field */
placeholder?: string;
/** Whether to show the time selector */
@@ -115,9 +115,8 @@ export const DateTimePicker = ({
const handleApply = () => {
setAnchorEl(null);
- if (onChange) {
- onChange(selectedDateTime);
- }
+ onChange(selectedDateTime);
+
if (onApply) {
onApply();
}
diff --git a/packages/manager/src/components/DatePicker/DateTimeRangePicker.tsx b/packages/manager/src/components/DatePicker/DateTimeRangePicker.tsx
index a2a0408947e..66170f05586 100644
--- a/packages/manager/src/components/DatePicker/DateTimeRangePicker.tsx
+++ b/packages/manager/src/components/DatePicker/DateTimeRangePicker.tsx
@@ -16,7 +16,7 @@ interface DateTimeRangePickerProps {
/** Format for displaying the date-time */
format?: string;
/** Callback when the date-time range changes */
- onChange?: (
+ onChange: (
start: DateTime | null,
end: DateTime | null,
startTimeZone?: null | string
@@ -92,26 +92,20 @@ export const DateTimeRangePicker = ({
setStartDateTime(newStart);
validateDates(newStart, endDateTime, 'start');
- if (onChange) {
- onChange(newStart, endDateTime, startTimeZone);
- }
+ onChange(newStart, endDateTime, startTimeZone);
};
const handleEndDateTimeChange = (newEnd: DateTime | null) => {
setEndDateTime(newEnd);
validateDates(startDateTime, newEnd, 'end');
- if (onChange) {
- onChange(startDateTime, newEnd, startTimeZone);
- }
+ onChange(startDateTime, newEnd, startTimeZone);
};
const handleStartTimeZoneChange = (newTimeZone: null | string) => {
setStartTimeZone(newTimeZone);
- if (onChange) {
- onChange(startDateTime, endDateTime, newTimeZone);
- }
+ onChange(startDateTime, endDateTime, newTimeZone);
};
return (
From 8e3c8c13b5c0aef1b62cf56046a947e5d873d3d6 Mon Sep 17 00:00:00 2001
From: cpathipa <119517080+cpathipa@users.noreply.github.com>
Date: Thu, 12 Dec 2024 14:40:44 -0600
Subject: [PATCH 35/35] PR feedback - @hana-akamai
---
.../manager/src/components/DatePicker/DatePicker.stories.tsx | 2 +-
.../src/components/DatePicker/DateTimePicker.stories.tsx | 2 +-
.../src/components/DatePicker/DateTimeRangePicker.stories.tsx | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/packages/manager/src/components/DatePicker/DatePicker.stories.tsx b/packages/manager/src/components/DatePicker/DatePicker.stories.tsx
index 6b3891e9889..640fae75510 100644
--- a/packages/manager/src/components/DatePicker/DatePicker.stories.tsx
+++ b/packages/manager/src/components/DatePicker/DatePicker.stories.tsx
@@ -118,7 +118,7 @@ const meta: Meta = {
value: null,
},
component: DatePicker,
- title: 'Components/DatePicker',
+ title: 'Components/DatePicker/DatePicker',
};
export default meta;
diff --git a/packages/manager/src/components/DatePicker/DateTimePicker.stories.tsx b/packages/manager/src/components/DatePicker/DateTimePicker.stories.tsx
index 9af57ff82fe..7df04ed26cd 100644
--- a/packages/manager/src/components/DatePicker/DateTimePicker.stories.tsx
+++ b/packages/manager/src/components/DatePicker/DateTimePicker.stories.tsx
@@ -139,7 +139,7 @@ const meta: Meta = {
placeholder: 'Select a date and time',
},
component: DateTimePicker,
- title: 'Components/DateTimePicker',
+ title: 'Components/DatePicker/DateTimePicker',
};
export default meta;
diff --git a/packages/manager/src/components/DatePicker/DateTimeRangePicker.stories.tsx b/packages/manager/src/components/DatePicker/DateTimeRangePicker.stories.tsx
index 23c1fe11f3d..f34ac6b190b 100644
--- a/packages/manager/src/components/DatePicker/DateTimeRangePicker.stories.tsx
+++ b/packages/manager/src/components/DatePicker/DateTimeRangePicker.stories.tsx
@@ -111,7 +111,7 @@ const meta: Meta = {
startLabel: 'Start Date and Time',
},
component: DateTimeRangePicker,
- title: 'Components/DateTimeRangePicker',
+ title: 'Components/DatePicker/DateTimeRangePicker',
};
export default meta;