[CLD-6678] Various improvements for IP filtering feature (#25485)
* Add GetInstallation function, allow IP Filtering page to fetch installation state, other fixes for IP filter feature * Fix pipelines * Run make build-templates * Fixing i18n * Fix openapi docs * Fix openapi docs again * make build-templates * Update test to ensure that spinner is removed after installation becomes stable * Fix types, style * update openapi because I can't validate locally... * Updates according to Matt's feedback * Add a limit to number of times installation is requested before an error is displayed * Make button disable immediately * Updates based on PR feedback * A couple missed occurrences of whitespace * Grammar fix in failed to fetch error --------- Co-authored-by: Gabe Jackson <3694686+gabrieljackson@users.noreply.github.com> Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
@@ -267,6 +267,36 @@
|
|||||||
$ref: "#/components/responses/Forbidden"
|
$ref: "#/components/responses/Forbidden"
|
||||||
"501":
|
"501":
|
||||||
$ref: "#/components/responses/NotImplemented"
|
$ref: "#/components/responses/NotImplemented"
|
||||||
|
/api/v4/cloud/installation:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- cloud
|
||||||
|
summary: GET endpoint for Installation information
|
||||||
|
description: >
|
||||||
|
An endpoint for fetching the installation information.
|
||||||
|
|
||||||
|
##### Permissions
|
||||||
|
|
||||||
|
Must have `sysconsole_read_site_ip_filters` permission and be licensed for Cloud.
|
||||||
|
|
||||||
|
__Minimum server version__: 9.1
|
||||||
|
__Note:__ This is intended for internal use and is subject to change.
|
||||||
|
operationId: GetEndpointForInstallationInformation
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Installation returned successfully
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/Installation"
|
||||||
|
"400":
|
||||||
|
$ref: "#/components/responses/BadRequest"
|
||||||
|
"401":
|
||||||
|
$ref: "#/components/responses/Unauthorized"
|
||||||
|
"403":
|
||||||
|
$ref: "#/components/responses/Forbidden"
|
||||||
|
"501":
|
||||||
|
$ref: "#/components/responses/NotImplemented"
|
||||||
/api/v4/cloud/subscription/invoices:
|
/api/v4/cloud/subscription/invoices:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
|
|||||||
@@ -3524,6 +3524,17 @@ components:
|
|||||||
Description:
|
Description:
|
||||||
description: A description for the CIDRBlock
|
description: A description for the CIDRBlock
|
||||||
type: string
|
type: string
|
||||||
|
Installation:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
description: A unique identifier
|
||||||
|
type: string
|
||||||
|
allowed_ip_ranges:
|
||||||
|
$ref: "#/components/schemas/AllowedIPRange"
|
||||||
|
state:
|
||||||
|
description: The current state of the installation
|
||||||
|
type: string
|
||||||
externalDocs:
|
externalDocs:
|
||||||
description: Find out more about Mattermost
|
description: Find out more about Mattermost
|
||||||
url: 'https://about.mattermost.com'
|
url: 'https://about.mattermost.com'
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ func (api *API) InitCloud() {
|
|||||||
// POST /api/v4/cloud/webhook
|
// POST /api/v4/cloud/webhook
|
||||||
api.BaseRoutes.Cloud.Handle("/webhook", api.CloudAPIKeyRequired(handleCWSWebhook)).Methods("POST")
|
api.BaseRoutes.Cloud.Handle("/webhook", api.CloudAPIKeyRequired(handleCWSWebhook)).Methods("POST")
|
||||||
|
|
||||||
|
// GET /api/v4/cloud/installation
|
||||||
|
api.BaseRoutes.Cloud.Handle("/installation", api.APISessionRequired(getInstallation)).Methods("GET")
|
||||||
|
|
||||||
// GET /api/v4/cloud/cws-health-check
|
// GET /api/v4/cloud/cws-health-check
|
||||||
api.BaseRoutes.Cloud.Handle("/check-cws-connection", api.APIHandler(handleCheckCWSConnection)).Methods("GET")
|
api.BaseRoutes.Cloud.Handle("/check-cws-connection", api.APIHandler(handleCheckCWSConnection)).Methods("GET")
|
||||||
|
|
||||||
@@ -474,6 +477,29 @@ func getCloudCustomer(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Write(json)
|
w.Write(json)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getInstallation(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
ensured := ensureCloudInterface(c, "Api4.getInstallation")
|
||||||
|
if !ensured {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadIPFilters) {
|
||||||
|
c.SetPermissionError(model.PermissionSysconsoleReadIPFilters)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
installation, err := c.App.Cloud().GetInstallation(c.AppContext.Session().UserId)
|
||||||
|
if err != nil {
|
||||||
|
c.Err = model.NewAppError("Api4.getInstallation", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewEncoder(w).Encode(installation); err != nil {
|
||||||
|
c.Err = model.NewAppError("Api4.getInstallation", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// getLicenseSelfServeStatus makes check for the license in the CWS self-serve portal and establishes if the license is renewable, expandable etc.
|
// getLicenseSelfServeStatus makes check for the license in the CWS self-serve portal and establishes if the license is renewable, expandable etc.
|
||||||
func getLicenseSelfServeStatus(c *Context, w http.ResponseWriter, r *http.Request) {
|
func getLicenseSelfServeStatus(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
ensured := ensureCloudInterface(c, "Api4.getLicenseSelfServeStatus")
|
ensured := ensureCloudInterface(c, "Api4.getLicenseSelfServeStatus")
|
||||||
|
|||||||
@@ -56,4 +56,5 @@ type CloudInterface interface {
|
|||||||
|
|
||||||
ApplyIPFilters(userID string, ranges *model.AllowedIPRanges) (*model.AllowedIPRanges, error)
|
ApplyIPFilters(userID string, ranges *model.AllowedIPRanges) (*model.AllowedIPRanges, error)
|
||||||
GetIPFilters(userID string) (*model.AllowedIPRanges, error)
|
GetIPFilters(userID string) (*model.AllowedIPRanges, error)
|
||||||
|
GetInstallation(userID string) (*model.Installation, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -395,6 +395,32 @@ func (_m *CloudInterface) GetIPFilters(userID string) (*model.AllowedIPRanges, e
|
|||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetInstallation provides a mock function with given fields: userID
|
||||||
|
func (_m *CloudInterface) GetInstallation(userID string) (*model.Installation, error) {
|
||||||
|
ret := _m.Called(userID)
|
||||||
|
|
||||||
|
var r0 *model.Installation
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(0).(func(string) (*model.Installation, error)); ok {
|
||||||
|
return rf(userID)
|
||||||
|
}
|
||||||
|
if rf, ok := ret.Get(0).(func(string) *model.Installation); ok {
|
||||||
|
r0 = rf(userID)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*model.Installation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||||
|
r1 = rf(userID)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
// GetInvoicePDF provides a mock function with given fields: userID, invoiceID
|
// GetInvoicePDF provides a mock function with given fields: userID, invoiceID
|
||||||
func (_m *CloudInterface) GetInvoicePDF(userID string, invoiceID string) ([]byte, string, error) {
|
func (_m *CloudInterface) GetInvoicePDF(userID string, invoiceID string) ([]byte, string, error) {
|
||||||
ret := _m.Called(userID, invoiceID)
|
ret := _m.Called(userID, invoiceID)
|
||||||
|
|||||||
@@ -301,6 +301,12 @@ type CreateSubscriptionRequest struct {
|
|||||||
DiscountID string `json:"discount_id"`
|
DiscountID string `json:"discount_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Installation struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
State string `json:"state"`
|
||||||
|
AllowedIPRanges *AllowedIPRanges `json:"allowed_ip_ranges"`
|
||||||
|
}
|
||||||
|
|
||||||
type Feedback struct {
|
type Feedback struct {
|
||||||
Reason string `json:"reason"`
|
Reason string `json:"reason"`
|
||||||
Comments string `json:"comments"`
|
Comments string `json:"comments"`
|
||||||
|
|||||||
@@ -367,7 +367,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td style="width:132px;">
|
<td style="width:132px;">
|
||||||
<img alt height="21" src="{{.Props.SiteURL}}/static/images/logo_email_dark.png" style="border:0;display:block;outline:none;text-decoration:none;height:21.76px;width:100%;font-size:13px;" width="132">
|
<img alt height="21" src="{{.Props.PortalURL}}/static/images/logo_email_dark.png" style="border:0;display:block;outline:none;text-decoration:none;height:21.76px;width:100%;font-size:13px;" width="132">
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -441,7 +441,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td style="width:312px;">
|
<td style="width:312px;">
|
||||||
<img alt height="auto" src="{{.Props.SiteURL}}/static/images/forgot_password_illustration.png" style="border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;" width="312">
|
<img alt height="auto" src="{{.Props.PortalURL}}/static/images/forgot_password_illustration.png" style="border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;" width="312">
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -4,32 +4,35 @@
|
|||||||
</mj-head>
|
</mj-head>
|
||||||
<mj-body css-class="emailBody" background-color="#FFFFFF">
|
<mj-body css-class="emailBody" background-color="#FFFFFF">
|
||||||
<mj-wrapper mj-class="email">
|
<mj-wrapper mj-class="email">
|
||||||
<mj-include path="./partials/logo.mjml" />
|
<mj-section padding="0px 0px 40px 0px">
|
||||||
|
<mj-column>
|
||||||
|
<mj-image mj-class="logo" src="{{.Props.PortalURL}}/static/images/logo_email_dark.png" />
|
||||||
|
</mj-column>
|
||||||
|
</mj-section>
|
||||||
<mj-include path="./partials/header.mjml" />
|
<mj-include path="./partials/header.mjml" />
|
||||||
<mj-section padding="0px">
|
<mj-section padding="0px">
|
||||||
<mj-column>
|
<mj-column>
|
||||||
<mj-image src="{{.Props.SiteURL}}/static/images/forgot_password_illustration.png" width="312px"
|
<mj-image src="{{.Props.PortalURL}}/static/images/forgot_password_illustration.png" width="312px" padding="0px" />
|
||||||
padding="0px" />
|
|
||||||
</mj-column>
|
</mj-column>
|
||||||
</mj-section>
|
</mj-section>
|
||||||
<mj-section padding="40px 0px 40px 0px">
|
<mj-section padding="40px 0px 40px 0px">
|
||||||
<mj-column>
|
<mj-column>
|
||||||
<mj-text padding-bottom="9px" css-class="footerTitle" padding="0px">
|
<mj-text padding-bottom="9px" css-class="footerTitle" padding="0px">
|
||||||
{{.Props.TroubleAccessingTitle}}
|
{{.Props.TroubleAccessingTitle}}
|
||||||
</mj-text>
|
</mj-text>
|
||||||
<mj-raw>{{if .Props.ActorEmail}}</mj-raw>
|
<mj-raw>{{if .Props.ActorEmail}}</mj-raw>
|
||||||
<mj-button padding-top="0px" padding-bottom="1px" font-size="14px" line-height="20px" background-color="transparent" color="#1C58D9" href="mailto:{{.Props.ActorEmail}}">
|
<mj-button padding-top="0px" padding-bottom="1px" font-size="14px" line-height="20px" background-color="transparent" color="#1C58D9" href="mailto:{{.Props.ActorEmail}}">
|
||||||
{{.Props.SendAnEmailTo}}
|
{{.Props.SendAnEmailTo}}
|
||||||
</mj-button>
|
</mj-button>
|
||||||
<mj-divider padding="0" css-class="divider" width="313px" border-width="1px" border-color="#3F4350"/>
|
<mj-divider padding="0" css-class="divider" width="313px" border-width="1px" border-color="#3F4350" />
|
||||||
<mj-raw>{{end}}</mj-raw>
|
<mj-raw>{{end}}</mj-raw>
|
||||||
<mj-raw>{{ if .Props.LogInToCustomerPortal}}</mj-raw>
|
<mj-raw>{{ if .Props.LogInToCustomerPortal}}</mj-raw>
|
||||||
<mj-button padding-top="6px" padding-bottom="1px" font-size="14px" line-height="20px" background-color="transparent" color="#1C58D9" href="{{.Props.PortalURL}}/console/cloud/ip-filtering">
|
<mj-button padding-top="6px" padding-bottom="1px" font-size="14px" line-height="20px" background-color="transparent" color="#1C58D9" href="{{.Props.PortalURL}}/console/cloud/ip-filtering">
|
||||||
{{.Props.LogInToCustomerPortal}}
|
{{.Props.LogInToCustomerPortal}}
|
||||||
</mj-button>
|
</mj-button>
|
||||||
<mj-divider padding="0px" css-class="divider" width="313px" border-width="1px" border-color="#3F4350"/>
|
<mj-divider padding="0px" css-class="divider" width="313px" border-width="1px" border-color="#3F4350" />
|
||||||
<mj-raw>{{end}}</mj-raw>
|
<mj-raw>{{end}}</mj-raw>
|
||||||
<mj-button padding-top="6px" font-size="14px" line-height="20px" background-color="transparent" color="#1C58D9" href="mailto:{{.Props.SupportEmail}}">
|
<mj-button padding-top="6px" font-size="14px" line-height="20px" background-color="transparent" color="#1C58D9" href="mailto:{{.Props.SupportEmail}}">
|
||||||
{{.Props.ContactSupport}}
|
{{.Props.ContactSupport}}
|
||||||
</mj-button>
|
</mj-button>
|
||||||
</mj-column>
|
</mj-column>
|
||||||
|
|||||||
@@ -84,6 +84,17 @@ export function completeStripeAddPaymentMethod(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getInstallation() {
|
||||||
|
return async () => {
|
||||||
|
try {
|
||||||
|
const installation = await Client4.getInstallation();
|
||||||
|
return {data: installation};
|
||||||
|
} catch (e: any) {
|
||||||
|
return {error: e.message};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function subscribeCloudSubscription(
|
export function subscribeCloudSubscription(
|
||||||
productId: string,
|
productId: string,
|
||||||
shippingAddress: Address = getBlankAddressWithCountry(),
|
shippingAddress: Address = getBlankAddressWithCountry(),
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ import {useDispatch} from 'react-redux';
|
|||||||
import {AlertOutlineIcon} from '@mattermost/compass-icons/components';
|
import {AlertOutlineIcon} from '@mattermost/compass-icons/components';
|
||||||
import type {AllowedIPRange, FetchIPResponse} from '@mattermost/types/config';
|
import type {AllowedIPRange, FetchIPResponse} from '@mattermost/types/config';
|
||||||
|
|
||||||
|
import type {DispatchFunc} from 'mattermost-redux/types/actions';
|
||||||
|
|
||||||
import {applyIPFilters, getCurrentIP, getIPFilters} from 'actions/admin_actions';
|
import {applyIPFilters, getCurrentIP, getIPFilters} from 'actions/admin_actions';
|
||||||
|
import {getInstallation} from 'actions/cloud';
|
||||||
import {closeModal, openModal} from 'actions/views/modals';
|
import {closeModal, openModal} from 'actions/views/modals';
|
||||||
|
|
||||||
import AdminHeader from 'components/widgets/admin_console/admin_header';
|
import AdminHeader from 'components/widgets/admin_console/admin_header';
|
||||||
@@ -27,7 +30,7 @@ import SaveChangesPanel from '../team_channel_settings/save_changes_panel';
|
|||||||
import './ip_filtering.scss';
|
import './ip_filtering.scss';
|
||||||
|
|
||||||
const IPFiltering = () => {
|
const IPFiltering = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch<DispatchFunc>();
|
||||||
const {formatMessage} = useIntl();
|
const {formatMessage} = useIntl();
|
||||||
const [ipFilters, setIpFilters] = useState<AllowedIPRange[] | null>(null);
|
const [ipFilters, setIpFilters] = useState<AllowedIPRange[] | null>(null);
|
||||||
const [originalIpFilters, setOriginalIpFilters] = useState<AllowedIPRange[] | null>(null);
|
const [originalIpFilters, setOriginalIpFilters] = useState<AllowedIPRange[] | null>(null);
|
||||||
@@ -35,8 +38,27 @@ const IPFiltering = () => {
|
|||||||
const [currentUsersIP, setCurrentUsersIP] = useState<string | null>(null);
|
const [currentUsersIP, setCurrentUsersIP] = useState<string | null>(null);
|
||||||
const [saving, setSaving] = useState<boolean>(false);
|
const [saving, setSaving] = useState<boolean>(false);
|
||||||
const [filterToggle, setFilterToggle] = useState<boolean>(false);
|
const [filterToggle, setFilterToggle] = useState<boolean>(false);
|
||||||
|
const [installationStatus, setInstallationStatus] = useState<string>('');
|
||||||
|
|
||||||
|
// savingMessage allows the component to change the label on the Save button in the SaveChangesPanel
|
||||||
|
const [savingMessage, setSavingMessage] = useState<string>('');
|
||||||
|
|
||||||
|
// savingDescription is a JSX element that will be displayed in the serverError bar on the SaveChangesPanel. This allows us to provide more information on loading while previous changes are applied
|
||||||
|
const [savingDescription, setSavingDescription] = useState<JSX.Element | null>(null);
|
||||||
|
|
||||||
|
const savingButtonMessages = {
|
||||||
|
SAVING_PREVIOUS_CHANGE: formatMessage({id: 'admin.ip_filtering.saving_previous_change', defaultMessage: 'Other changes being applied...'}),
|
||||||
|
SAVING_CHANGES: formatMessage({id: 'admin.ip_filtering.saving_changes', defaultMessage: 'Applying changes...'}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const savingDescriptionMessages = {
|
||||||
|
SAVING_PREVIOUS_CHANGE: formatMessage({id: 'admin.ip_filtering.saving_previous_change_description', defaultMessage: 'Please wait while changes from another admin are applied.'}),
|
||||||
|
SAVING_CHANGES: formatMessage({id: 'admin.ip_filtering.saving_changes_description', defaultMessage: 'Please wait while your changes are applied.'}),
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
getInstallationStatus();
|
||||||
|
|
||||||
getIPFilters((data: AllowedIPRange[]) => {
|
getIPFilters((data: AllowedIPRange[]) => {
|
||||||
setIpFilters(data);
|
setIpFilters(data);
|
||||||
setOriginalIpFilters(data);
|
setOriginalIpFilters(data);
|
||||||
@@ -57,7 +79,7 @@ const IPFiltering = () => {
|
|||||||
setSaveNeeded(haveFiltersChanged);
|
setSaveNeeded(haveFiltersChanged);
|
||||||
}, [ipFilters, originalIpFilters]);
|
}, [ipFilters, originalIpFilters]);
|
||||||
|
|
||||||
const currentIPIsInRange = () => {
|
const currentIPIsInRange = (): boolean => {
|
||||||
if (!filterToggle) {
|
if (!filterToggle) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -92,6 +114,61 @@ const IPFiltering = () => {
|
|||||||
}
|
}
|
||||||
}, [filterToggle]);
|
}, [filterToggle]);
|
||||||
|
|
||||||
|
function pollInstallationStatus() {
|
||||||
|
let installationFetchAttempts = 0;
|
||||||
|
const interval = setInterval(async () => {
|
||||||
|
if (installationFetchAttempts > 15) {
|
||||||
|
// Average time for provisioner to update is around 30 seconds. This allows up to 75 seconds before it will stop fetching, displaying an error
|
||||||
|
setSavingDescription((
|
||||||
|
<>
|
||||||
|
<AlertOutlineIcon size={16}/> {formatMessage({id: 'admin.ip_filtering.failed_to_fetch_installation_state', defaultMessage: 'Failed to fetch your workspace\'s status. Please try again later or contact support.'})}
|
||||||
|
</>
|
||||||
|
));
|
||||||
|
clearInterval(interval);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const result = await dispatch(getInstallation());
|
||||||
|
installationFetchAttempts++;
|
||||||
|
if (result.data) {
|
||||||
|
const {data} = result;
|
||||||
|
if (data.state === 'stable') {
|
||||||
|
setSaving(false);
|
||||||
|
setSavingDescription(null);
|
||||||
|
clearInterval(interval);
|
||||||
|
}
|
||||||
|
setInstallationStatus(data.state);
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getInstallationStatus() {
|
||||||
|
const result = await dispatch(getInstallation());
|
||||||
|
if (result.data) {
|
||||||
|
const {data} = result;
|
||||||
|
setInstallationStatus(data.state);
|
||||||
|
if (installationStatus === '' && data.state !== 'stable') {
|
||||||
|
// This is the first load of the page, and the installation is not stable, so we must lock saving until it becomes stable
|
||||||
|
setSaving(true);
|
||||||
|
|
||||||
|
// Override the default messages for the save button and the error message to be communicative of the current state to the user
|
||||||
|
setSavingMessage(savingButtonMessages.SAVING_PREVIOUS_CHANGE);
|
||||||
|
changeSavingDescription(savingDescriptionMessages.SAVING_PREVIOUS_CHANGE);
|
||||||
|
}
|
||||||
|
if (data.state !== 'stable') {
|
||||||
|
pollInstallationStatus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeSavingDescription(text: string) {
|
||||||
|
setSavingDescription((
|
||||||
|
<div className='saving-message-description'>
|
||||||
|
{text}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function handleEditFilter(filter: AllowedIPRange, existingRange?: AllowedIPRange) {
|
function handleEditFilter(filter: AllowedIPRange, existingRange?: AllowedIPRange) {
|
||||||
setIpFilters((prevIpFilters) => {
|
setIpFilters((prevIpFilters) => {
|
||||||
if (!prevIpFilters) {
|
if (!prevIpFilters) {
|
||||||
@@ -155,13 +232,16 @@ const IPFiltering = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleSave() {
|
function handleSave() {
|
||||||
|
setInstallationStatus('update-requested');
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
|
setSavingMessage(savingButtonMessages.SAVING_CHANGES);
|
||||||
|
changeSavingDescription(savingDescriptionMessages.SAVING_CHANGES);
|
||||||
dispatch(closeModal(ModalIdentifiers.IP_FILTERING_SAVE_CONFIRMATION_MODAL));
|
dispatch(closeModal(ModalIdentifiers.IP_FILTERING_SAVE_CONFIRMATION_MODAL));
|
||||||
|
|
||||||
const success = (data: AllowedIPRange[]) => {
|
const success = (data: AllowedIPRange[]) => {
|
||||||
setIpFilters(data);
|
setIpFilters(data);
|
||||||
setSaving(false);
|
setOriginalIpFilters(data);
|
||||||
setSaveNeeded(false);
|
getInstallationStatus();
|
||||||
};
|
};
|
||||||
|
|
||||||
applyIPFilters(ipFilters ?? [], success);
|
applyIPFilters(ipFilters ?? [], success);
|
||||||
@@ -220,6 +300,10 @@ const IPFiltering = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const saveBarError = () => {
|
const saveBarError = () => {
|
||||||
|
if (savingDescription !== null) {
|
||||||
|
return savingDescription;
|
||||||
|
}
|
||||||
|
|
||||||
if (currentIPIsInRange()) {
|
if (currentIPIsInRange()) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -256,10 +340,11 @@ const IPFiltering = () => {
|
|||||||
</div>
|
</div>
|
||||||
<SaveChangesPanel
|
<SaveChangesPanel
|
||||||
saving={saving}
|
saving={saving}
|
||||||
saveNeeded={saveNeeded}
|
saveNeeded={saveNeeded || installationStatus !== 'stable'}
|
||||||
isDisabled={!currentIPIsInRange}
|
isDisabled={!currentIPIsInRange() || installationStatus !== 'stable'}
|
||||||
onClick={handleSaveClick}
|
onClick={handleSaveClick}
|
||||||
serverError={saveBarError()}
|
serverError={saveBarError()}
|
||||||
|
savingMessage={savingMessage}
|
||||||
cancelLink=''
|
cancelLink=''
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
.IPFiltering {
|
.IPFiltering {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
|
.admin-console-save {
|
||||||
|
.btn.btn-primary:disabled {
|
||||||
|
color: rgba(var(--center-channel-color-rgb), 0.32) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.MainPanel {
|
.MainPanel {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -78,5 +84,10 @@
|
|||||||
margin-right: 7px;
|
margin-right: 7px;
|
||||||
margin-left: 7px;
|
margin-left: 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.saving-message-description {
|
||||||
|
margin-left: 16px;
|
||||||
|
color: rgba(var(--center-channel-color-rgb), 0.72) !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {IntlProvider} from 'react-intl';
|
|||||||
import {Provider} from 'react-redux';
|
import {Provider} from 'react-redux';
|
||||||
import {BrowserRouter as Router} from 'react-router-dom';
|
import {BrowserRouter as Router} from 'react-router-dom';
|
||||||
|
|
||||||
|
import type {Installation} from '@mattermost/types/cloud';
|
||||||
import type {AllowedIPRange, FetchIPResponse} from '@mattermost/types/config';
|
import type {AllowedIPRange, FetchIPResponse} from '@mattermost/types/config';
|
||||||
|
|
||||||
import {Client4} from 'mattermost-redux/client';
|
import {Client4} from 'mattermost-redux/client';
|
||||||
@@ -36,11 +37,13 @@ describe('IPFiltering', () => {
|
|||||||
const applyIPFiltersMock = jest.fn(() => Promise.resolve(ipFilters));
|
const applyIPFiltersMock = jest.fn(() => Promise.resolve(ipFilters));
|
||||||
const getIPFiltersMock = jest.fn(() => Promise.resolve(ipFilters));
|
const getIPFiltersMock = jest.fn(() => Promise.resolve(ipFilters));
|
||||||
const getCurrentIPMock = jest.fn(() => Promise.resolve({ip: currentIP} as FetchIPResponse));
|
const getCurrentIPMock = jest.fn(() => Promise.resolve({ip: currentIP} as FetchIPResponse));
|
||||||
|
const getInstallationMock = jest.fn(() => Promise.resolve({id: 'abc123', state: 'stable'} as Installation));
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
Client4.applyIPFilters = applyIPFiltersMock;
|
Client4.applyIPFilters = applyIPFiltersMock;
|
||||||
Client4.getIPFilters = getIPFiltersMock;
|
Client4.getIPFilters = getIPFiltersMock;
|
||||||
Client4.getCurrentIP = getCurrentIPMock;
|
Client4.getCurrentIP = getCurrentIPMock;
|
||||||
|
Client4.getInstallation = getInstallationMock;
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockedStore = configureStore({
|
const mockedStore = configureStore({
|
||||||
@@ -201,4 +204,68 @@ describe('IPFiltering', () => {
|
|||||||
expect(applyIPFiltersMock).toHaveBeenCalledTimes(1);
|
expect(applyIPFiltersMock).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Save button is disabled when users IP is not within the allowed ranges', async () => {
|
||||||
|
const {getByLabelText, getByText, queryByText, getByTestId} = render(wrapWithIntlProviderAndStore(<IPFiltering/>));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(getByText('Test IP Filter')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.mouseEnter(screen.getByText('Test IP Filter'));
|
||||||
|
fireEvent.click(screen.getByRole('button', {
|
||||||
|
name: /Edit/i,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const descriptionInput = getByLabelText('Enter a name for this rule');
|
||||||
|
const cidrInput = getByLabelText('Enter IP Range');
|
||||||
|
const saveButton = screen.getByTestId('save-add-edit-button');
|
||||||
|
|
||||||
|
fireEvent.change(cidrInput, {target: {value: '192.168.0.0/16'}});
|
||||||
|
fireEvent.change(descriptionInput, {target: {value: 'zzzzzfilter'}});
|
||||||
|
fireEvent.click(saveButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(getByText('zzzzzfilter')).toBeInTheDocument();
|
||||||
|
expect(getByText('192.168.0.0/16')).toBeInTheDocument();
|
||||||
|
|
||||||
|
// ensure that the old description is gone, because we've now changed it
|
||||||
|
expect(queryByText('Test IP Filter')).toBeNull();
|
||||||
|
expect(getByTestId('saveSetting')).toBeDisabled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Save button is disabled with a spinner when the page is loaded with a not-stable installation', async () => {
|
||||||
|
const getInstallationNotStableMock = jest.fn(() => Promise.resolve({id: 'abc123', state: 'update-in-progress'} as Installation));
|
||||||
|
Client4.getInstallation = getInstallationNotStableMock;
|
||||||
|
|
||||||
|
jest.useFakeTimers();
|
||||||
|
const {getByText, queryByText} = render(wrapWithIntlProviderAndStore(<IPFiltering/>));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId('filterToggle-button')).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('button', {pressed: true})).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByTestId('filterToggle-button'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByRole('button', {pressed: false})).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(queryByText('Test IP Filter')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getByText('Other changes being applied...')).toBeInTheDocument();
|
||||||
|
expect(getByText('Other changes being applied...').closest('button')).toBeDisabled();
|
||||||
|
|
||||||
|
// Adjust mock so it now returns a stable state
|
||||||
|
Client4.getInstallation = getInstallationMock;
|
||||||
|
|
||||||
|
jest.advanceTimersByTime(5100);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(getByText('Save')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,13 +2,11 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {FormattedMessage} from 'react-intl';
|
import {FormattedMessage, useIntl} from 'react-intl';
|
||||||
|
|
||||||
import BlockableLink from 'components/admin_console/blockable_link';
|
import BlockableLink from 'components/admin_console/blockable_link';
|
||||||
import SaveButton from 'components/save_button';
|
import SaveButton from 'components/save_button';
|
||||||
|
|
||||||
import {localizeMessage} from 'utils/utils';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
saving: boolean;
|
saving: boolean;
|
||||||
saveNeeded: boolean;
|
saveNeeded: boolean;
|
||||||
@@ -16,16 +14,18 @@ type Props = {
|
|||||||
cancelLink: string;
|
cancelLink: string;
|
||||||
serverError?: JSX.Element;
|
serverError?: JSX.Element;
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
|
savingMessage?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SaveChangesPanel = ({saveNeeded, onClick, saving, serverError, cancelLink, isDisabled}: Props) => {
|
const SaveChangesPanel = ({saveNeeded, onClick, saving, serverError, cancelLink, isDisabled, savingMessage}: Props) => {
|
||||||
|
const {formatMessage} = useIntl();
|
||||||
return (
|
return (
|
||||||
<div className='admin-console-save'>
|
<div className='admin-console-save'>
|
||||||
<SaveButton
|
<SaveButton
|
||||||
saving={saving}
|
saving={saving}
|
||||||
disabled={isDisabled || !saveNeeded}
|
disabled={isDisabled || !saveNeeded}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
savingMessage={localizeMessage('admin.team_channel_settings.saving', 'Saving Config...')}
|
savingMessage={savingMessage ?? formatMessage({id: 'admin.team_channel_settings.saving', defaultMessage: 'Saving Config...'})}
|
||||||
/>
|
/>
|
||||||
{
|
{
|
||||||
cancelLink !== '' &&
|
cancelLink !== '' &&
|
||||||
|
|||||||
@@ -1204,6 +1204,7 @@
|
|||||||
"admin.ip_filtering.enable_ip_filtering": "Enable IP Filtering",
|
"admin.ip_filtering.enable_ip_filtering": "Enable IP Filtering",
|
||||||
"admin.ip_filtering.enable_ip_filtering_description": "Limit access to your workspace by IP address. <learnmore>Learn more in the docs</learnmore>",
|
"admin.ip_filtering.enable_ip_filtering_description": "Limit access to your workspace by IP address. <learnmore>Learn more in the docs</learnmore>",
|
||||||
"admin.ip_filtering.error_on_page": "Your IP address is not included in your filters",
|
"admin.ip_filtering.error_on_page": "Your IP address is not included in your filters",
|
||||||
|
"admin.ip_filtering.failed_to_fetch_installation_state": "Failed to fetch your workspace's status. Please try again later or contact support.",
|
||||||
"admin.ip_filtering.filter_name": "Filter Name",
|
"admin.ip_filtering.filter_name": "Filter Name",
|
||||||
"admin.ip_filtering.include_your_ip": "Include your IP address in at least one of the rules below to continue.",
|
"admin.ip_filtering.include_your_ip": "Include your IP address in at least one of the rules below to continue.",
|
||||||
"admin.ip_filtering.ip_address_range": "IP Address Range",
|
"admin.ip_filtering.ip_address_range": "IP Address Range",
|
||||||
@@ -1216,6 +1217,10 @@
|
|||||||
"admin.ip_filtering.save": "Save",
|
"admin.ip_filtering.save": "Save",
|
||||||
"admin.ip_filtering.save_disclaimer_subtitle": "If you happen to block yourself with these settings, your workspace owner can log in to the <customerportal>Customer Portal</customerportal> to disable IP filtering to restore access.",
|
"admin.ip_filtering.save_disclaimer_subtitle": "If you happen to block yourself with these settings, your workspace owner can log in to the <customerportal>Customer Portal</customerportal> to disable IP filtering to restore access.",
|
||||||
"admin.ip_filtering.save_disclaimer_title": "Using the Customer Portal to restore access",
|
"admin.ip_filtering.save_disclaimer_title": "Using the Customer Portal to restore access",
|
||||||
|
"admin.ip_filtering.saving_changes": "Applying changes...",
|
||||||
|
"admin.ip_filtering.saving_changes_description": "Please wait while your changes are applied.",
|
||||||
|
"admin.ip_filtering.saving_previous_change": "Other changes being applied...",
|
||||||
|
"admin.ip_filtering.saving_previous_change_description": "Please wait while changes from another admin are applied.",
|
||||||
"admin.ip_filtering.turn_off_ip_filtering": "Are you sure you want to turn off IP Filtering? <strong>All IP addresses will have access to the workspace.</strong>",
|
"admin.ip_filtering.turn_off_ip_filtering": "Are you sure you want to turn off IP Filtering? <strong>All IP addresses will have access to the workspace.</strong>",
|
||||||
"admin.ip_filtering.update_filter": "Update filter",
|
"admin.ip_filtering.update_filter": "Update filter",
|
||||||
"admin.ip_filtering.yes_disable_ip_filtering": "Yes, disable IP Filtering",
|
"admin.ip_filtering.yes_disable_ip_filtering": "Yes, disable IP Filtering",
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
Feedback,
|
Feedback,
|
||||||
WorkspaceDeletionRequest,
|
WorkspaceDeletionRequest,
|
||||||
NewsletterRequestBody,
|
NewsletterRequestBody,
|
||||||
|
Installation,
|
||||||
} from '@mattermost/types/cloud';
|
} from '@mattermost/types/cloud';
|
||||||
import {
|
import {
|
||||||
SelfHostedSignupForm,
|
SelfHostedSignupForm,
|
||||||
@@ -3916,6 +3917,13 @@ export default class Client4 {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getInstallation = () => {
|
||||||
|
return this.doFetch<Installation>(
|
||||||
|
`${this.getCloudRoute()}/installation`,
|
||||||
|
{method: 'get'},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
getRenewalLink = () => {
|
getRenewalLink = () => {
|
||||||
return this.doFetch<{renewal_link: string}>(
|
return this.doFetch<{renewal_link: string}>(
|
||||||
`${this.getBaseRoute()}/license/renewal`,
|
`${this.getBaseRoute()}/license/renewal`,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {AllowedIPRange} from './config';
|
||||||
import {ValueOf} from './utilities';
|
import {ValueOf} from './utilities';
|
||||||
|
|
||||||
export type CloudState = {
|
export type CloudState = {
|
||||||
@@ -26,6 +27,12 @@ export type CloudState = {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Installation = {
|
||||||
|
id: string;
|
||||||
|
state: string;
|
||||||
|
allowed_ip_ranges: AllowedIPRange[];
|
||||||
|
}
|
||||||
|
|
||||||
export type Subscription = {
|
export type Subscription = {
|
||||||
id: string;
|
id: string;
|
||||||
customer_id: string;
|
customer_id: string;
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user