[CLD-9186] Remove onboarding tasklist, add preview banner (#31203)
* Remove pricing modal. Adjust everywhere to instead open mattermost.com/pricing. When air gapped, don't show buttons to view plans. * Fix lint * Further clean up of unused code. Fixes for linter * Remove onboarding tasklist for previews, add Cloud previer banner * Fixes for linter, i18n * Revert dev lines * Fix lint * When below one minute, switch to seconds * fix linter * fixes for PR feedback * useExternalLink for opening pricing modal with enriched params * Fix i17n * Fix style, tests * Update webapp/channels/src/components/announcement_bar/cloud_preview_announcement_bar/index.tsx Co-authored-by: Guillermo Vayá <guillermo.vaya@mattermost.com> * Fix linter --------- Co-authored-by: Guillermo Vayá <guillermo.vaya@mattermost.com>
Этот коммит содержится в:
@@ -62,7 +62,34 @@ func ensureCloudInterface(c *Context, where string) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getPreviewSubscription(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
license := c.App.Channels().License()
|
||||||
|
subscription := &model.Subscription{
|
||||||
|
ID: "cloud-preview",
|
||||||
|
ProductID: license.SkuName,
|
||||||
|
StartAt: license.StartsAt,
|
||||||
|
TrialEndAt: license.ExpiresAt,
|
||||||
|
EndAt: license.ExpiresAt,
|
||||||
|
IsFreeTrial: "true",
|
||||||
|
IsCloudPreview: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
json, err := json.Marshal(subscription)
|
||||||
|
if err != nil {
|
||||||
|
c.Err = model.NewAppError("Api4.getSubscription", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Write(json)
|
||||||
|
}
|
||||||
|
|
||||||
func getSubscription(c *Context, w http.ResponseWriter, r *http.Request) {
|
func getSubscription(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Preview subscription is a special case for cloud preview licenses.
|
||||||
|
if c.App.Channels().License().IsCloudPreview() {
|
||||||
|
getPreviewSubscription(c, w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
ensured := ensureCloudInterface(c, "Api4.getSubscription")
|
ensured := ensureCloudInterface(c, "Api4.getSubscription")
|
||||||
if !ensured {
|
if !ensured {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -191,6 +191,7 @@ type Subscription struct {
|
|||||||
CancelAt *int64 `json:"cancel_at"`
|
CancelAt *int64 `json:"cancel_at"`
|
||||||
WillRenew string `json:"will_renew"`
|
WillRenew string `json:"will_renew"`
|
||||||
SimulatedCurrentTimeMs *int64 `json:"simulated_current_time_ms"`
|
SimulatedCurrentTimeMs *int64 `json:"simulated_current_time_ms"`
|
||||||
|
IsCloudPreview bool `json:"is_cloud_preview"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Subscription) DaysToExpiration() int64 {
|
func (s *Subscription) DaysToExpiration() int64 {
|
||||||
|
|||||||
@@ -350,6 +350,11 @@ func (l *License) IsStarted() bool {
|
|||||||
return l.StartsAt < GetMillis()
|
return l.StartsAt < GetMillis()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cloud preview is a cloud license, that is also a trial, and the difference between the start and end date is exactly 1 hour.
|
||||||
|
func (l *License) IsCloudPreview() bool {
|
||||||
|
return l.IsCloud() && l.IsTrialLicense() && l.ExpiresAt-l.StartsAt == 1*time.Hour.Milliseconds()
|
||||||
|
}
|
||||||
|
|
||||||
func (l *License) IsCloud() bool {
|
func (l *License) IsCloud() bool {
|
||||||
return l != nil && l.Features != nil && l.Features.Cloud != nil && *l.Features.Cloud
|
return l != nil && l.Features != nil && l.Features.Cloud != nil && *l.Features.Cloud
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type {ClientLicense, ClientConfig, WarnMetricStatus} from '@mattermost/ty
|
|||||||
|
|
||||||
import withGetCloudSubscription from 'components/common/hocs/cloud/with_get_cloud_subscription';
|
import withGetCloudSubscription from 'components/common/hocs/cloud/with_get_cloud_subscription';
|
||||||
|
|
||||||
|
import CloudPreviewAnnouncementBar from './cloud_preview_announcement_bar';
|
||||||
import CloudTrialAnnouncementBar from './cloud_trial_announcement_bar';
|
import CloudTrialAnnouncementBar from './cloud_trial_announcement_bar';
|
||||||
import CloudTrialEndAnnouncementBar from './cloud_trial_ended_announcement_bar';
|
import CloudTrialEndAnnouncementBar from './cloud_trial_ended_announcement_bar';
|
||||||
import ConfigurationAnnouncementBar from './configuration_bar';
|
import ConfigurationAnnouncementBar from './configuration_bar';
|
||||||
@@ -66,6 +67,7 @@ class AnnouncementBarController extends React.PureComponent<Props> {
|
|||||||
let paymentAnnouncementBar = null;
|
let paymentAnnouncementBar = null;
|
||||||
let cloudTrialAnnouncementBar = null;
|
let cloudTrialAnnouncementBar = null;
|
||||||
let cloudTrialEndAnnouncementBar = null;
|
let cloudTrialEndAnnouncementBar = null;
|
||||||
|
let cloudPreviewAnnouncementBar = null;
|
||||||
const notifyAdminDowngradeDelinquencyBar = null;
|
const notifyAdminDowngradeDelinquencyBar = null;
|
||||||
const toYearlyNudgeBannerDismissable = null;
|
const toYearlyNudgeBannerDismissable = null;
|
||||||
if (this.props.license?.Cloud === 'true') {
|
if (this.props.license?.Cloud === 'true') {
|
||||||
@@ -78,6 +80,9 @@ class AnnouncementBarController extends React.PureComponent<Props> {
|
|||||||
cloudTrialEndAnnouncementBar = (
|
cloudTrialEndAnnouncementBar = (
|
||||||
<CloudTrialEndAnnouncementBar/>
|
<CloudTrialEndAnnouncementBar/>
|
||||||
);
|
);
|
||||||
|
cloudPreviewAnnouncementBar = (
|
||||||
|
<CloudPreviewAnnouncementBar/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let autoStartTrialModal = null;
|
let autoStartTrialModal = null;
|
||||||
@@ -109,6 +114,7 @@ class AnnouncementBarController extends React.PureComponent<Props> {
|
|||||||
{paymentAnnouncementBar}
|
{paymentAnnouncementBar}
|
||||||
{cloudTrialAnnouncementBar}
|
{cloudTrialAnnouncementBar}
|
||||||
{cloudTrialEndAnnouncementBar}
|
{cloudTrialEndAnnouncementBar}
|
||||||
|
{cloudPreviewAnnouncementBar}
|
||||||
{notifyAdminDowngradeDelinquencyBar}
|
{notifyAdminDowngradeDelinquencyBar}
|
||||||
{toYearlyNudgeBannerDismissable}
|
{toYearlyNudgeBannerDismissable}
|
||||||
{this.props.license?.Cloud !== 'true' && <OverageUsersBanner/>}
|
{this.props.license?.Cloud !== 'true' && <OverageUsersBanner/>}
|
||||||
|
|||||||
@@ -0,0 +1,283 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import * as reactRedux from 'react-redux';
|
||||||
|
|
||||||
|
import type {Subscription} from '@mattermost/types/cloud';
|
||||||
|
|
||||||
|
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
|
||||||
|
import mockStore from 'tests/test_store';
|
||||||
|
|
||||||
|
import CloudPreviewAnnouncementBar from './index';
|
||||||
|
|
||||||
|
describe('components/announcement_bar/CloudPreviewAnnouncementBar', () => {
|
||||||
|
const useDispatchMock = jest.spyOn(reactRedux, 'useDispatch');
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
useDispatchMock.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
const baseSubscription: Subscription = {
|
||||||
|
id: 'test-id',
|
||||||
|
customer_id: 'test-customer',
|
||||||
|
product_id: 'test-product',
|
||||||
|
add_ons: [],
|
||||||
|
start_at: Date.now() - (24 * 60 * 60 * 1000), // 1 day ago
|
||||||
|
end_at: Date.now() + (2 * 60 * 60 * 1000), // 2 hours from now
|
||||||
|
create_at: Date.now() - (24 * 60 * 60 * 1000),
|
||||||
|
seats: 10,
|
||||||
|
trial_end_at: 0,
|
||||||
|
is_free_trial: 'false',
|
||||||
|
is_cloud_preview: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
views: {
|
||||||
|
announcementBar: {
|
||||||
|
announcementBarState: {
|
||||||
|
announcementBarCount: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
entities: {
|
||||||
|
general: {
|
||||||
|
license: {
|
||||||
|
IsLicensed: 'true',
|
||||||
|
Cloud: 'true',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
currentUserId: 'current_user_id',
|
||||||
|
profiles: {
|
||||||
|
current_user_id: {roles: 'system_admin'},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cloud: {
|
||||||
|
subscription: baseSubscription,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should show banner when is_cloud_preview is true and isCloud is true', () => {
|
||||||
|
const store = mockStore(initialState);
|
||||||
|
|
||||||
|
const dummyDispatch = jest.fn();
|
||||||
|
useDispatchMock.mockReturnValue(dummyDispatch);
|
||||||
|
|
||||||
|
const wrapper = mountWithIntl(
|
||||||
|
<reactRedux.Provider store={store}>
|
||||||
|
<CloudPreviewAnnouncementBar/>
|
||||||
|
</reactRedux.Provider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(wrapper.find('AnnouncementBar').exists()).toEqual(true);
|
||||||
|
expect(wrapper.text()).toContain('This is your Mattermost preview environment');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show banner when is_cloud_preview is false', () => {
|
||||||
|
const state = JSON.parse(JSON.stringify(initialState));
|
||||||
|
state.entities.cloud.subscription = {
|
||||||
|
...baseSubscription,
|
||||||
|
is_cloud_preview: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const store = mockStore(state);
|
||||||
|
|
||||||
|
const dummyDispatch = jest.fn();
|
||||||
|
useDispatchMock.mockReturnValue(dummyDispatch);
|
||||||
|
|
||||||
|
const wrapper = mountWithIntl(
|
||||||
|
<reactRedux.Provider store={store}>
|
||||||
|
<CloudPreviewAnnouncementBar/>
|
||||||
|
</reactRedux.Provider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(wrapper.find('AnnouncementBar').exists()).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show banner when not cloud', () => {
|
||||||
|
const state = JSON.parse(JSON.stringify(initialState));
|
||||||
|
state.entities.general.license.Cloud = 'false';
|
||||||
|
|
||||||
|
const store = mockStore(state);
|
||||||
|
|
||||||
|
const dummyDispatch = jest.fn();
|
||||||
|
useDispatchMock.mockReturnValue(dummyDispatch);
|
||||||
|
|
||||||
|
const wrapper = mountWithIntl(
|
||||||
|
<reactRedux.Provider store={store}>
|
||||||
|
<CloudPreviewAnnouncementBar/>
|
||||||
|
</reactRedux.Provider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(wrapper.find('AnnouncementBar').exists()).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show banner when subscription is undefined', () => {
|
||||||
|
const state = JSON.parse(JSON.stringify(initialState));
|
||||||
|
state.entities.cloud.subscription = undefined;
|
||||||
|
|
||||||
|
const store = mockStore(state);
|
||||||
|
|
||||||
|
const dummyDispatch = jest.fn();
|
||||||
|
useDispatchMock.mockReturnValue(dummyDispatch);
|
||||||
|
|
||||||
|
const wrapper = mountWithIntl(
|
||||||
|
<reactRedux.Provider store={store}>
|
||||||
|
<CloudPreviewAnnouncementBar/>
|
||||||
|
</reactRedux.Provider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(wrapper.find('AnnouncementBar').exists()).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display time in correct format when less than a day', () => {
|
||||||
|
const store = mockStore(initialState);
|
||||||
|
|
||||||
|
const dummyDispatch = jest.fn();
|
||||||
|
useDispatchMock.mockReturnValue(dummyDispatch);
|
||||||
|
|
||||||
|
const wrapper = mountWithIntl(
|
||||||
|
<reactRedux.Provider store={store}>
|
||||||
|
<CloudPreviewAnnouncementBar/>
|
||||||
|
</reactRedux.Provider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should show format like "02h 00m"
|
||||||
|
expect(wrapper.text()).toMatch(/Time left: \d{2}h \d{2}m/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display time with days when more than a day', () => {
|
||||||
|
const state = JSON.parse(JSON.stringify(initialState));
|
||||||
|
state.entities.cloud.subscription = {
|
||||||
|
...baseSubscription,
|
||||||
|
end_at: Date.now() + (25 * 60 * 60 * 1000), // 25 hours from now
|
||||||
|
};
|
||||||
|
|
||||||
|
const store = mockStore(state);
|
||||||
|
|
||||||
|
const dummyDispatch = jest.fn();
|
||||||
|
useDispatchMock.mockReturnValue(dummyDispatch);
|
||||||
|
|
||||||
|
const wrapper = mountWithIntl(
|
||||||
|
<reactRedux.Provider store={store}>
|
||||||
|
<CloudPreviewAnnouncementBar/>
|
||||||
|
</reactRedux.Provider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should show format like "1d 01h 00m"
|
||||||
|
expect(wrapper.text()).toMatch(/Time left: 1d \d{2}h \d{2}m/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display only minutes when less than an hour', () => {
|
||||||
|
const state = JSON.parse(JSON.stringify(initialState));
|
||||||
|
state.entities.cloud.subscription = {
|
||||||
|
...baseSubscription,
|
||||||
|
end_at: Date.now() + (45 * 60 * 1000), // 45 minutes from now
|
||||||
|
};
|
||||||
|
|
||||||
|
const store = mockStore(state);
|
||||||
|
|
||||||
|
const dummyDispatch = jest.fn();
|
||||||
|
useDispatchMock.mockReturnValue(dummyDispatch);
|
||||||
|
|
||||||
|
const wrapper = mountWithIntl(
|
||||||
|
<reactRedux.Provider store={store}>
|
||||||
|
<CloudPreviewAnnouncementBar/>
|
||||||
|
</reactRedux.Provider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should show format like "45m"
|
||||||
|
expect(wrapper.text()).toMatch(/Time left: \d{2}m/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display seconds when less than a minute', () => {
|
||||||
|
const state = JSON.parse(JSON.stringify(initialState));
|
||||||
|
state.entities.cloud.subscription = {
|
||||||
|
...baseSubscription,
|
||||||
|
end_at: Date.now() + (30 * 1000), // 30 seconds from now
|
||||||
|
};
|
||||||
|
|
||||||
|
const store = mockStore(state);
|
||||||
|
|
||||||
|
const dummyDispatch = jest.fn();
|
||||||
|
useDispatchMock.mockReturnValue(dummyDispatch);
|
||||||
|
|
||||||
|
const wrapper = mountWithIntl(
|
||||||
|
<reactRedux.Provider store={store}>
|
||||||
|
<CloudPreviewAnnouncementBar/>
|
||||||
|
</reactRedux.Provider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should show format like "30s"
|
||||||
|
expect(wrapper.text()).toMatch(/Time left: \d+s/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display 00:00 when time has expired', () => {
|
||||||
|
const state = JSON.parse(JSON.stringify(initialState));
|
||||||
|
state.entities.cloud.subscription = {
|
||||||
|
...baseSubscription,
|
||||||
|
end_at: Date.now() - 1000, // 1 second ago
|
||||||
|
};
|
||||||
|
|
||||||
|
const store = mockStore(state);
|
||||||
|
|
||||||
|
const dummyDispatch = jest.fn();
|
||||||
|
useDispatchMock.mockReturnValue(dummyDispatch);
|
||||||
|
|
||||||
|
const wrapper = mountWithIntl(
|
||||||
|
<reactRedux.Provider store={store}>
|
||||||
|
<CloudPreviewAnnouncementBar/>
|
||||||
|
</reactRedux.Provider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(wrapper.text()).toContain('Time left: 00:00');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not be dismissable', () => {
|
||||||
|
const store = mockStore(initialState);
|
||||||
|
|
||||||
|
const dummyDispatch = jest.fn();
|
||||||
|
useDispatchMock.mockReturnValue(dummyDispatch);
|
||||||
|
|
||||||
|
const wrapper = mountWithIntl(
|
||||||
|
<reactRedux.Provider store={store}>
|
||||||
|
<CloudPreviewAnnouncementBar/>
|
||||||
|
</reactRedux.Provider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(wrapper.find('AnnouncementBar').prop('showCloseButton')).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show contact sales button', () => {
|
||||||
|
const store = mockStore(initialState);
|
||||||
|
|
||||||
|
const dummyDispatch = jest.fn();
|
||||||
|
useDispatchMock.mockReturnValue(dummyDispatch);
|
||||||
|
|
||||||
|
const wrapper = mountWithIntl(
|
||||||
|
<reactRedux.Provider store={store}>
|
||||||
|
<CloudPreviewAnnouncementBar/>
|
||||||
|
</reactRedux.Provider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(wrapper.text()).toContain('Contact sales');
|
||||||
|
expect(wrapper.find('AnnouncementBar').prop('showLinkAsButton')).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have advisor type', () => {
|
||||||
|
const store = mockStore(initialState);
|
||||||
|
|
||||||
|
const dummyDispatch = jest.fn();
|
||||||
|
useDispatchMock.mockReturnValue(dummyDispatch);
|
||||||
|
|
||||||
|
const wrapper = mountWithIntl(
|
||||||
|
<reactRedux.Provider store={store}>
|
||||||
|
<CloudPreviewAnnouncementBar/>
|
||||||
|
</reactRedux.Provider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(wrapper.find('AnnouncementBar').prop('type')).toEqual('advisor');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React, {useEffect, useState, useCallback} from 'react';
|
||||||
|
import {FormattedMessage} from 'react-intl';
|
||||||
|
import {useSelector} from 'react-redux';
|
||||||
|
|
||||||
|
import {InformationOutlineIcon} from '@mattermost/compass-icons/components';
|
||||||
|
|
||||||
|
import {getCloudSubscription} from 'mattermost-redux/selectors/entities/cloud';
|
||||||
|
import {getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||||
|
|
||||||
|
import {trackEvent} from 'actions/telemetry_actions';
|
||||||
|
|
||||||
|
import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink';
|
||||||
|
|
||||||
|
import {AnnouncementBarTypes} from 'utils/constants';
|
||||||
|
|
||||||
|
import AnnouncementBar from '../default_announcement_bar';
|
||||||
|
|
||||||
|
const CloudPreviewAnnouncementBar: React.FC = () => {
|
||||||
|
const subscription = useSelector(getCloudSubscription);
|
||||||
|
const license = useSelector(getLicense);
|
||||||
|
const isCloud = license?.Cloud === 'true';
|
||||||
|
const [openContactSales] = useOpenSalesLink();
|
||||||
|
|
||||||
|
const [timeLeft, setTimeLeft] = useState<string>('');
|
||||||
|
|
||||||
|
const calculateTimeLeft = useCallback(() => {
|
||||||
|
if (!subscription?.end_at) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const endTime = subscription.end_at;
|
||||||
|
const timeDiff = endTime - now;
|
||||||
|
|
||||||
|
if (timeDiff <= 0) {
|
||||||
|
return '00:00';
|
||||||
|
}
|
||||||
|
|
||||||
|
const days = Math.floor(timeDiff / (1000 * 60 * 60 * 24));
|
||||||
|
const hours = Math.floor((timeDiff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||||
|
const minutes = Math.floor((timeDiff % (1000 * 60 * 60)) / (1000 * 60));
|
||||||
|
|
||||||
|
// If less than 1 minute, show seconds
|
||||||
|
if (days === 0 && hours === 0 && minutes === 0) {
|
||||||
|
const seconds = Math.floor((timeDiff % (1000 * 60)) / 1000);
|
||||||
|
return `${seconds}s`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build time string based on what units are needed
|
||||||
|
const parts = [];
|
||||||
|
if (days > 0) {
|
||||||
|
parts.push(`${days}d`);
|
||||||
|
}
|
||||||
|
if (hours > 0 || days > 0) {
|
||||||
|
parts.push(`${hours.toString().padStart(2, '0')}h`);
|
||||||
|
}
|
||||||
|
parts.push(`${minutes.toString().padStart(2, '0')}m`);
|
||||||
|
|
||||||
|
return parts.join(' ');
|
||||||
|
}, [subscription?.end_at]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!subscription?.is_cloud_preview || !isCloud) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
let interval: NodeJS.Timeout;
|
||||||
|
|
||||||
|
const updateTimeAndScheduleNext = () => {
|
||||||
|
setTimeLeft(calculateTimeLeft());
|
||||||
|
|
||||||
|
// Calculate time remaining to determine next interval
|
||||||
|
const now = Date.now();
|
||||||
|
const endTime = subscription.end_at || 0;
|
||||||
|
const timeDiff = endTime - now;
|
||||||
|
const minutesLeft = Math.floor(timeDiff / (1000 * 60));
|
||||||
|
|
||||||
|
// Use 1 second interval if less than 1 minute remains, otherwise 60 seconds
|
||||||
|
const intervalTime = minutesLeft < 1 ? 1000 : 60000;
|
||||||
|
|
||||||
|
// Schedule the next update
|
||||||
|
interval = setTimeout(updateTimeAndScheduleNext, intervalTime);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start the update cycle
|
||||||
|
updateTimeAndScheduleNext();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (interval) {
|
||||||
|
clearTimeout(interval);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [subscription, isCloud, calculateTimeLeft]);
|
||||||
|
|
||||||
|
const handleContactSalesClick = useCallback((e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
trackEvent('admin', 'cloud_preview_announcement_bar_contact_sales');
|
||||||
|
openContactSales();
|
||||||
|
}, [openContactSales]);
|
||||||
|
|
||||||
|
if (!subscription?.is_cloud_preview || !isCloud) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='announcement_bar.cloud_preview.message'
|
||||||
|
defaultMessage='This is your Mattermost preview environment. Time left: {timeLeft}'
|
||||||
|
values={{timeLeft: timeLeft || '00:00'}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const contactSalesText = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='announcement_bar.cloud_preview.contact_sales'
|
||||||
|
defaultMessage='Contact sales'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnnouncementBar
|
||||||
|
type={AnnouncementBarTypes.ADVISOR}
|
||||||
|
showCloseButton={false}
|
||||||
|
message={message}
|
||||||
|
icon={<InformationOutlineIcon size={16}/>}
|
||||||
|
showLinkAsButton={true}
|
||||||
|
onButtonClick={handleContactSalesClick}
|
||||||
|
ctaText={contactSalesText}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CloudPreviewAnnouncementBar;
|
||||||
@@ -88,8 +88,8 @@ class CloudTrialAnnouncementBarInternal extends React.PureComponent<PropsWithPri
|
|||||||
};
|
};
|
||||||
|
|
||||||
shouldShowBanner = () => {
|
shouldShowBanner = () => {
|
||||||
const {isFreeTrial, userIsAdmin, isCloud, isAirGapped} = this.props;
|
const {isFreeTrial, userIsAdmin, isCloud, isAirGapped, subscription} = this.props;
|
||||||
return isFreeTrial && userIsAdmin && isCloud && !isAirGapped;
|
return isFreeTrial && userIsAdmin && isCloud && !isAirGapped && !subscription?.is_cloud_preview;
|
||||||
};
|
};
|
||||||
|
|
||||||
isDismissable = () => {
|
isDismissable = () => {
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import {CloseIcon, PlaylistCheckIcon} from '@mattermost/compass-icons/components
|
|||||||
|
|
||||||
import {getPrevTrialLicense} from 'mattermost-redux/actions/admin';
|
import {getPrevTrialLicense} from 'mattermost-redux/actions/admin';
|
||||||
import {getMyPreferences, savePreferences} from 'mattermost-redux/actions/preferences';
|
import {getMyPreferences, savePreferences} from 'mattermost-redux/actions/preferences';
|
||||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
import {getCloudSubscription} from 'mattermost-redux/selectors/entities/cloud';
|
||||||
|
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||||
import {
|
import {
|
||||||
getBool,
|
getBool,
|
||||||
getMyPreferences as getMyPreferencesSelector,
|
getMyPreferences as getMyPreferencesSelector,
|
||||||
@@ -128,6 +129,10 @@ const Button = styled.button<{open: boolean}>(({open}) => {
|
|||||||
const OnBoardingTaskList = (): JSX.Element | null => {
|
const OnBoardingTaskList = (): JSX.Element | null => {
|
||||||
const {formatMessage} = useIntl();
|
const {formatMessage} = useIntl();
|
||||||
const hasPreferences = useSelector((state: GlobalState) => Object.keys(getMyPreferencesSelector(state)).length !== 0);
|
const hasPreferences = useSelector((state: GlobalState) => Object.keys(getMyPreferencesSelector(state)).length !== 0);
|
||||||
|
const subscription = useSelector(getCloudSubscription);
|
||||||
|
const license = useSelector(getLicense);
|
||||||
|
const isCloud = license?.Cloud === 'true';
|
||||||
|
const isCloudPreview = subscription?.is_cloud_preview === true;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(getPrevTrialLicense());
|
dispatch(getPrevTrialLicense());
|
||||||
@@ -244,7 +249,7 @@ const OnBoardingTaskList = (): JSX.Element | null => {
|
|||||||
trackEvent(OnboardingTaskCategory, open ? OnboardingTaskList.ONBOARDING_TASK_LIST_CLOSE : OnboardingTaskList.ONBOARDING_TASK_LIST_OPEN);
|
trackEvent(OnboardingTaskCategory, open ? OnboardingTaskList.ONBOARDING_TASK_LIST_CLOSE : OnboardingTaskList.ONBOARDING_TASK_LIST_OPEN);
|
||||||
}, [open, currentUserId]);
|
}, [open, currentUserId]);
|
||||||
|
|
||||||
if (!hasPreferences || !showTaskList || !isEnableOnboardingFlow) {
|
if (!hasPreferences || !showTaskList || !isEnableOnboardingFlow || (isCloud && isCloudPreview)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,7 +308,7 @@ const OnBoardingTaskList = (): JSX.Element | null => {
|
|||||||
>
|
>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='onboardingTask.checklist.dismiss_link'
|
id='onboardingTask.checklist.dismiss_link'
|
||||||
defaultMessage='No thanks, I’ll figure it out myself'
|
defaultMessage="No thanks, I'll figure it out myself"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -3148,6 +3148,8 @@
|
|||||||
"analytics.team.totalUsers": "Total Activated Users",
|
"analytics.team.totalUsers": "Total Activated Users",
|
||||||
"analytics.team.totalUsers.title.tooltip.hint": "Also called Registered Users",
|
"analytics.team.totalUsers.title.tooltip.hint": "Also called Registered Users",
|
||||||
"analytics.team.totalUsers.title.tooltip.title": "Activated users on this server",
|
"analytics.team.totalUsers.title.tooltip.title": "Activated users on this server",
|
||||||
|
"announcement_bar.cloud_preview.contact_sales": "Contact sales",
|
||||||
|
"announcement_bar.cloud_preview.message": "This is your Mattermost preview environment. Time left: {timeLeft}",
|
||||||
"announcement_bar.error.email_verification_required": "Check your email inbox to verify the address.",
|
"announcement_bar.error.email_verification_required": "Check your email inbox to verify the address.",
|
||||||
"announcement_bar.error.license_expired": "{licenseSku} license is expired and some features may be disabled.",
|
"announcement_bar.error.license_expired": "{licenseSku} license is expired and some features may be disabled.",
|
||||||
"announcement_bar.error.license_expiring": "{licenseSku} license expires on {date, date, long}.",
|
"announcement_bar.error.license_expiring": "{licenseSku} license expires on {date, date, long}.",
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export type Subscription = {
|
|||||||
cancel_at?: number;
|
cancel_at?: number;
|
||||||
will_renew?: string;
|
will_renew?: string;
|
||||||
simulated_current_time_ms?: number;
|
simulated_current_time_ms?: number;
|
||||||
|
is_cloud_preview?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Product = {
|
export type Product = {
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user