From 22a9eb7d6030bc88c8199d3b1495cdeabd564e74 Mon Sep 17 00:00:00 2001 From: jacobshandling <61553566+jacobshandling@users.noreply.github.com> Date: Tue, 16 Jul 2024 13:27:33 -0700 Subject: [PATCH] Include timezone write when updating events; Write updated gcal timezone if only event change (#20435) ## Addresses #20431 https://www.loom.com/share/0d88eceb8fb44ef3bec70d2b0dc7479c?sid=350bb4c2-2abe-4b80-b99f-ef6c8109efac - Include timezone write when updating events - Write updated gcal timezone to Fleet events, even if it's the only change - Have frontend handle `"UTC"` being set as timezone as if it were `nil` - Small cleanups - [x] Added/updated tests - [x] Manual QA for all new/changed functionality --------- Co-authored-by: Jacob Shandling --- ee/server/calendar/google_calendar.go | 28 +++++++++++-- ee/server/calendar/google_calendar_test.go | 39 +++++++++++-------- .../pages/DashboardPage/DashboardPage.tsx | 1 - .../DashboardPage/cards/Software/Software.tsx | 1 - .../details/cards/HostSummary/HostSummary.tsx | 27 ++++++------- server/fleet/hosts.go | 2 +- 6 files changed, 63 insertions(+), 35 deletions(-) diff --git a/ee/server/calendar/google_calendar.go b/ee/server/calendar/google_calendar.go index 69bbe07012..09fe94a832 100644 --- a/ee/server/calendar/google_calendar.go +++ b/ee/server/calendar/google_calendar.go @@ -261,7 +261,7 @@ func (c *GoogleCalendar) Configure(userEmail string) error { return nil } -func (c *GoogleCalendar) GetAndUpdateEvent(event *fleet.CalendarEvent, genBodyFn func(conflict bool) (body string, ok bool, err error)) ( +func (c *GoogleCalendar) GetAndUpdateEvent(event *fleet.CalendarEvent, genBodyFn func(conflict bool) (body string, updated bool, err error)) ( *fleet.CalendarEvent, bool, error, ) { // We assume that the Fleet event has not already ended. We will simply return it if it has not been modified. @@ -269,11 +269,26 @@ func (c *GoogleCalendar) GetAndUpdateEvent(event *fleet.CalendarEvent, genBodyFn if err != nil { return nil, false, err } + + // Set current calendar instance timezone to the latest from google calendar. + c.location, err = getTimezone(c) + if err != nil { + return nil, false, err + } + tzUpdated := c.location.String() != event.TimeZone + gEvent, err := c.config.API.GetEvent(details.ID, details.ETag) + var deleted, channelStopped bool switch { // http.StatusNotModified is returned sometimes, but not always, so we need to check ETag explicitly later case googleapi.IsNotModified(err): + if tzUpdated { + // this condition occurs when the event itself hasn't been updated, but the calendar timezone + // has been, so update the Fleet event's timezone + event.TimeZone = c.location.String() + return event, true, nil + } return event, false, nil // http.StatusNotFound should be very rare -- Google keeps events for a while after they are deleted case isNotFound(err): @@ -284,6 +299,12 @@ func (c *GoogleCalendar) GetAndUpdateEvent(event *fleet.CalendarEvent, genBodyFn if !deleted && gEvent.Status != "cancelled" { if details.ETag != "" && details.ETag == gEvent.Etag { // Event was not modified + if tzUpdated { + // this condition occurs when the event itself hasn't been updated, but the calendar timezone + // has been, so just update the event's timezone + event.TimeZone = c.location.String() + return event, true, nil + } return event, false, nil } if gEvent.End == nil || (gEvent.End.DateTime == "" && gEvent.End.Date == "") { @@ -593,12 +614,12 @@ func adjustEventTimes(endTime time.Time, dayEnd time.Time) (eventStart time.Time func getTimezone(gCal *GoogleCalendar) (location *time.Location, err error) { config := gCal.config // "The ID of the user’s timezone." https://developers.google.com/calendar/api/v3/reference/settings - tz, err := config.API.GetSetting("timezone") + gCalTz, err := config.API.GetSetting("timezone") if err != nil { return nil, ctxerr.Wrap(config.Context, err, "retrieving Google calendar timezone") } - return getLocation(tz.Value, config), nil + return getLocation(gCalTz.Value, config), nil } func getLocation(tz string, config *GoogleCalendarConfig) *time.Location { @@ -616,6 +637,7 @@ func (c *GoogleCalendar) googleEventToFleetEvent(startTime time.Time, endTime ti resourceID string) ( *fleet.CalendarEvent, error, ) { + fleetEvent := &fleet.CalendarEvent{} fleetEvent.StartTime = startTime fleetEvent.EndTime = endTime diff --git a/ee/server/calendar/google_calendar_test.go b/ee/server/calendar/google_calendar_test.go index 848be3693b..5cd96bfb67 100644 --- a/ee/server/calendar/google_calendar_test.go +++ b/ee/server/calendar/google_calendar_test.go @@ -2,16 +2,17 @@ package calendar import ( "context" + "net/http" + "os" + "testing" + "time" + "github.com/fleetdm/fleet/v4/server/fleet" "github.com/go-kit/log" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/api/calendar/v3" "google.golang.org/api/googleapi" - "net/http" - "os" - "testing" - "time" ) const ( @@ -202,6 +203,8 @@ func TestGoogleCalendar_GetAndUpdateEvent(t *testing.T) { const baseETag = "event-eTag" const baseEventID = "event-id" const baseResourceID = "resource-id" + const baseTzName = "America/New_York" + baseTzLocation, _ := time.LoadLocation(baseTzName) mockAPI.GetEventFunc = func(id, eTag string) (*calendar.Event, error) { assert.Equal(t, baseEventID, id) assert.Equal(t, baseETag, eTag) @@ -209,6 +212,9 @@ func TestGoogleCalendar_GetAndUpdateEvent(t *testing.T) { Etag: baseETag, // ETag matches -- no modifications to event }, nil } + mockAPI.GetSettingFunc = func(name string) (*calendar.Setting, error) { + return &calendar.Setting{Value: baseTzName}, nil + } genBodyFn := func(bool) (string, bool, error) { t.Error("genBodyFn should not be called") return "event-body", false, nil @@ -217,11 +223,12 @@ func TestGoogleCalendar_GetAndUpdateEvent(t *testing.T) { err := cal.Configure(baseUserEmail) assert.NoError(t, err) - eventStartTime := time.Now().UTC() + eventStartTime := time.Now().In(baseTzLocation) event := &fleet.CalendarEvent{ StartTime: eventStartTime, - EndTime: time.Now().Add(time.Hour), + EndTime: time.Now().Add(time.Hour).In(baseTzLocation), Data: []byte(`{"ID":"` + baseEventID + `","ETag":"` + baseETag + `"}`), + TimeZone: baseTzName, } // ETag matches @@ -316,16 +323,19 @@ func TestGoogleCalendar_GetAndUpdateEvent(t *testing.T) { assert.Error(t, err) // Event has been modified, with custom timezone. - tzId := "Africa/Kinshasa" - location, _ := time.LoadLocation(tzId) - startTime = time.Now().Add(time.Minute).Truncate(time.Second).In(location) - endTime = time.Now().Add(time.Hour).Truncate(time.Second).In(location) + newTzName := "Africa/Kinshasa" + newTzLocation, _ := time.LoadLocation(newTzName) + mockAPI.GetSettingFunc = func(name string) (*calendar.Setting, error) { + return &calendar.Setting{Value: newTzName}, nil + } + startTime = time.Now().Add(time.Minute).Truncate(time.Second).In(newTzLocation) + endTime = time.Now().Add(time.Hour).Truncate(time.Second).In(newTzLocation) mockAPI.GetEventFunc = func(id, eTag string) (*calendar.Event, error) { return &calendar.Event{ Id: baseEventID, Etag: "new-eTag", - Start: &calendar.EventDateTime{DateTime: startTime.UTC().Format(time.RFC3339), TimeZone: tzId}, - End: &calendar.EventDateTime{DateTime: endTime.Format(time.RFC3339), TimeZone: tzId}, + Start: &calendar.EventDateTime{DateTime: startTime.UTC().Format(time.RFC3339), TimeZone: newTzName}, + End: &calendar.EventDateTime{DateTime: endTime.Format(time.RFC3339), TimeZone: newTzName}, }, nil } retrievedEvent, updated, err = cal.GetAndUpdateEvent(event, genBodyFn) @@ -341,9 +351,6 @@ func TestGoogleCalendar_GetAndUpdateEvent(t *testing.T) { mockAPI.GetEventFunc = func(id, eTag string) (*calendar.Event, error) { return nil, &googleapi.Error{Code: http.StatusNotFound} } - mockAPI.GetSettingFunc = func(name string) (*calendar.Setting, error) { - return &calendar.Setting{Value: "UTC"}, nil - } mockAPI.ListEventsFunc = func(timeMin, timeMax string) (*calendar.Events, error) { return &calendar.Events{}, nil } @@ -383,7 +390,7 @@ func TestGoogleCalendar_GetAndUpdateEvent(t *testing.T) { assert.Equal(t, uuid, retrievedEvent.UUID) assert.Equal(t, baseUserEmail, retrievedEvent.Email) newEventDate := calculateNewEventDate(eventStartTime) - expectedStartTime := time.Date(newEventDate.Year(), newEventDate.Month(), newEventDate.Day(), startHour, 0, 0, 0, time.UTC) + expectedStartTime := time.Date(newEventDate.Year(), newEventDate.Month(), newEventDate.Day(), startHour, 0, 0, 0, newTzLocation) assert.Equal(t, expectedStartTime.UTC(), retrievedEvent.StartTime.UTC()) assert.Equal(t, expectedStartTime.Add(eventLength).UTC(), retrievedEvent.EndTime.UTC()) assert.True(t, eventCreated) diff --git a/frontend/pages/DashboardPage/DashboardPage.tsx b/frontend/pages/DashboardPage/DashboardPage.tsx index 321ba4c2c7..345e6eb7b6 100644 --- a/frontend/pages/DashboardPage/DashboardPage.tsx +++ b/frontend/pages/DashboardPage/DashboardPage.tsx @@ -660,7 +660,6 @@ const DashboardPage = ({ router, location }: IDashboardProps): JSX.Element => { isSoftwareEnabled={isSoftwareEnabled} software={software} teamId={currentTeamId} - pageIndex={softwarePageIndex} navTabIndex={softwareNavTabIndex} onTabChange={onSoftwareTabChange} onQueryChange={onSoftwareQueryChange} diff --git a/frontend/pages/DashboardPage/cards/Software/Software.tsx b/frontend/pages/DashboardPage/cards/Software/Software.tsx index 86e0836ef7..0b2af37375 100644 --- a/frontend/pages/DashboardPage/cards/Software/Software.tsx +++ b/frontend/pages/DashboardPage/cards/Software/Software.tsx @@ -23,7 +23,6 @@ interface ISoftwareCardProps { isSoftwareEnabled?: boolean; software?: ISoftwareResponse; teamId?: number; - pageIndex: number; navTabIndex: number; onTabChange: (index: number, last: number, event: Event) => boolean | void; onQueryChange?: diff --git a/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx b/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx index 146809ad56..b3d272d95f 100644 --- a/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx +++ b/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx @@ -364,19 +364,20 @@ const HostSummary = ({ DATE_FNS_FORMAT_STRINGS.dateAtTime ); - const tip = timezone ? ( - <> - End user's time zone: -
- (GMT{starts_at.slice(-6)}) {timezone.replace("_", " ")} - - ) : ( - <> - End user's timezone unavailable. -
- Displaying in UTC. - - ); + const tip = + timezone && timezone !== "UTC" ? ( + <> + End user's time zone: +
+ (GMT{starts_at.slice(-6)}) {timezone.replace("_", " ")} + + ) : ( + <> + End user's timezone unavailable. +
+ Displaying in UTC. + + ); return (